pax_global_header00006660000000000000000000000064146567243240014527gustar00rootroot0000000000000052 comment=0267b5b32ef79f02e1b26eca53813b482cb02a99 yalexs-8.0.2/000077500000000000000000000000001465672432400130435ustar00rootroot00000000000000yalexs-8.0.2/.github/000077500000000000000000000000001465672432400144035ustar00rootroot00000000000000yalexs-8.0.2/.github/workflows/000077500000000000000000000000001465672432400164405ustar00rootroot00000000000000yalexs-8.0.2/.github/workflows/ci.yml000066400000000000000000000052061465672432400175610ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - uses: pre-commit/action@v3.0.0 # Make sure commit messages follow the conventional commits convention: # https://www.conventionalcommits.org commitlint: name: Lint Commit Messages runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v6 test: strategy: fail-fast: false matrix: python-version: - "3.9" - "3.10" - "3.11" - "3.12" os: - ubuntu-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1.3.4 - name: Install Dependencies run: poetry install shell: bash - name: Test with Pytest run: poetry run pytest --cov-report=xml shell: bash - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} release: needs: - test - lint - commitlint runs-on: ubuntu-latest environment: release concurrency: release permissions: id-token: write contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.head_ref || github.ref_name }} # Do a dry run of PSR - name: Test release uses: python-semantic-release/python-semantic-release@v8.5.1 if: github.ref_name != 'main' with: root_options: --noop # On main branch: actual PSR + upload to PyPI & GitHub - name: Release uses: python-semantic-release/python-semantic-release@v8.5.1 id: release if: github.ref_name == 'main' with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 if: steps.release.outputs.released == 'true' - name: Publish package distributions to GitHub Releases uses: python-semantic-release/upload-to-gh-release@main if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} yalexs-8.0.2/.gitignore000066400000000000000000000022341465672432400150340ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.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 .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .idea # IDEs .vscode/ yalexs-8.0.2/.pre-commit-config.yaml000066400000000000000000000033351465672432400173300ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks exclude: "CHANGELOG.md" default_stages: [commit] ci: autofix_commit_msg: "chore(pre-commit.ci): auto fixes" autoupdate_commit_msg: "chore(pre-commit.ci): pre-commit autoupdate" repos: - repo: https://github.com/commitizen-tools/commitizen rev: v3.29.0 hooks: - id: commitizen stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: debug-statements - id: check-builtin-literals - id: check-case-conflict - id: check-docstring-first - id: check-json - id: check-toml - id: check-xml - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: trailing-whitespace - id: debug-statements - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: - id: prettier args: ["--tab-width", "2"] - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: - id: flake8 # - repo: https://github.com/pre-commit/mirrors-mypy #rev: v0.931 #hooks: #- id: mypy # additional_dependencies: [] - repo: https://github.com/PyCQA/bandit rev: 1.7.9 hooks: - id: bandit args: [-x, tests] yalexs-8.0.2/CHANGELOG.md000066400000000000000000001615721465672432400146700ustar00rootroot00000000000000# CHANGELOG ## v8.0.2 (2024-08-13) ### Fix * fix: library version pins being too narrow (#149) ([`9cd8b13`](https://github.com/bdraco/yalexs/commit/9cd8b13475b50662f81885f9a7948d2f782a2616)) ## v8.0.1 (2024-08-13) ### Fix * fix: doorbells are not supported with yale global brand (#148) ([`a85ca5c`](https://github.com/bdraco/yalexs/commit/a85ca5ccf33fc082452ab2b790912aaa61bdbd0c)) ## v8.0.0 (2024-08-13) ### Breaking * feat!: add yale global support (#146) ([`5dc4bd7`](https://github.com/bdraco/yalexs/commit/5dc4bd7c41561e09a5e34440a163ae5d4b27a3af)) ### Fix * fix: force release (#147) ([`b409c80`](https://github.com/bdraco/yalexs/commit/b409c80eaefc854938449d66419ae49f843efd73)) ## v7.1.3 (2024-08-13) ### Fix * fix: temp use yale home keys for key global (#145) ([`a22beb8`](https://github.com/bdraco/yalexs/commit/a22beb869e239ba53ed62e622b76056db4c712d6)) ## v7.1.2 (2024-08-13) ### Fix * fix: handle empty login method for oauth (#144) ([`b686ceb`](https://github.com/bdraco/yalexs/commit/b686ceba5fde4247c32803730fe17106125e6544)) ## v7.1.1 (2024-08-13) ### Fix * fix: handle oauth when there is no username saved (#143) ([`317f674`](https://github.com/bdraco/yalexs/commit/317f6747a0a09129de9c44ae8e2872ea060571c9)) ## v7.1.0 (2024-08-13) ### Feature * feat: allow passing an auth class (#142) ([`377d75c`](https://github.com/bdraco/yalexs/commit/377d75cb4cb2cee6d0294fbf3acebb1b3c39798c)) ## v7.0.0 (2024-08-13) ### Breaking * feat!: remove sync api (#141) ([`6a0db94`](https://github.com/bdraco/yalexs/commit/6a0db9426e4ba586a3449ff2a7d549f0035a1255)) ### Feature * feat: add yale global brand (#140) ([`597e9f9`](https://github.com/bdraco/yalexs/commit/597e9f9bc6693093dc1bdeb2c56b41ba22d161ca)) ## v6.6.0 (2024-08-13) ### Feature * feat: make fetching the access token a coro (#139) ([`451080d`](https://github.com/bdraco/yalexs/commit/451080d50f261566cb8a4feeeac6a92e5b76634d)) ## v6.5.1 (2024-08-13) ### Fix * fix: cleanup _async_dict_to_api for oauth conversion (#138) ([`a41474d`](https://github.com/bdraco/yalexs/commit/a41474d98bc1f56fe014f9a1ac09c32aed7afc14)) ## v6.5.0 (2024-08-12) ### Feature * feat: cleanup activity construction code (#136) ([`b994c0b`](https://github.com/bdraco/yalexs/commit/b994c0be4f3500abc954a718a77c78fa332498f2)) ## v6.4.4 (2024-08-05) ### Fix * fix: tests with aiohttp >= 3.10.0 (#134) ([`a25deb4`](https://github.com/bdraco/yalexs/commit/a25deb453a19854cd5f5c7eb1cc3114718248880)) ## v6.4.3 (2024-07-15) ### Fix * fix: replace pubnub with freenub to keep MIT license (#130) ([`57e41af`](https://github.com/bdraco/yalexs/commit/57e41af16331d2031294107cd1f438fab3e3a524)) ## v6.4.2 (2024-07-07) ### Fix * fix: keypad battery level for non-english locales (#128) ([`cca641f`](https://github.com/bdraco/yalexs/commit/cca641ff829984f7144be7fccf29ca50816aa38b)) ## v6.4.1 (2024-06-23) ### Fix * fix: blocking I/O in the event loop to stat the auth file (#123) ([`44ce0c4`](https://github.com/bdraco/yalexs/commit/44ce0c44c12c7c18ec12d83261346fdc25917afe)) ## v6.4.0 (2024-06-19) ### Feature * feat: abstract sources to tell which ones are push (#122) ([`7b0fdb9`](https://github.com/bdraco/yalexs/commit/7b0fdb90d4d67423c8e51fabc3098243e7805e8d)) ## v6.3.0 (2024-06-18) ### Feature * feat: expose the brand on the data object for backwards compat (#121) ([`1e04f18`](https://github.com/bdraco/yalexs/commit/1e04f18c6c9479f680302f2afa2c352454ba2efc)) ## v6.2.0 (2024-06-18) ### Feature * feat: add async_get_doorbell_image method to the YaleXSData object (#120) ([`a33b842`](https://github.com/bdraco/yalexs/commit/a33b84237721f28a43c95dce81ec6400868a9e15)) ## v6.1.1 (2024-06-18) ### Fix * fix: handle teardown if setup is cancelled (#119) ([`cb50051`](https://github.com/bdraco/yalexs/commit/cb500517d3cb3a6a413fdd8515f3de21a6a0523b)) ## v6.1.0 (2024-06-18) ### Feature * feat: abstract the need to check pubnub since it yale will use websocets (#118) ([`4aabe59`](https://github.com/bdraco/yalexs/commit/4aabe59cb19b9dcece2297900d325dca4e837175)) ## v6.0.0 (2024-06-10) ### Breaking * feat!: Add data module (#116) ([`14d6513`](https://github.com/bdraco/yalexs/commit/14d65137a3967d0a9f10264b49dc591f643d9f3a)) ### Fix * fix: bump commitlint to v6 (#117) ([`af95d48`](https://github.com/bdraco/yalexs/commit/af95d48665bb3e3c44a2b60b95600473a546b4df)) ## v5.2.0 (2024-06-09) ### Feature * feat: improve performance of subscribers (#115) ([`98bb11a`](https://github.com/bdraco/yalexs/commit/98bb11a5eedd2188cca73d39e21101a57c6e3415)) ## v5.1.2 (2024-06-09) ### Fix * fix: allow pubnub unsub to complete since nothing currently waits (#114) ([`82b0c58`](https://github.com/bdraco/yalexs/commit/82b0c58a0ed64afa107145be2ad161039e6a8a3a)) ## v5.1.1 (2024-06-09) ### Fix * fix: cleanup duplicate code in pubnub (#113) ([`6c189e2`](https://github.com/bdraco/yalexs/commit/6c189e22af847a2760522d3f75c9ef7250cbeadc)) ## v5.1.0 (2024-06-09) ### Feature * feat: cleanup pubnub code (#112) ([`67fc84a`](https://github.com/bdraco/yalexs/commit/67fc84a838bf584ce57e10a9294d066c4c0b67a6)) ## v5.0.1 (2024-06-09) ### Fix * fix: typing on async_create_pubnub (#111) ([`c02b3e8`](https://github.com/bdraco/yalexs/commit/c02b3e82dcb9ec540a090bbb90cdf9b0ac0b26be)) ## v5.0.0 (2024-06-09) ### Breaking * fix!: async_create_pubnub now returns a coro that needs to be awaited… (#110) ([`e1de91a`](https://github.com/bdraco/yalexs/commit/e1de91a428280808e20db6582472cd0e2fe938ba)) ## v4.0.0 (2024-06-08) ### Breaking * feat!: add manager (#109) ([`107a747`](https://github.com/bdraco/yalexs/commit/107a747d040c3a233845adaee1e8cbc0c27f05d0)) ## v3.2.0 (2024-06-08) ### Feature * feat: switch to poetry (#108) ([`e7d4609`](https://github.com/bdraco/yalexs/commit/e7d460944c803be814b539fa4d439d12090d0b74)) ## v3.1.0 (2024-05-01) ### Unknown * Bump version: 3.0.1 → 3.1.0 ([`74ab415`](https://github.com/bdraco/yalexs/commit/74ab415bf73c0ba6e65b87e1d154fda32a78fa9b)) * async_unlatch_return_activities tests and fixtures (#103) Co-authored-by: J. Nick Koston <nick@koston.org> ([`b0b4d94`](https://github.com/bdraco/yalexs/commit/b0b4d9479bcfd0a87a46ce3ebe9665caf9b09b54)) * fix codecov by adding token (#106) ([`79070f7`](https://github.com/bdraco/yalexs/commit/79070f7340a60dfe6c7391e099f71902e9459fdb)) * add unlatch property to lock (#105) ([`28b4263`](https://github.com/bdraco/yalexs/commit/28b42632eb2c8ef06a577990abc3bdb55d4b6193)) ## v3.0.1 (2024-04-03) ### Unknown * Bump version: 3.0.0 → 3.0.1 ([`5897263`](https://github.com/bdraco/yalexs/commit/58972630c697fd5f64450a2044590391d9da2eb0)) * Improve performance of activity processing (#104) ([`3089b70`](https://github.com/bdraco/yalexs/commit/3089b708b4b84c06fb9d2ca0dde610eafe4c5c21)) ## v3.0.0 (2024-03-19) ### Unknown * Bump version: 2.0.0 → 3.0.0 ([`3dc3750`](https://github.com/bdraco/yalexs/commit/3dc3750af51952783b8d604d681254c28657a825)) * Add unlatch action (#101) ([`26d1305`](https://github.com/bdraco/yalexs/commit/26d130510bcc88d3618e1b3f552744108952aa7c)) ## v2.0.0 (2024-02-28) ### Unknown * Bump version: 1.11.4 → 2.0.0 ([`2fb5f85`](https://github.com/bdraco/yalexs/commit/2fb5f85df8dcaafd8f5f819bfcde8bd91d639a44)) * Camera content-token raise error on expiry (#97) ([`0719fb5`](https://github.com/bdraco/yalexs/commit/0719fb56e3c599f1783357e6497077d90e99db51)) ## v1.11.4 (2024-02-27) ### Unknown * Bump version: 1.11.3 → 1.11.4 ([`7bc4617`](https://github.com/bdraco/yalexs/commit/7bc4617288cbf753ee3ef9a04b44070431ffaf4a)) * Bump pubnub to 7.4.1 (#98) ([`c4ffd9a`](https://github.com/bdraco/yalexs/commit/c4ffd9a202ab0570f7d55a91ff95d6eea364b32b)) ## v1.11.3 (2024-02-26) ### Unknown * Bump version: 1.11.2 → 1.11.3 ([`6ff849e`](https://github.com/bdraco/yalexs/commit/6ff849ead7849169aacdf76feb1ee8b24ea7bb37)) * Don't create activity from lock status polls (#95) ([`289da40`](https://github.com/bdraco/yalexs/commit/289da4019cd5393dab2c8e0c33bb9621a43a64db)) ## v1.11.2 (2024-02-10) ### Unknown * Bump version: 1.11.1 → 1.11.2 ([`9a68e17`](https://github.com/bdraco/yalexs/commit/9a68e17ac4292a914520759d88169a9a85668caa)) * Add missing actions for home key (#94) ([`83e6df7`](https://github.com/bdraco/yalexs/commit/83e6df7d0d3401dd0483c9bc075550ba88e19e95)) ## v1.11.1 (2024-02-09) ### Unknown * Bump version: 1.11.0 → 1.11.1 ([`2dd2f23`](https://github.com/bdraco/yalexs/commit/2dd2f23505a3ba2bad1d9cdd6626b193f1469803)) * Add contentToken Auth header for camera images (Yale home) (#92) Co-authored-by: Alexander Björck <alexander.bjorck@assaabloy.com> Co-authored-by: J. Nick Koston <nick@koston.org> ([`b41e8cb`](https://github.com/bdraco/yalexs/commit/b41e8cbf250b29d45e3b962e1c69d3cb3a39259b)) ## v1.11.0 (2024-01-24) ### Unknown * Bump version: 1.10.0 → 1.11.0 ([`3474eb0`](https://github.com/bdraco/yalexs/commit/3474eb00fe3fb7fa3855b3e3df58b51e2d9004de)) * Add ssl context helper (#90) ([`a004dd9`](https://github.com/bdraco/yalexs/commit/a004dd99ad9e73c09c25311df298bf702df07db7)) ## v1.10.0 (2023-09-21) ### Unknown * Bump version: 1.9.0 → 1.10.0 ([`c75cb85`](https://github.com/bdraco/yalexs/commit/c75cb850d391758f8cf46c538d65b3344517659f)) * Add more activity types (#87) ([`983fc58`](https://github.com/bdraco/yalexs/commit/983fc580c7e78c6903ee0d8976b9d22159e4d349)) ## v1.9.0 (2023-09-13) ### Unknown * Bump version: 1.8.1 → 1.9.0 ([`4ea45b2`](https://github.com/bdraco/yalexs/commit/4ea45b2fc6af504f0a086c1e9a889eb2f30c56d3)) ## v1.8.1 (2023-09-13) ### Unknown * Bump version: 1.8.0 → 1.8.1 ([`58692fe`](https://github.com/bdraco/yalexs/commit/58692fe5760c0f27e0b7d08025e5c6a30deda05a)) * Add support for type 10 doormans (#85) ([`1a5b242`](https://github.com/bdraco/yalexs/commit/1a5b242fc00a8fd7d749c3284a2e97a171f5408e)) ## v1.8.0 (2023-08-21) ### Unknown * Bump version: 1.7.0 → 1.8.0 ([`f473535`](https://github.com/bdraco/yalexs/commit/f473535b6f033078045ee1ac82a153530ed972ee)) * Cache timestamp conversion (#84) ([`8fc664d`](https://github.com/bdraco/yalexs/commit/8fc664d70a7d8c26e6161503f43581588066c7b8)) ## v1.7.0 (2023-08-21) ### Unknown * Bump version: 1.6.0 → 1.7.0 ([`9572fe1`](https://github.com/bdraco/yalexs/commit/9572fe175afc2809fe496748840d30704fa2b2af)) * Speed up activity processing (#83) ([`4c4a06e`](https://github.com/bdraco/yalexs/commit/4c4a06e72b1ce15ef6a5f4cedfcbb4b7c1034576)) ## v1.6.0 (2023-08-21) ### Unknown * Bump version: 1.5.2 → 1.6.0 ([`531dd24`](https://github.com/bdraco/yalexs/commit/531dd24e116befadaf554c21c4bbb6035dd570a2)) * Defer construction of activities and use property caching (#82) ([`d95f1d7`](https://github.com/bdraco/yalexs/commit/d95f1d71c77878ee0fc53ffd8067f8176f8355e5)) ## v1.5.2 (2023-08-07) ### Feature * feat: add support for detecting locks that have a doorbell (#81) ([`54eec8e`](https://github.com/bdraco/yalexs/commit/54eec8ee6db112f4573f0ba965cb0953b2497e6a)) ### Unknown * Bump version: 1.5.1 → 1.5.2 ([`47d1500`](https://github.com/bdraco/yalexs/commit/47d150082988e821aa67734423d226b24745f00d)) ## v1.5.1 (2023-05-22) ### Unknown * Bump version: 1.5.0 → 1.5.1 ([`e836026`](https://github.com/bdraco/yalexs/commit/e836026d639764e92b6cc9294dee6d4d2a6b3cc3)) * Fix missing await in api retry (#75) ([`45d1de3`](https://github.com/bdraco/yalexs/commit/45d1de38b586992fb0dee7bcecd7f5e157708943)) ## v1.5.0 (2023-05-22) ### Unknown * Bump version: 1.4.6 → 1.5.0 ([`7bff08c`](https://github.com/bdraco/yalexs/commit/7bff08cee84ed099b4f59c58b7f97a86fd33f8d3)) * Speed up parsing datetimes (#74) ([`9c92c0c`](https://github.com/bdraco/yalexs/commit/9c92c0c0e47e3a0fb3ec9a5f55f3f723b5d48d27)) * Add support for fetching the configuration URL (#73) ([`8ba22b9`](https://github.com/bdraco/yalexs/commit/8ba22b986f6485d8a5c820be00f1ac3890db2a4a)) * Fix typing on async_create_pubnub (#72) ([`06ccffb`](https://github.com/bdraco/yalexs/commit/06ccffb5b7e84ffdc8acfb073202a257ce7f53cd)) * Add test for manuallock (#71) ([`94d28d3`](https://github.com/bdraco/yalexs/commit/94d28d399371f4defa591b093cfe3cd644d86a91)) * Add test for manualunlock (#70) ([`21aac10`](https://github.com/bdraco/yalexs/commit/21aac1040c58777593ad8ab512fd52855c06e560)) * Use relative imports (#69) ([`7165e78`](https://github.com/bdraco/yalexs/commit/7165e7879f407aa170ea1b5ea08c0d090c503eb9)) ## v1.4.6 (2023-05-18) ### Unknown * Bump version: 1.4.5 → 1.4.6 ([`61f88a2`](https://github.com/bdraco/yalexs/commit/61f88a236f0d8cb0906af0a068f4c02d26bcbb0e)) * Add more retries (#68) ([`5fa20a8`](https://github.com/bdraco/yalexs/commit/5fa20a8a9b91a4176a5e8b7f8555e43e3e9063b9)) * Handle more cases of bridge offline (#67) ([`46805b8`](https://github.com/bdraco/yalexs/commit/46805b8b5ea599faddc3d5768841c49d63892b4c)) ## v1.4.5 (2023-05-18) ### Unknown * Bump version: 1.4.4 → 1.4.5 ([`87bc81c`](https://github.com/bdraco/yalexs/commit/87bc81c384df69c0b3678b5d3f6cfece5d2f8f2a)) * Update tokens for Yale Home (#66) ([`5466b3c`](https://github.com/bdraco/yalexs/commit/5466b3c33678b837ac046efa70cf43dea10d0eb9)) ## v1.4.4 (2023-05-17) ### Unknown * Bump version: 1.4.3 → 1.4.4 ([`105045d`](https://github.com/bdraco/yalexs/commit/105045d4cc05e2f80588809bfd728a4207ebb135)) * Adjust brand header based on brand (#65) ([`aaba169`](https://github.com/bdraco/yalexs/commit/aaba1692051ed28177fb35d44d04f0450812fb26)) ## v1.4.3 (2023-05-17) ### Unknown * Bump version: 1.4.2 → 1.4.3 ([`7678a09`](https://github.com/bdraco/yalexs/commit/7678a09337018a3e794d896eb3cbc244b41a3a0c)) * Fix handling wrong code and hide password in logging (#64) ([`d3a6010`](https://github.com/bdraco/yalexs/commit/d3a6010db33d1e022df71647525cff0b99256b95)) ## v1.4.2 (2023-05-17) ### Unknown * Bump version: 1.4.1 → 1.4.2 ([`cf048d8`](https://github.com/bdraco/yalexs/commit/cf048d876c567721e78cd729df8ff1b1da132b88)) * Fix some typing (#63) ([`3fddde8`](https://github.com/bdraco/yalexs/commit/3fddde8887118820493c14e108e02c574125e58b)) ## v1.4.1 (2023-05-17) ### Unknown * Bump version: 1.4.0 → 1.4.1 ([`530791d`](https://github.com/bdraco/yalexs/commit/530791dc9efea965c872937c8e1efb61aceb42ee)) * Handle late auth failure when switching brands (Yale Home) (#62) ([`5a960fa`](https://github.com/bdraco/yalexs/commit/5a960fa12efde95a357f27d280828f3e6561b2b8)) ## v1.4.0 (2023-05-17) ### Unknown * Bump version: 1.3.3 → 1.4.0 ([`072901e`](https://github.com/bdraco/yalexs/commit/072901ed8225504bc794358e0dd64b0b3d98353a)) * Preliminary support for Yale Home (#61) ([`c73b38c`](https://github.com/bdraco/yalexs/commit/c73b38c0d29e13f8e766309125a66ec90db8e514)) ## v1.3.3 (2023-04-30) ### Fix * fix: handle activities that happen at the same microsecond (#57) ([`4dca5db`](https://github.com/bdraco/yalexs/commit/4dca5dbe8e050852a223e20fac74f8b763ebc1e7)) ### Unknown * Bump version: 1.3.2 → 1.3.3 ([`c7c7c43`](https://github.com/bdraco/yalexs/commit/c7c7c4381ba1799440346192d01f1ffc7f4883ae)) ## v1.3.2 (2023-04-24) ### Unknown * Bump version: 1.3.1 → 1.3.2 ([`fc43ee7`](https://github.com/bdraco/yalexs/commit/fc43ee770a47cfff6a8505023fb55d1d566dfd7d)) * Avoid setting the operator if the lock event is a replay (#56) ([`47a9d04`](https://github.com/bdraco/yalexs/commit/47a9d04a2097b5ad16bf163f758be942b890c38f)) ## v1.3.1 (2023-04-24) ### Unknown * Bump version: 1.3.0 → 1.3.1 ([`493f702`](https://github.com/bdraco/yalexs/commit/493f702c5ed46f7d9ef263f0b2d8b1e00198c2f3)) * Fix incorrect times from pubnub messages (#55) ([`fab0619`](https://github.com/bdraco/yalexs/commit/fab06194ebde1ba0cc07e2eb7d2f2ec248d4c055)) * Add support for doorman l3 doorbell event (#54) Co-authored-by: J. Nick Koston <nick@koston.org> ([`637fdd6`](https://github.com/bdraco/yalexs/commit/637fdd6962b009255b92483ec3ec31958d7168e8)) ## v1.3.0 (2023-04-15) ### Unknown * Bump version: 1.2.9 → 1.3.0 ([`0f0954a`](https://github.com/bdraco/yalexs/commit/0f0954a8dbc9ff174d1cda4a585819d37e960c99)) ## v1.2.9 (2023-04-15) ### Unknown * Bump version: 1.2.8 → 1.2.9 ([`0f23d20`](https://github.com/bdraco/yalexs/commit/0f23d209530d709ee0ceb4cd0a7a5d0de0397e00)) * Handle alternate user ID field in events (#53) Co-authored-by: J. Nick Koston <nick@koston.org> ([`f8a3afa`](https://github.com/bdraco/yalexs/commit/f8a3afaa1367252ba363af492475b09249f5e23f)) ## v1.2.8 (2023-02-20) ### Unknown * Bump version: 1.2.7 → 1.2.8 ([`0af3db1`](https://github.com/bdraco/yalexs/commit/0af3db15b187075f6847602c2978e58d1297340d)) * Key error when there are accessType "always" (#48) ([`25e69d8`](https://github.com/bdraco/yalexs/commit/25e69d8ac76515857bd40a2fd87f9ed0dd49394f)) ## v1.2.7 (2023-02-17) ### Unknown * Bump version: 1.2.6 → 1.2.7 ([`a68f8d4`](https://github.com/bdraco/yalexs/commit/a68f8d45a7b09a14cf894e7c7524b86aa2bb0878)) * Fix missing microseconds in time conversion (#47) ([`888cf9c`](https://github.com/bdraco/yalexs/commit/888cf9c82a9cd26fb2e024fba4edf9d1567e604b)) ## v1.2.6 (2022-10-12) ### Unknown * Bump version: 1.2.5 → 1.2.6 ([`9536fed`](https://github.com/bdraco/yalexs/commit/9536fed0965941ceeff04c78ef9f42f3db66ea63)) * Add names to local lock operations for compat (#43) ([`26e7b06`](https://github.com/bdraco/yalexs/commit/26e7b06ab786d6922951e9fc0bcae3ff4088e382)) * add more activity tests (#42) ([`4980073`](https://github.com/bdraco/yalexs/commit/4980073d91322b0228fb9dcae0b9c92285ea04d8)) * add tests for manual operations (#41) ([`71fea19`](https://github.com/bdraco/yalexs/commit/71fea1970f5beda1c90c5c181eb62f8f4e4e5280)) ## v1.2.5 (2022-10-12) ### Unknown * Bump version: 1.2.4 → 1.2.5 ([`def745a`](https://github.com/bdraco/yalexs/commit/def745a8789f89eef6535516f38c142793255809)) * Add support for additional activities (#40) ([`d4b1569`](https://github.com/bdraco/yalexs/commit/d4b156991cf901c8bb65a7b9f917ca10d4f167da)) ## v1.2.4 (2022-09-29) ### Fix * fix: doorbell parser for new api (#38) ([`c757289`](https://github.com/bdraco/yalexs/commit/c757289097de93cdf69385a2022ad7c1e7251757)) ### Unknown * Bump version: 1.2.3 → 1.2.4 ([`2996d8a`](https://github.com/bdraco/yalexs/commit/2996d8a143e282b8689137505631a409a66acdc9)) ## v1.2.3 (2022-09-28) ### Feature * feat: map additional activities (#37) ([`1868282`](https://github.com/bdraco/yalexs/commit/18682827d358a3f38bbbe9cc2e74b449c46c8f84)) ### Unknown * Bump version: 1.2.2 → 1.2.3 ([`d4df7ec`](https://github.com/bdraco/yalexs/commit/d4df7ecb6a38b9418338544cf88fd6b3b25cb4ae)) ## v1.2.2 (2022-09-23) ### Unknown * Bump version: 1.2.1 → 1.2.2 ([`e9f496a`](https://github.com/bdraco/yalexs/commit/e9f496a86cddf5fe33e0d43580757fb1c81a4f82)) * Update for activity api change (#36) ([`8ba4e65`](https://github.com/bdraco/yalexs/commit/8ba4e65da7513a525187d76b7a3eafaa477aef13)) ## v1.2.1 (2022-08-06) ### Unknown * Bump version: 1.2.0 → 1.2.1 ([`6c82e9b`](https://github.com/bdraco/yalexs/commit/6c82e9b4a51023c87923071dcde128b776804f7a)) * Expose mac address on api (#32) ([`7dc0b88`](https://github.com/bdraco/yalexs/commit/7dc0b88a73b3251d9bd565fd2a0c0713654f3a71)) ## v1.2.0 (2022-07-28) ### Unknown * Bump version: 1.1.25 → 1.2.0 ([`cc2377a`](https://github.com/bdraco/yalexs/commit/cc2377a6dcc5ec106a8a61789347918250add625)) * Add support for getting the offline keys (#31) ([`4ff8d98`](https://github.com/bdraco/yalexs/commit/4ff8d98d2ca213ac1258e529c71f519d6b04fd37)) ## v1.1.25 (2022-05-11) ### Unknown * Bump version: 1.1.24 → 1.1.25 ([`01a2964`](https://github.com/bdraco/yalexs/commit/01a2964e789abeae71a51ba7f9f995565389a996)) * Handling secure mode (#29) When setting the lock in "secure mode" (cannot be physically opened from the inside) august reports `kAugLockState_SecureMode`. This isn't interpenetrated as locked. This change proposes to change that. ([`0480734`](https://github.com/bdraco/yalexs/commit/048073440bb2f1852abbcc2da077c9833a0a13e9)) ## v1.1.24 (2022-05-06) ### Unknown * Bump version: 1.1.23 → 1.1.24 ([`0291b0a`](https://github.com/bdraco/yalexs/commit/0291b0a650f78fc307ddf6ced74abab8715b41ba)) * Only parse Authentication expire time once (#28) ([`6abae88`](https://github.com/bdraco/yalexs/commit/6abae883220f53f5bf9d6b3fa603362d77944c55)) ## v1.1.23 (2022-03-15) ### Unknown * Bump version: 1.1.22 → 1.1.23 ([`95d4b80`](https://github.com/bdraco/yalexs/commit/95d4b80bbd989ca01a9b41b22d1860d91b0d405a)) * Provide raw access to data for diagnostics (#27) ([`9e43427`](https://github.com/bdraco/yalexs/commit/9e43427adf068c9158e43c77ff34fa4e8f779a0d)) ## v1.1.22 (2022-02-11) ### Unknown * Bump version: 1.1.21 → 1.1.22 ([`98cc55c`](https://github.com/bdraco/yalexs/commit/98cc55c0ce8dd5915c8e0257e0cd55ba715c6247)) * Switch to using JWT for token decoding to handle emojii (#26) ([`8000542`](https://github.com/bdraco/yalexs/commit/80005429ba49c1029e96cf74a13d1d4d355eb142)) ## v1.1.21 (2022-02-11) ### Unknown * Bump version: 1.1.20 → 1.1.21 ([`b3dd252`](https://github.com/bdraco/yalexs/commit/b3dd2520fb5407b7ae985c918d431a90dff41010)) * Add additional debug logging (#25) ([`2fee78d`](https://github.com/bdraco/yalexs/commit/2fee78df329891c4b2354bfc04c710be05e84b71)) ## v1.1.20 (2022-01-31) ### Unknown * Bump version: 1.1.19 → 1.1.20 ([`fa2a139`](https://github.com/bdraco/yalexs/commit/fa2a139eeeff98a9f15c19e4acbb18151d15e146)) * Update august agent (#24) ([`88abc08`](https://github.com/bdraco/yalexs/commit/88abc08684f9536962ba19289b1b841c0b684bb0)) ## v1.1.19 (2022-01-15) ### Unknown * Bump version: 1.1.18 → 1.1.19 ([`35d38ac`](https://github.com/bdraco/yalexs/commit/35d38ac820db362d0a0155b19f430ac650948418)) * Fix unlock/lock with older bridges (#23) ([`b620936`](https://github.com/bdraco/yalexs/commit/b6209366b0387073db7d16301d592ddfd4b06f0d)) ## v1.1.18 (2022-01-13) ### Unknown * Bump version: 1.1.17 → 1.1.18 ([`88dbab7`](https://github.com/bdraco/yalexs/commit/88dbab72f6588e4e4786b204a491277b5f195998)) * Add async_status_async (#22) ([`2a5f2c4`](https://github.com/bdraco/yalexs/commit/2a5f2c4479ca1b9afade7f93f1f0e058ac252548)) ## v1.1.17 (2022-01-08) ### Unknown * Bump version: 1.1.16 → 1.1.17 ([`2087a47`](https://github.com/bdraco/yalexs/commit/2087a47462ec6f8b8a0be5d8b696a95c9a4903e8)) * Improve august lock/unlock reliablity when api is slow (#21) ([`710fce2`](https://github.com/bdraco/yalexs/commit/710fce22873b2165d7eb17a983e4d975c6828347)) ## v1.1.16 (2021-12-24) ### Unknown * Bump version: 1.1.15 → 1.1.16 ([`f444315`](https://github.com/bdraco/yalexs/commit/f444315e10fb464e7546378ee79f74a828f7aaa6)) * Guard against empty pubnub callback on disconnect (#20) ([`64465e9`](https://github.com/bdraco/yalexs/commit/64465e9a664ea367499eb50bec62f311fb6f7cb6)) ## v1.1.15 (2021-12-17) ### Unknown * Bump version: 1.1.14 → 1.1.15 ([`e4d51c8`](https://github.com/bdraco/yalexs/commit/e4d51c8117c5f50b2d4797caef83c74e53e0c11f)) * Add support for the DoorbellImageCaptureActivity to update_doorbell_image_from_activity (#19) ([`924b5cf`](https://github.com/bdraco/yalexs/commit/924b5cf282818ce4aff59d27d7ecc52f41a70c93)) ## v1.1.14 (2021-12-17) ### Unknown * Bump version: 1.1.13 → 1.1.14 ([`b24bf39`](https://github.com/bdraco/yalexs/commit/b24bf3910c336f87b115a3dcdb52429dcbe55f9d)) * BREAKING CHANGE: Split motion and imagecapture into separate activities (#18) ([`f27758a`](https://github.com/bdraco/yalexs/commit/f27758a23085cb3b5b98095dc8fc9c7003cc4a08)) ## v1.1.13 (2021-07-28) ### Unknown * Bump version: 1.1.12 → 1.1.13 ([`99c243f`](https://github.com/bdraco/yalexs/commit/99c243fc6f10421adc6f886359123309523152c9)) * Add new state LockDoorStatus.DISABLED for when doorsense is explicitly disabled (#15) ([`aa2bf72`](https://github.com/bdraco/yalexs/commit/aa2bf72047614598d32116ad01350c8d14bf1e9c)) * Remove python 3.10 alpha from the CI (#16) ([`be24643`](https://github.com/bdraco/yalexs/commit/be24643473850027270fbfa3c32b7cd951a3a3d6)) ## v1.1.12 (2021-07-10) ### Unknown * Bump version: 1.1.11 → 1.1.12 ([`d5c8075`](https://github.com/bdraco/yalexs/commit/d5c8075905fe8380e8ce462dfa2aaf5541136415)) * Bump version: 1.1.10 → 1.1.11 ([`e1a57b2`](https://github.com/bdraco/yalexs/commit/e1a57b2578be3f8c87aec7082345f98a8086c1e3)) * Fix isort in test (#14) ([`d548b9a`](https://github.com/bdraco/yalexs/commit/d548b9afa1d460f9fbda962b8af365baf5c7590d)) * Add support for unlocking and jammed status (#13) ([`b952d81`](https://github.com/bdraco/yalexs/commit/b952d81f039a5b37387e3e122d9ee59c47b237a0)) * Handle initial LockStatus missing (#12) ([`cf9cab6`](https://github.com/bdraco/yalexs/commit/cf9cab63a8008e1728b4c9ac446253c4a464ba1e)) ## v1.1.10 (2021-03-30) ### Unknown * Bump version: 1.1.9 → 1.1.10 ([`afc93f6`](https://github.com/bdraco/yalexs/commit/afc93f60eda6e8a9810e82582811316b7a1d6b4f)) * Revert pubnub reconnect workaround and update to pubnub 5.1.1 (#11) ([`be95829`](https://github.com/bdraco/yalexs/commit/be95829ec1e6e6092501cb189390e60c17f54be2)) ## v1.1.9 (2021-03-27) ### Unknown * Bump version: 1.1.8 → 1.1.9 ([`b597dbf`](https://github.com/bdraco/yalexs/commit/b597dbf6f703667a3898c971dd2d6176e54a9da0)) * Change pubnub version to >=5.1.0 (#10) ([`b790850`](https://github.com/bdraco/yalexs/commit/b7908503b08dc00c531c6b5725e6cfdfdfbeaf7e)) ## v1.1.8 (2021-03-27) ### Unknown * Bump version: 1.1.7 → 1.1.8 ([`8d980af`](https://github.com/bdraco/yalexs/commit/8d980af4b9c42be9d55a08fa0051329e7c6daccc)) * Workaround pubnub reconnect bug (#9) ([`7fc8039`](https://github.com/bdraco/yalexs/commit/7fc803928c21e3d51a6efd1edd729ed68ddbc545)) ## v1.1.7 (2021-03-27) ### Unknown * Bump version: 1.1.6 → 1.1.7 ([`d279972`](https://github.com/bdraco/yalexs/commit/d279972593df8059aab058f6276d440111b34a05)) * Always reconnect on unknown errors (#8) ([`9b9af69`](https://github.com/bdraco/yalexs/commit/9b9af69dfb4709d92b031acff60b8ddb7b526d5f)) ## v1.1.6 (2021-03-26) ### Unknown * Bump version: 1.1.5 → 1.1.6 ([`39a74c2`](https://github.com/bdraco/yalexs/commit/39a74c2a5740a915c4d4beef19df5036ffb9ad9d)) * Improve pubnub reconnect logic (#7) ([`7927614`](https://github.com/bdraco/yalexs/commit/792761495a62e7058365be94bbbce747d5d258cd)) ## v1.1.5 (2021-03-22) ### Unknown * Bump version: 1.1.4 → 1.1.5 ([`56d108a`](https://github.com/bdraco/yalexs/commit/56d108abb1de9655612953f43c87968f5b468d03)) * Ensure pubnub reconnects on outage (#6) ([`993a93c`](https://github.com/bdraco/yalexs/commit/993a93cb05e1fb1ba6ba3c27001111617d91a297)) ## v1.1.4 (2021-03-21) ### Unknown * Bump version: 1.1.3 → 1.1.4 ([`89b40b3`](https://github.com/bdraco/yalexs/commit/89b40b3ea1c0c8defd59f5e11710b72e1c6cf21e)) * Do not store operator for pubnub (#5) ([`dd44b3b`](https://github.com/bdraco/yalexs/commit/dd44b3b875091b1b5f2bfbbbde48ec425cedf1d3)) ## v1.1.3 (2021-03-21) ### Unknown * Bump version: 1.1.2 → 1.1.3 ([`c6d3ca4`](https://github.com/bdraco/yalexs/commit/c6d3ca440075c462a28e556dcf395cc5bef681c6)) * Include the activity source to allow the log to always win (#4) ([`4129ed9`](https://github.com/bdraco/yalexs/commit/4129ed998cc7fef3cb169b13c886e5935ee9e3cf)) ## v1.1.2 (2021-03-21) ### Unknown * Bump version: 1.1.1 → 1.1.2 ([`37791de`](https://github.com/bdraco/yalexs/commit/37791dea71aae803b833b9d091cfe807f973898d)) * Do not inject the user, always get it from activity since pubnub is the caller not the operator (#3) ([`0e9b86e`](https://github.com/bdraco/yalexs/commit/0e9b86e4877ffb6f4d3d6eb22866be0080a17467)) ## v1.1.1 (2021-03-21) ### Unknown * Bump version: 1.1.0 → 1.1.1 ([`45fef09`](https://github.com/bdraco/yalexs/commit/45fef096f837d60229366824eb112bea5f040786)) * Handle pubnub messages without an operator (#2) ([`aca82e1`](https://github.com/bdraco/yalexs/commit/aca82e17fc64b07088f1dbcb862a8e40bfe7553a)) ## v1.1.0 (2021-03-21) ### Unknown * Bump version: 1.0.3 → 1.1.0 ([`9f79b0b`](https://github.com/bdraco/yalexs/commit/9f79b0b6c54a06b90bd507688b45442ed368de61)) * Switch to creating activities from pubnub (#1) ([`849532f`](https://github.com/bdraco/yalexs/commit/849532fac134d0d5de36a3bde284926350d30a73)) ## v1.0.3 (2021-03-20) ### Unknown * Bump version: 1.0.2 → 1.0.3 ([`b04cda3`](https://github.com/bdraco/yalexs/commit/b04cda3ea4f068ad91ff52376fef0f500f01ae1f)) * Switch to setuptools ([`aea8963`](https://github.com/bdraco/yalexs/commit/aea8963c0f24cf7043bd5b0e2af1e51d6a77e858)) ## v1.0.2 (2021-03-20) ### Unknown * Bump version: 1.0.1 → 1.0.2 ([`9b14523`](https://github.com/bdraco/yalexs/commit/9b1452300bfe73e3bc462dedbcf59384d8802cd5)) * Tweaks to setup.py ([`c0f467e`](https://github.com/bdraco/yalexs/commit/c0f467ecfcb8fcfd40a9ea79ddbe933b8ba32b70)) * Tweaks to setup.py ([`fc40662`](https://github.com/bdraco/yalexs/commit/fc4066232371e0c8e21806c4259cbcd846418f97)) * Fix CI ([`bad669d`](https://github.com/bdraco/yalexs/commit/bad669de938fc031a81addf18555878cc8fa323e)) * format black ([`4ad6634`](https://github.com/bdraco/yalexs/commit/4ad6634975b9243190df831f0fcd332b282daab7)) * Cleanup setup.py ([`b16e789`](https://github.com/bdraco/yalexs/commit/b16e7892838a0680070f67ad219cdb454342c7ea)) * Add build.sh ([`9946a3e`](https://github.com/bdraco/yalexs/commit/9946a3efb1f29d4bd86a7565d87949b36a30fa60)) ## v1.0.1 (2021-03-20) ### Unknown * Bump version: 1.0.0 → 1.0.1 ([`8ef8c3a`](https://github.com/bdraco/yalexs/commit/8ef8c3af1bf21be7d1e91432fb2170bb2aa8d3b1)) * Bumpversion ([`4ee92c7`](https://github.com/bdraco/yalexs/commit/4ee92c72715bbd7140c6083cf5e52b2b672039ff)) * Bumpversion ([`6feebd1`](https://github.com/bdraco/yalexs/commit/6feebd16e0660c629ed8b55de95b9254084394a4)) * Rename to yalexs ([`2e7e619`](https://github.com/bdraco/yalexs/commit/2e7e6198e5e95ecb12a37d86c379bbf995207093)) * Add basic pubnub support ([`1d2b17b`](https://github.com/bdraco/yalexs/commit/1d2b17b2b596e993981481dc92daaf2b6d91452e)) * Fix CI and switch to GH actions ([`707f9b9`](https://github.com/bdraco/yalexs/commit/707f9b96194322d17152e707ba174eb7a775f07a)) * Merge pull request #56 from ai-write-city/master Bumped version to 0.25.2 ([`78b25da`](https://github.com/bdraco/yalexs/commit/78b25da03194d68d70115f36bf926a3a6443e555)) * Bumped version to 0.25.2 ([`5b3b539`](https://github.com/bdraco/yalexs/commit/5b3b539851dea7f166eb93f7abb5de6283b15e46)) * Merge pull request #55 from ai-write-city/master Bumped version to 0.25.1 + fix build ([`30f77e8`](https://github.com/bdraco/yalexs/commit/30f77e8d59e7d37e44b35601b7c7ec5320ee649b)) * Fixed incorrect dep (dateutil -> python-dateutil) Fixed lint error ([`cc695f6`](https://github.com/bdraco/yalexs/commit/cc695f67eef46ba12cd93e232c24e9a9e264dfb6)) * Merge branch 'master' of github.com:ai-write-city/py-august ([`f0d6af6`](https://github.com/bdraco/yalexs/commit/f0d6af65ec94dc5b91e6b2e5bd456eec5f785ccf)) * Merge pull request #1 from snjoetw/master Merge in from upstream ([`6fef771`](https://github.com/bdraco/yalexs/commit/6fef771a091aa2098c7e45af068332c6acfc76a5)) * Bump version to 0.25.1 ([`c79b0d5`](https://github.com/bdraco/yalexs/commit/c79b0d5b0de3c0d54e85d14eafee7c93bcbddafc)) * Merge pull request #54 from ai-write-city/master Fixed lint warnings ([`fdd7e82`](https://github.com/bdraco/yalexs/commit/fdd7e828179f252c9b3e2c5465f37f428950ea64)) * Fixed lint warnings ([`6704a4c`](https://github.com/bdraco/yalexs/commit/6704a4c50c01c32c62e49cd60222f9b50df54b1d)) * Merge pull request #52 from fabaff/patch-1 Add dateutil ([`e8b875d`](https://github.com/bdraco/yalexs/commit/e8b875d08720ae507e4657cfcfeb01b347d12aeb)) * Merge pull request #50 from bdraco/fix_token_refresh Fix august token refresh ([`61ab1cf`](https://github.com/bdraco/yalexs/commit/61ab1cf3a911d4651eba733faa6667cd1fd12c07)) * Add dateutil ([`d734dbb`](https://github.com/bdraco/yalexs/commit/d734dbbd09a76b3c690d07d0f24a59833845542e)) * Fix august token refresh ([`90046a8`](https://github.com/bdraco/yalexs/commit/90046a8c514b0e079c8cfbe61a35990546537de2)) * Merge pull request #48 from bdraco/fix_validation_and_add_tests Fix validation code verification passing login_method as a string instead of var ([`a8eb0df`](https://github.com/bdraco/yalexs/commit/a8eb0df53bacc21999ae8172bc4ab8f1df4b9b9f)) * Fix validation code verification passing login_method as a string instead of a var ([`adc6718`](https://github.com/bdraco/yalexs/commit/adc6718d17b9406e89a8011e5f95d5216a55edeb)) * Merge pull request #47 from bdraco/operation_details Add additional details to the Lock activity ([`3556550`](https://github.com/bdraco/yalexs/commit/35565506601e1a3e99f77ae21ec20cab9cb58c1f)) * Merge pull request #46 from bdraco/add_async_api Add async support ([`dee8716`](https://github.com/bdraco/yalexs/commit/dee87163949d554de09bb01fc61073ad2de16999)) * Add additional details to the Lock activity * Lock operation was remote * Lock operation was done via keypad * Lock operation done by autorelock * Lock operator image url * Lock operator thumbnail url ([`d11777b`](https://github.com/bdraco/yalexs/commit/d11777b98641e0c0d92ad82f1d8c85edb1ed9af0)) * Add async support This adds support for working with the august api with async The existing non-async support is left in-tact and it now shares a common base module for Api and Authenticator to ensure backwards compat. When using async, the following changes need to be made in your code Api is now ApiAsync ApiAsync must be passed an aiohttp ClientSession() as the first argument await async_setup() must be called to setup the object after creating it Authenticator is now AuthenticatorAsync AuthenticatorAsync must be passed an aiohttp ClientSession() as the first argument await async_setup_authentication() must be called to setup the object AugustApiHTTPError is replaced with AugustApiAIOHTTPError Checking for RequestException is replaced with checking for ClientError All of the async functions are prefixed with async_ Ex get_operable_locks is now async_get_operable_locks ([`f984e8d`](https://github.com/bdraco/yalexs/commit/f984e8da024a4305bba7f837a25a3a72056f06e4)) * Merge pull request #45 from bdraco/gen2_battery Additional battery percentage estimations ([`87dd062`](https://github.com/bdraco/yalexs/commit/87dd0621bdf8e6b00988925e5b8c39f2bc62fc22)) * Additional battery percentage estimations * Battery percentage for the mars2 lock * Battery percentage for the keypad These mirror the name to percentage mappings currently used in home assistant ([`e6eaa3e`](https://github.com/bdraco/yalexs/commit/e6eaa3ed3ee5d5e49369f8d0bb63f0e1a4d316a5)) * Merge pull request #44 from bdraco/add_offline_doorbell_tests Add offline doorbell tests ([`9adeef6`](https://github.com/bdraco/yalexs/commit/9adeef67dc9e6c3f731a58183881ddd6e22d6bb9)) * Merge pull request #43 from bdraco/expose_model Expose model and keypad name ([`2c58436`](https://github.com/bdraco/yalexs/commit/2c584365d8122a27fb0a447a7e306dac748197e2)) * Merge pull request #42 from bdraco/bridge_is_online Bridge is online ([`3579532`](https://github.com/bdraco/yalexs/commit/3579532e2aa2a7cc49db8eebb70d5b10886edec2)) * Merge pull request #41 from bdraco/doorbell_image_fetch Doorbell image fetch ([`8688e8e`](https://github.com/bdraco/yalexs/commit/8688e8ecf0b17947e18eb7151ce9de40555ae2b9)) * Add tests for offline doorbells ([`ba4d8dd`](https://github.com/bdraco/yalexs/commit/ba4d8dd1ca52a31eb28f76b3b016a8e57cdf3411)) * Expose model and keypad name Homeassistant uses this information in the device registry: https://developers.home-assistant.io/docs/device_registry_index/ This is the final piece needed to convert august to config_flow ([`ee1909d`](https://github.com/bdraco/yalexs/commit/ee1909dc504d9121f97e9f094fd1ac817df81c90)) * Merge pull request #40 from bdraco/doorbell_image_updates_from_activity Add ability to update doorbell images from activity ([`860c2cc`](https://github.com/bdraco/yalexs/commit/860c2cc55d6dd1ada4b7d935fc784072c0f72b1e)) * Add a bridge_is_online property to LockDetail This allows us to normalize reporting of available status in homeassistant ([`5ef620f`](https://github.com/bdraco/yalexs/commit/5ef620f8613eaa1d042caa35b6429f5f608a395c)) * Add support for fetching the doorbell image Homeassistant wants all requests handled in the py-august module ([`1e86023`](https://github.com/bdraco/yalexs/commit/1e86023f9254699bd3bb994cace79b775f2d9d1f)) * Add ability to update doorbell images from activity * Fix tests for doors when timezone is not utc * Add check for standby status to doorbells so they do not unexpectedly get marked unavailable ([`e99e939`](https://github.com/bdraco/yalexs/commit/e99e939cd01bdc21798ee37dc2d575af3f4c6b33)) * Merge pull request #39 from bdraco/lock_with_detail_so_we_can_get_door_state_sq Add lock_return_activity and unlock_return_activity apis (additional reduction in august api calls) ([`f41a3a9`](https://github.com/bdraco/yalexs/commit/f41a3a97b82922f517df749250e8f3839e6f918b)) * Merge pull request #38 from bdraco/update_lock_detail_with_activity Update lock detail with activity ([`13d2a55`](https://github.com/bdraco/yalexs/commit/13d2a550fc0a09a304a34ff98d5d7ae4a7347325)) * Merge pull request #37 from bdraco/add_status_info_to_detail_to_avoid_multiple_api_calls Add door status to lock detail ([`e417c71`](https://github.com/bdraco/yalexs/commit/e417c71c1ccd2fb87cb61e751f494037912c079c)) * Add lock_return_activity and unlock_return_activity apis It is now possible to call the lock and unlock remote operation and get back a LockOperationActivity that can be consumed by update_lock_detail_from_activity. If the lock supports doorsense, a DoorOperationActivity is also returned since the underlying August API returns this. Lock operations now avoid the need to fetch lock details afterward which further reduces the number of API calls we make to the August API ([`78078dc`](https://github.com/bdraco/yalexs/commit/78078dcd5d4061f128c02757014d763f5f6a5cb4)) * Provide a util to update LockDetail from activity Home Assistant checks the activity log far more frequently than other apis in order to reduce the number of api calls. The new update_lock_from_activity util provides a way to update a LockDetail class with one of the following activities: LockOperationActivity DoorOperationActivity ([`24b08ee`](https://github.com/bdraco/yalexs/commit/24b08ee13f503be9242a3c10adf476a37fb82dbb)) * Add door status to lock detail In order to reduce the number of api calls we can now get the door status from the single lock detail endpoint since it is contained inside the detail api. Provide sets for the lock status and door state in the LockDetail so they can be updated with data from the activity api ([`6beafb0`](https://github.com/bdraco/yalexs/commit/6beafb0e1a22de1242ad9448ad1b94dbf64a55de)) * - fixed lint error ([`8a0f029`](https://github.com/bdraco/yalexs/commit/8a0f029045be823ba415672d5804c5f2c0dc9be5)) * Merge pull request #35 from bdraco/hide_implementation_details_behind_exception Hide implementation details with exceptions ([`7089897`](https://github.com/bdraco/yalexs/commit/7089897415e1b1809fa2bc07ce879e1efa59d287)) * Merge pull request #36 from bdraco/handle_doorsense_in_init_state Handle doorsense in "init" state (should be set to false) ([`75eedb0`](https://github.com/bdraco/yalexs/commit/75eedb0461cc900a5b62ee23f3aa654c47447f75)) * Handle doorsense in "init" state (should be set to false) ([`402e74e`](https://github.com/bdraco/yalexs/commit/402e74ed6b13e60b6870c4dd4a6d2451860d93d0)) * Hide implementation behind exceptions The exceptions returned from the api requests should be more helpful to users so they can figure out how to resolve them. Home assistant has issues opened when a bridge if offline or unavailable because users do not understand the exception being provided. ([`ec5904b`](https://github.com/bdraco/yalexs/commit/ec5904bac64247a1108caeaa73ebb527b84a3366)) * Merge pull request #33 from bdraco/bridge_and_doorsense Expose bridge information and if doorsense is installed ([`d7e1756`](https://github.com/bdraco/yalexs/commit/d7e1756870a65d42ef94926d44376629b665bde2)) * Merge pull request #34 from bdraco/doorbell_battery_level Expose the battery level for doorbells ([`d8dfd2e`](https://github.com/bdraco/yalexs/commit/d8dfd2e3118525f371b7bae967e8763f6342067e)) * Expose the battery level for doorbells ([`0099e14`](https://github.com/bdraco/yalexs/commit/0099e1438172aeb1659a6f9b3be36f2c55acf663)) * black ([`203a0ad`](https://github.com/bdraco/yalexs/commit/203a0ad1c274d2f0ec4aa1db83bd876a570b2898)) * bol ([`8f954e5`](https://github.com/bdraco/yalexs/commit/8f954e5b3086825dbc23980e1db689353db937c7)) * bump ([`3c9fe51`](https://github.com/bdraco/yalexs/commit/3c9fe51d233665483a55fdbd9eabad0f1ae75316)) * more tests for bridge ([`b17d3ab`](https://github.com/bdraco/yalexs/commit/b17d3ab6aad194f90deed0cdd6777d7c7d2606f5)) * add ([`77b2370`](https://github.com/bdraco/yalexs/commit/77b2370a16e0970e21843c2468303b9d473b1e99)) * add ([`313045c`](https://github.com/bdraco/yalexs/commit/313045ce88e728ec21eaf76153d4244fb91e5ffe)) * Add bridge and doorsense properties ([`a53ca0d`](https://github.com/bdraco/yalexs/commit/a53ca0ddb4791bd36c50567698bfc67404b090bd)) * - bumped version to 0.12.0 ([`73420f0`](https://github.com/bdraco/yalexs/commit/73420f05174f867b11cb0fbc71178de284696fdc)) * - updated tox to use py36 and 37 ([`ecba2e1`](https://github.com/bdraco/yalexs/commit/ecba2e1d3c1233e76e8a987049d8709ae39ae5f7)) * - fixed flake8 errors ([`c32e37f`](https://github.com/bdraco/yalexs/commit/c32e37fda8d482a1aa6a066a047acf2178d4de2d)) * Merge pull request #31 from bdraco/const_for_actions Add documentation and tests for new actions in activities ([`56b717b`](https://github.com/bdraco/yalexs/commit/56b717bdfb16168eac2942553a3b9ab9dde0f158)) * Merge pull request #32 from bdraco/add_missing_dateutil Add missing python-dateutil to setup.py ([`961aded`](https://github.com/bdraco/yalexs/commit/961aded38c228e5f38638dbb7675f78e9985cc98)) * Add missing python-dateutil to setup.py ([`ebf0a16`](https://github.com/bdraco/yalexs/commit/ebf0a163fcd127df6b1a77d8608688458b4e3379)) * Merge branch 'docs_for_activity' into const_for_actions ([`cdd90ff`](https://github.com/bdraco/yalexs/commit/cdd90ff13392b83b49725c460e8f599935df627a)) * Add tests for get_house_activities ([`1ebd906`](https://github.com/bdraco/yalexs/commit/1ebd90684a05e1b622f68a4b08e44810be0a6fa2)) * add activity tests ([`8f8ce77`](https://github.com/bdraco/yalexs/commit/8f8ce77128efc4c7a234c20197ed127dfffd76ff)) * sort ([`f71fee2`](https://github.com/bdraco/yalexs/commit/f71fee236927f62ee1dcbf17ef32e3b874ff1e64)) * Add ACTIVITY_ACTION_STATES ([`506a8a3`](https://github.com/bdraco/yalexs/commit/506a8a31d9b686e96322fdfe1c7aa530d3237450)) * Add constants for actions ([`da020cb`](https://github.com/bdraco/yalexs/commit/da020cb244e9ae291723c41d1a3752a034014909)) * Keep track of known activities in known_activities.md ([`1cea295`](https://github.com/bdraco/yalexs/commit/1cea2958d5cf6550de6a1aded9d02ff982633c5e)) * - bumped version to 0.11.0 ([`3e2a646`](https://github.com/bdraco/yalexs/commit/3e2a646c9df9c9f092536715bf56e90b1391cce1)) * - fixed flake8 error ([`079ba2b`](https://github.com/bdraco/yalexs/commit/079ba2b6c138b03a6659756f6736bd595662300a)) * Merge pull request #30 from bdraco/fix_timezone_compare_on_token_refresh Make sure the expire time we get back from the ([`4fd857a`](https://github.com/bdraco/yalexs/commit/4fd857aa66d0807698af99a848e58955cdebbe10)) * Merge pull request #29 from bdraco/doorsense_support Add support for doorsense to the activity module ([`752dc8e`](https://github.com/bdraco/yalexs/commit/752dc8eae272cee76afd56f68d152c5924af28f5)) * Merge pull request #28 from bdraco/onetouchlock Add support for onetouchlock ([`4c2662e`](https://github.com/bdraco/yalexs/commit/4c2662e654c322f039b42ccad0bfda65c9837aef)) * Make sure the expire time we get back from the refresh api has a timezone so we are consistent for other comparisons ([`f9c4cf4`](https://github.com/bdraco/yalexs/commit/f9c4cf49e27757bb6af66e513b24066185964055)) * Add support for doorsense to the activity module ([`3b9e86d`](https://github.com/bdraco/yalexs/commit/3b9e86d39ceb1e0fa3a20cf15bf31c040d639d4b)) * Add support for onetouchlock ([`c3d5cee`](https://github.com/bdraco/yalexs/commit/c3d5cee1b4283888ea5cd6f0d6abbad902a795ac)) * - updated travis build to only use python 3.6 and 3.7 ([`92eab92`](https://github.com/bdraco/yalexs/commit/92eab92a8abe3d05f8bd49fb5d48e116976f7665)) * - fixed test ([`18f2682`](https://github.com/bdraco/yalexs/commit/18f2682fbfb0ace16d5b1fdc6e47679bf51a85c5)) * - bumped version to 0.10.1 ([`dfe8347`](https://github.com/bdraco/yalexs/commit/dfe8347afb9b970b289b78fad9f64336f9ca156a)) * Merge pull request #25 from bdraco/fix_timezone_compare Get time with a timezone. ([`3f5e94a`](https://github.com/bdraco/yalexs/commit/3f5e94abee6c87add6e6605f93412d43f5ecd1f0)) * - fixed lint and flake8 errors ([`3bbe9ea`](https://github.com/bdraco/yalexs/commit/3bbe9eab664d9547fd86df8962d38c6305d20bc3)) * Get time with a timezone. Fixes: Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/setup.py", line 174, in _async_setup_component component.setup, hass, processed_config # type: ignore File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "/config/custom_components/august/__init__.py", line 161, in setup access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE), File "/usr/local/lib/python3.7/site-packages/august/authenticator.py", line 110, in __init__ if self._authentication.is_expired(): File "/usr/local/lib/python3.7/site-packages/august/authenticator.py", line 76, in is_expired return self.parsed_expiration_time() < datetime.utcnow() TypeError: can't compare offset-naive and offset-aware datetimes ([`c872aee`](https://github.com/bdraco/yalexs/commit/c872aee2cc4bb108676bcfa06f0822e38c68a956)) * - fixed lint errors ([`84001fc`](https://github.com/bdraco/yalexs/commit/84001fc59495763418d87039c56f6bfbffd83c19)) * - bumped version to 0.10.0 ([`e51354d`](https://github.com/bdraco/yalexs/commit/e51354d6e0a6254d689320d79023b737efe6fc2b)) * - bumped version to 0.9.0 ([`b45747e`](https://github.com/bdraco/yalexs/commit/b45747ed6b80be4f0e5df0f57fc9522444127155)) * Merge pull request #23 from sidoh/refresh_access_token Add support for refreshing access token ([`052a50c`](https://github.com/bdraco/yalexs/commit/052a50c2d0de64551ba67fd221bc3d656d50e65f)) * Merge pull request #24 from bdraco/handle_429 Handle 429 ([`fb76d0d`](https://github.com/bdraco/yalexs/commit/fb76d0d815f6abc329c1ba0fbb08c3a905dd830f)) * tweak timing ([`f434de6`](https://github.com/bdraco/yalexs/commit/f434de6b3c407d42b60bfa4255c9e9b8da46941c)) * try harder ([`281147c`](https://github.com/bdraco/yalexs/commit/281147c80d15c641ff1e8c8ebff7f5eb3542f408)) * Handle 429 from august ([`9fb029a`](https://github.com/bdraco/yalexs/commit/9fb029ad83526afa1c9ca3e5269d9283b1774f67)) * Ignore .vscode ([`c6150c2`](https://github.com/bdraco/yalexs/commit/c6150c2becbde58481c41dc228e4118fa4de7184)) * add test ([`53e86f3`](https://github.com/bdraco/yalexs/commit/53e86f378df99a91ad0fa0cf366e15092e15f479)) * Add methods to auto-refresh access token ([`c89c4a2`](https://github.com/bdraco/yalexs/commit/c89c4a248cf4d6f594441254bb4c295044286fda)) * Add method to get new access token from API ([`3ff3183`](https://github.com/bdraco/yalexs/commit/3ff3183c6103dffe7394ebe1671692b6ebbc1612)) * Merge pull request #21 from aijayadams/master Set install_requires for pip deps ([`b5a7c98`](https://github.com/bdraco/yalexs/commit/b5a7c9849253b8c556f175592102d1d8b84f0f28)) * Set install_requires for pip deps ([`e79516c`](https://github.com/bdraco/yalexs/commit/e79516c213fd57adb97d1c48347bdda85a6f7d14)) * - fix lint error ([`f777ccf`](https://github.com/bdraco/yalexs/commit/f777ccf93d5f0918e08859392be29e92f89e9bd3)) * - refactored pin.py to fix lint error - added test for get_pins API ([`02d62a8`](https://github.com/bdraco/yalexs/commit/02d62a8f127ce5ef47d971c5cc5d46f8f37b930b)) * - fixed lint error ([`0605e85`](https://github.com/bdraco/yalexs/commit/0605e858d29161e9e32deebe1d7877c9d1313713)) * Merge pull request #17 from msmeeks/api-usage-readme Add example Api usage to the README ([`1ea9cc2`](https://github.com/bdraco/yalexs/commit/1ea9cc20ed0da6b67ebd3f3e8853e9fdbb988fa3)) * Merge pull request #15 from ehendrix23/Fix-token-expired Check if token is expired ([`67279d0`](https://github.com/bdraco/yalexs/commit/67279d0c78c779bbe6d786024fa84179e4ff1a9d)) * Add example Api usage to the README ([`5f10723`](https://github.com/bdraco/yalexs/commit/5f10723d85a729e3e1667154bbce9d4e9fc95474)) * Check if token is expired Check if token is expired or will almost expire. If token expires within 7 days then log a warning, if token is expired then log an error. If token is expired then also set state that authentication is required. ([`9221c0c`](https://github.com/bdraco/yalexs/commit/9221c0cc132c77956a45b2f375542b17dbb64006)) * bump up version to 0.7.0 ([`f08e0c7`](https://github.com/bdraco/yalexs/commit/f08e0c7d7c0ca8049e192f43d5fa047c700cbff8)) * Merge pull request #11 from ehendrix23/Use-Session-and-combine-lock-with-door-status Use session and combine lock with door status ([`1dc9bc3`](https://github.com/bdraco/yalexs/commit/1dc9bc331d4f32e748140e810d869f99afe081fd)) * Merge pull request #12 from rjames86/rm-keypad-pins Adding support for lock keypads and get pins ([`48064ae`](https://github.com/bdraco/yalexs/commit/48064ae8ab1c44a85c16caf2327fac9981c94aba)) * remove print statement ([`b44a954`](https://github.com/bdraco/yalexs/commit/b44a9544809e63ce63ed05aef8cdaaa75ccf78b6)) * adding support for lock keypads and get pins ([`61ce49d`](https://github.com/bdraco/yalexs/commit/61ce49d8a5a45b137108eb4f49b5e23daad331c1)) * Request Session as parameter Changed to have the Request.Session be a parameter as part of init instead of creating it within the API itself. This way no need to worry about closing it as it is the responsibility of the calling program to do so. ([`84d6749`](https://github.com/bdraco/yalexs/commit/84d67495d54d2dd9e6aff8fc7e5d1d75a2e8d2bd)) * Additional comment for the close method Added additional comment to clarify the close method. ([`db342ec`](https://github.com/bdraco/yalexs/commit/db342ec032b4e7050f6164e42673338096e54662)) * Add Session and support for retrieving door&lock status together Add option to use session allowing for re-using the TCP connection. Add option to retrieve the door status together with lock (or lock together with door) if both are wanted. ([`1ab4dc7`](https://github.com/bdraco/yalexs/commit/1ab4dc76a4bba319ca1d818fb18226af7b47c8b3)) * Bumped version to 0.6.0 ([`2f6f4dc`](https://github.com/bdraco/yalexs/commit/2f6f4dc56587ed86285d7bf4161001f39b07f653)) * Fixed failing tests ([`3c5fce6`](https://github.com/bdraco/yalexs/commit/3c5fce601cf3c8940fb1bd6134acb33fc90aa421)) * Addressed lint warning ([`f52b811`](https://github.com/bdraco/yalexs/commit/f52b811cd8144989a424f4e62713d3fc0480bb65)) * Updated api key as mention in #6 ([`688ed4e`](https://github.com/bdraco/yalexs/commit/688ed4e3727447109868d4ed848f3bedc99a00db)) * Bumped version 0.5.0 ([`edaadc3`](https://github.com/bdraco/yalexs/commit/edaadc3c550f6b201fc2597207406183bb84d1d6)) * Fixed lint errors in tests ([`d378cdc`](https://github.com/bdraco/yalexs/commit/d378cdcab0c61c174b8957bcca3d2cb820d0737d)) * Fixed lint errors ([`40e9c16`](https://github.com/bdraco/yalexs/commit/40e9c16174f2a8c26399c9be836c42442ddeb9da)) * Merge pull request #4 from sfiorini/master Added Lock Door Status (August DoorSense) ([`643253f`](https://github.com/bdraco/yalexs/commit/643253f593b348a882e352337a9a8754269c086b)) * Added missing import for LockDoorStatus. ([`ce4d4e4`](https://github.com/bdraco/yalexs/commit/ce4d4e4cb3ea0368387518a93321c680735c90a4)) * Updated fixture to return door state attribute. ([`fc6f979`](https://github.com/bdraco/yalexs/commit/fc6f979f83cf90ce180728f290daa0a2f4aff214)) * Added tests for Lock Door Status. ([`c0e4c40`](https://github.com/bdraco/yalexs/commit/c0e4c4094d4c50547d3d3f3a79a54e65deb60ded)) * Added method to retrieve lock door status ("DoorSense" sensor) ([`988dac8`](https://github.com/bdraco/yalexs/commit/988dac8d40c4af179a6f97a1a21d12253de08fd3)) * Bumped up version to 0.4.0 ([`0d743d5`](https://github.com/bdraco/yalexs/commit/0d743d52200e4919e95491027b6969d74bc79767)) * Fixed https://github.com/snjoetw/py-august/issues/1 - Added get_operable_locks() API to only include locks owned by superuser ([`e1004e7`](https://github.com/bdraco/yalexs/commit/e1004e758ef1cf3958be6b7c06dfb3d010d7d386)) * - Updated MANIFEST ([`443c5a9`](https://github.com/bdraco/yalexs/commit/443c5a9b7877f3d35de65989129705202d48e2f2)) * - Bumped version to 0.3.0 ([`13c98ad`](https://github.com/bdraco/yalexs/commit/13c98ad1ccef834a03c9b06f13329e0cd0bda8f4)) * - Fixed pylint error ([`360bb23`](https://github.com/bdraco/yalexs/commit/360bb232c5f5e7746590e5cdad6a1124c986711b)) * - Deleted august.py ([`976ca0c`](https://github.com/bdraco/yalexs/commit/976ca0c8ab607a98f934996a4f7243dd8baddb6c)) * - Added __init__.py ([`4d1c8f9`](https://github.com/bdraco/yalexs/commit/4d1c8f99736f0a69727b4329b3ee0c559563dbba)) * - Fixed incorrect lock status handling ([`6b53424`](https://github.com/bdraco/yalexs/commit/6b5342414eaa387cc6834d3ef18df13f9b4a0412)) * - Added LockOperationActivity ([`b1fc3bc`](https://github.com/bdraco/yalexs/commit/b1fc3bc34bc594fcd26b7202028c6c4864143c02)) * - Renamed API method, get_lock() => get_lock_detail(), get_doorbell() => get_doorbell_detail() ([`59e2b81`](https://github.com/bdraco/yalexs/commit/59e2b8176785d184b8a97499c63befd913fbf0f6)) * - Added LockDetail and DoorbellDetail to represent more detailed device information - Updated Api to take command_timeout so that lock/unlock can have different timeout then other GET APIs - get_lock and get_doorbell API now returns LockDetail and DoorbellDetail - Added is_online property to Doorbell - Added more tests ([`cea3da5`](https://github.com/bdraco/yalexs/commit/cea3da508852381917e9c27445af7268f7dca032)) * Added August class that wraps API call with easier to use methods ([`f4edc95`](https://github.com/bdraco/yalexs/commit/f4edc95e922a48b9ff798efdde8e271405febeee)) * Added vol dependency ([`4ab7921`](https://github.com/bdraco/yalexs/commit/4ab7921a0fcb6251295e073919e87ac455be2ada)) * Merge branch 'master' of https://github.com/snjoetw/py-august ([`1ff4d9b`](https://github.com/bdraco/yalexs/commit/1ff4d9b1bdb5bad306d725707a3965988424c3df)) * Added tests for lock related APIs ([`d14b994`](https://github.com/bdraco/yalexs/commit/d14b994c64fae8966868b2670d8b76bb22360af9)) * Added new APIs - get_houses - get_house(house_id) - lock(lock_id) - unlock(lock_id) ([`88b7b16`](https://github.com/bdraco/yalexs/commit/88b7b167471bb9a4fc94ad622f1cd476b93b4163)) * Comment cleanup ([`41b2cf7`](https://github.com/bdraco/yalexs/commit/41b2cf7846faacfb188e76b19171022f8daf92d3)) * Added Lock entity Made Lock and Doorbell a child class of Device ([`94c0566`](https://github.com/bdraco/yalexs/commit/94c0566dcc85e91cb49c8d6516f7ae468bcb693f)) * Update README.md ([`68d1c98`](https://github.com/bdraco/yalexs/commit/68d1c986695e23d07e90a3064f1bbac11549244e)) * Merge branch 'master' of https://github.com/snjoetw/py-august ([`79a3f04`](https://github.com/bdraco/yalexs/commit/79a3f04a01f157a002aa789f66d7882bd0e5fb03)) * Added manifest ([`a77b7ef`](https://github.com/bdraco/yalexs/commit/a77b7efb8d10f3b41fe6cec4e5d631df1c8295e9)) * Update README.md ([`6386916`](https://github.com/bdraco/yalexs/commit/6386916ab0535b554efa67639c92db376957390a)) * Bumper version to 0.2.0 ([`8f6b78c`](https://github.com/bdraco/yalexs/commit/8f6b78c5f1ae030a1dd24334d1f3966632322919)) * Fixed pylint errors ([`160b2f8`](https://github.com/bdraco/yalexs/commit/160b2f8b501d3524c4441150c515fd975f67ccda)) * Fixed pylint errors ([`aefa3d8`](https://github.com/bdraco/yalexs/commit/aefa3d8b772efd7b00d662ba579f2a5548a2654b)) * Added .travis.yml ([`a16fff3`](https://github.com/bdraco/yalexs/commit/a16fff39ee92f7e0a657405c396b522f2291531c)) * Fixed API url length lint violation ([`096dcd7`](https://github.com/bdraco/yalexs/commit/096dcd7e96856fc476f4e2563ac1f2a6b2a13933)) * Removed authentication flow logic and let caller handle the logic Added ValidationResult which will be returned by Authenticator.validate_verification_code( Added tests for Authenticator ([`bf9c43f`](https://github.com/bdraco/yalexs/commit/bf9c43f26599deeef0ff2269e10ffbb8faddc804)) * Added Api.get_doorbell(device_id) ([`4b0fbca`](https://github.com/bdraco/yalexs/commit/4b0fbca5f097256f37dafc262243550e38af12fe)) * Renamed Doorbell.id to Doorbell.device_id Added Doorbell.has_subscription property ([`21a2085`](https://github.com/bdraco/yalexs/commit/21a20855831aacf3f5ceaac53cdc0c1b364f52d4)) * Renamed Activity.id to Activity.activity_id ([`426a7da`](https://github.com/bdraco/yalexs/commit/426a7da1c59a77ba3f2664d1329f4a4223d8adcf)) * Added build badge ([`55b6a28`](https://github.com/bdraco/yalexs/commit/55b6a28f5f114cfa8101d75fa6f9fef0b5bdda13)) * Added support for doorbell_call_initiated activity ([`c1d04ce`](https://github.com/bdraco/yalexs/commit/c1d04ce57e593ac489661f6e2cda744610e03024)) * Added Activity class Changed get_house_activities API to return a list of Activity objects ([`d589244`](https://github.com/bdraco/yalexs/commit/d589244da97ef1f1673d1fa62b4f9af519beed15)) * Initial commit ([`5fe824b`](https://github.com/bdraco/yalexs/commit/5fe824b2fd25e303d3307ee5d677ffafae2b8bd4)) * Initial commit ([`f462999`](https://github.com/bdraco/yalexs/commit/f4629999331f8cdc157cceaebae42e4c0c1dce0f)) yalexs-8.0.2/LICENSE000066400000000000000000000020471465672432400140530ustar00rootroot00000000000000MIT License Copyright (c) 2017 Joe Lu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. yalexs-8.0.2/MANIFEST000066400000000000000000000006161465672432400141770ustar00rootroot00000000000000# file GENERATED by distutils, do NOT edit setup.cfg setup.py yalexs/__init__.py yalexs/activity.py yalexs/api.py yalexs/api_async.py yalexs/api_common.py yalexs/authenticator.py yalexs/authenticator_async.py yalexs/authenticator_common.py yalexs/bridge.py yalexs/device.py yalexs/doorbell.py yalexs/exceptions.py yalexs/keypad.py yalexs/lock.py yalexs/pin.py yalexs/pubnub_async.py yalexs/util.py yalexs-8.0.2/README.md000066400000000000000000000131511465672432400143230ustar00rootroot00000000000000# yalexs [![PyPI version](https://badge.fury.io/py/yalexs.svg)](https://badge.fury.io/py/yalexs) [![Build Status](https://github.com/bdraco/yalexs/workflows/CI/badge.svg)](https://github.com/bdraco/yalexs) [![codecov](https://codecov.io/gh/bdraco/yalexs/branch/master/graph/badge.svg)](https://codecov.io/gh/bdraco/yalexs) [![Python Versions](https://img.shields.io/pypi/pyversions/yalexs.svg)](https://pypi.python.org/pypi/yalexs/) Python API for Yale Access (formerly August) Smart Lock and Doorbell. This is used in [Home Assistant](https://home-assistant.io) but should be generic enough that can be used elsewhere. ## Yale Access formerly August This library is a fork of Joe Lu's excellent august library from https://github.com/snjoetw/py-august ## Classes ### Authenticator Authenticator is responsible for all authentication related logic, this includes authentication and verifying the account belongs to the user by sending a verification code to email or phone. #### Constructor | Argument | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | api | See Api class. | | login_method | Login method, either "phone" or "email". | | username | If you're login_method is phone, then this is your full phone# including "+" and country code; otherwise enter your email address here. | | password | Password. | | install_id\* | ID that's generated when Yale Access app is installed. If not specified, Authenticator will auto-generate one. If an install_id is provisioned, then it's good to provide the provisioned install_id as you'll bypass verification process. | | access_token_cache_file\* | Path to access_token cache file. If specified, access_token info will be cached in the file. Subsequent authentication will utilize information in the file to determine correct authentication state. | \* means optional #### Methods ##### authenticate Authenticates using specified login_method, username and password. Outcome of this method is an Authentication object. Use Authentication.state figure out authentication state. User is authenticated only if Authentication.state = AuthenticationState.AUTHENTICATED. If an authenticated access_token is already in the access_token_cache_file, this method will return cached authentication. ##### send_verification_code Sends a 6-digits verification code to phone or email depending on login_method. ##### validate_verification_code Validates verification code. This method returns ValidationResult. Check the value to see if verification code is valid or not. ## Install ```bash pip install yalexs ``` ## Usage ```python from yalexs.api import Api from yalexs.authenticator import Authenticator, AuthenticationState api = Api(timeout=20) authenticator = Authenticator(api, "phone", "YOUR_USERNAME", "YOUR_PASSWORD", access_token_cache_file="PATH_TO_ACCESS_TOKEN_CACHE_FILE") authentication = authenticator.authenticate() # State can be either REQUIRES_VALIDATION, BAD_PASSWORD or AUTHENTICATED # You'll need to call different methods to finish authentication process, see below state = authentication.state # If AuthenticationState is BAD_PASSWORD, that means your login_method, username and password do not match # If AuthenticationState is AUTHENTICATED, that means you're authenticated already. If you specify "access_token_cache_file", the authentication is cached in a file. Every time you try to authenticate again, it'll read from that file and if you're authenticated already, Authenticator won't call Yale Access again as you have a valid access_token # If AuthenticationState is REQUIRES_VALIDATION, then you'll need to go through verification process # send_verification_code() will send a code to either your phone or email depending on login_method authenticator.send_verification_code() # Wait for your code and pass it in to validate_verification_code() validation_result = authenticator.validate_verification_code(123456) # If ValidationResult is INVALID_VERIFICATION_CODE, then you'll need to either enter correct one or resend by calling send_verification_code() again # If ValidationResult is VALIDATED, then you'll need to call authenticate() again to finish authentication process authentication = authenticator.authenticate() # Once you have authenticated and validated you can use the access token to make API calls locks = api.get_locks(authentication.access_token) ``` yalexs-8.0.2/build.sh000066400000000000000000000001021465672432400144670ustar00rootroot00000000000000#!/bin/sh python setup.py sdist bdist_wheel twine upload dist/* yalexs-8.0.2/known_activities.md000066400000000000000000000102761465672432400167530ustar00rootroot00000000000000# Known Activity Actions ## doorclosed ``` { "entities": { "device": "", "callingUser": "deleted", "otherUser": "deleted", "house": "", "activity": "" }, "house": { "houseID": "", "houseName": "" }, "dateTime": , "action": "doorclosed", "deviceName": "", "deviceID": "", "deviceType": "lock", "callingUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "DateLogActionID": "" } } ``` ## dooropen ``` { "entities": { "device": "", "callingUser": "deleted", "otherUser": "deleted", "house": "", "activity": "" }, "house": { "houseID": "", "houseName": }, "dateTime": , "action": "dooropen", "deviceName": , "deviceID": "", "deviceType": "lock", "callingUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "DateLogActionID": "" } } ``` ## unlock ``` { "entities": { "device": "", "callingUser": "", "otherUser": "deleted", "house": "", "activity": "" }, "house": { "houseID": "", "houseName": }, "source": { "sourceType": "mercury" }, "dateTime": , "action": "unlock", "deviceName": , "deviceID": "", "deviceType": "lock", "callingUser": { "UserID": "", "FirstName": "", "LastName": "", "imageInfo": { "original": { "width": , "height": , "format": "", "url": "", "secure_url": "" }, "thumbnail": { "width": , "height": , "format": "", "url": "", "secure_url": "" } } }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "agent": "mercury", "keypad": true } } ``` ## lock ``` { "entities": { "device": "", "callingUser": "", "otherUser": "deleted", "house": "", "activity": "" }, "house": { "houseID": "", "houseName": }, "dateTime": , "action": "lock", "deviceName": , "deviceID": "", "deviceType": "lock", "callingUser": { "UserID": "", "FirstName": "", "LastName": "" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "remote": true, "DateLogActionID": "" } } ``` ## onetouchlock ``` { "entities": { "device": "", "callingUser": "deleted", "otherUser": "deleted", "house": "", "activity": "" }, "house": { "houseID": "", "houseName": }, "dateTime": , "action": "onetouchlock", "deviceName": , "deviceID": "", "deviceType": "lock", "callingUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "DateLogActionID": "" } } ``` yalexs-8.0.2/poetry.lock000066400000000000000000004530531465672432400152510ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiofiles" version = "24.1.0" description = "File support for asyncio." optional = false python-versions = ">=3.8" files = [ {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, ] [[package]] name = "aiohappyeyeballs" version = "2.3.5" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" files = [ {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, ] [[package]] name = "aiohttp" version = "3.10.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc36cbdedf6f259371dbbbcaae5bb0e95b879bc501668ab6306af867577eb5db"}, {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85466b5a695c2a7db13eb2c200af552d13e6a9313d7fa92e4ffe04a2c0ea74c1"}, {file = "aiohttp-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71bb1d97bfe7e6726267cea169fdf5df7658831bb68ec02c9c6b9f3511e108bb"}, {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baec1eb274f78b2de54471fc4c69ecbea4275965eab4b556ef7a7698dee18bf2"}, {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13031e7ec1188274bad243255c328cc3019e36a5a907978501256000d57a7201"}, {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bbc55a964b8eecb341e492ae91c3bd0848324d313e1e71a27e3d96e6ee7e8e8"}, {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8cc0564b286b625e673a2615ede60a1704d0cbbf1b24604e28c31ed37dc62aa"}, {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f817a54059a4cfbc385a7f51696359c642088710e731e8df80d0607193ed2b73"}, {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8542c9e5bcb2bd3115acdf5adc41cda394e7360916197805e7e32b93d821ef93"}, {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:671efce3a4a0281060edf9a07a2f7e6230dca3a1cbc61d110eee7753d28405f7"}, {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0974f3b5b0132edcec92c3306f858ad4356a63d26b18021d859c9927616ebf27"}, {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:44bb159b55926b57812dca1b21c34528e800963ffe130d08b049b2d6b994ada7"}, {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6ae9ae382d1c9617a91647575255ad55a48bfdde34cc2185dd558ce476bf16e9"}, {file = "aiohttp-3.10.3-cp310-cp310-win32.whl", hash = "sha256:aed12a54d4e1ee647376fa541e1b7621505001f9f939debf51397b9329fd88b9"}, {file = "aiohttp-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b51aef59370baf7444de1572f7830f59ddbabd04e5292fa4218d02f085f8d299"}, {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e021c4c778644e8cdc09487d65564265e6b149896a17d7c0f52e9a088cc44e1b"}, {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24fade6dae446b183e2410a8628b80df9b7a42205c6bfc2eff783cbeedc224a2"}, {file = "aiohttp-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bc8e9f15939dacb0e1f2d15f9c41b786051c10472c7a926f5771e99b49a5957f"}, {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a9ec959b5381271c8ec9310aae1713b2aec29efa32e232e5ef7dcca0df0279"}, {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a5d0ea8a6467b15d53b00c4e8ea8811e47c3cc1bdbc62b1aceb3076403d551f"}, {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9ed607dbbdd0d4d39b597e5bf6b0d40d844dfb0ac6a123ed79042ef08c1f87e"}, {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e66d5b506832e56add66af88c288c1d5ba0c38b535a1a59e436b300b57b23e"}, {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fda91ad797e4914cca0afa8b6cccd5d2b3569ccc88731be202f6adce39503189"}, {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:61ccb867b2f2f53df6598eb2a93329b5eee0b00646ee79ea67d68844747a418e"}, {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d881353264e6156f215b3cb778c9ac3184f5465c2ece5e6fce82e68946868ef"}, {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b031ce229114825f49cec4434fa844ccb5225e266c3e146cb4bdd025a6da52f1"}, {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5337cc742a03f9e3213b097abff8781f79de7190bbfaa987bd2b7ceb5bb0bdec"}, {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab3361159fd3dcd0e48bbe804006d5cfb074b382666e6c064112056eb234f1a9"}, {file = "aiohttp-3.10.3-cp311-cp311-win32.whl", hash = "sha256:05d66203a530209cbe40f102ebaac0b2214aba2a33c075d0bf825987c36f1f0b"}, {file = "aiohttp-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:70b4a4984a70a2322b70e088d654528129783ac1ebbf7dd76627b3bd22db2f17"}, {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:166de65e2e4e63357cfa8417cf952a519ac42f1654cb2d43ed76899e2319b1ee"}, {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7084876352ba3833d5d214e02b32d794e3fd9cf21fdba99cff5acabeb90d9806"}, {file = "aiohttp-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d98c604c93403288591d7d6d7d6cc8a63459168f8846aeffd5b3a7f3b3e5e09"}, {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d73b073a25a0bb8bf014345374fe2d0f63681ab5da4c22f9d2025ca3e3ea54fc"}, {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8da6b48c20ce78f5721068f383e0e113dde034e868f1b2f5ee7cb1e95f91db57"}, {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a9dcdccf50284b1b0dc72bc57e5bbd3cc9bf019060dfa0668f63241ccc16aa7"}, {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56fb94bae2be58f68d000d046172d8b8e6b1b571eb02ceee5535e9633dcd559c"}, {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf75716377aad2c718cdf66451c5cf02042085d84522aec1f9246d3e4b8641a6"}, {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c51ed03e19c885c8e91f574e4bbe7381793f56f93229731597e4a499ffef2a5"}, {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b84857b66fa6510a163bb083c1199d1ee091a40163cfcbbd0642495fed096204"}, {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c124b9206b1befe0491f48185fd30a0dd51b0f4e0e7e43ac1236066215aff272"}, {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3461d9294941937f07bbbaa6227ba799bc71cc3b22c40222568dc1cca5118f68"}, {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08bd0754d257b2db27d6bab208c74601df6f21bfe4cb2ec7b258ba691aac64b3"}, {file = "aiohttp-3.10.3-cp312-cp312-win32.whl", hash = "sha256:7f9159ae530297f61a00116771e57516f89a3de6ba33f314402e41560872b50a"}, {file = "aiohttp-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:e1128c5d3a466279cb23c4aa32a0f6cb0e7d2961e74e9e421f90e74f75ec1edf"}, {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d1100e68e70eb72eadba2b932b185ebf0f28fd2f0dbfe576cfa9d9894ef49752"}, {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a541414578ff47c0a9b0b8b77381ea86b0c8531ab37fc587572cb662ccd80b88"}, {file = "aiohttp-3.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5548444ef60bf4c7b19ace21f032fa42d822e516a6940d36579f7bfa8513f9c"}, {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba2e838b5e6a8755ac8297275c9460e729dc1522b6454aee1766c6de6d56e5e"}, {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48665433bb59144aaf502c324694bec25867eb6630fcd831f7a893ca473fcde4"}, {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bac352fceed158620ce2d701ad39d4c1c76d114255a7c530e057e2b9f55bdf9f"}, {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0f670502100cdc567188c49415bebba947eb3edaa2028e1a50dd81bd13363f"}, {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b09f38a67679e32d380fe512189ccb0b25e15afc79b23fbd5b5e48e4fc8fd9"}, {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:cd788602e239ace64f257d1c9d39898ca65525583f0fbf0988bcba19418fe93f"}, {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:214277dcb07ab3875f17ee1c777d446dcce75bea85846849cc9d139ab8f5081f"}, {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:32007fdcaab789689c2ecaaf4b71f8e37bf012a15cd02c0a9db8c4d0e7989fa8"}, {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:123e5819bfe1b87204575515cf448ab3bf1489cdeb3b61012bde716cda5853e7"}, {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:812121a201f0c02491a5db335a737b4113151926a79ae9ed1a9f41ea225c0e3f"}, {file = "aiohttp-3.10.3-cp38-cp38-win32.whl", hash = "sha256:b97dc9a17a59f350c0caa453a3cb35671a2ffa3a29a6ef3568b523b9113d84e5"}, {file = "aiohttp-3.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:3731a73ddc26969d65f90471c635abd4e1546a25299b687e654ea6d2fc052394"}, {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38d91b98b4320ffe66efa56cb0f614a05af53b675ce1b8607cdb2ac826a8d58e"}, {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9743fa34a10a36ddd448bba8a3adc2a66a1c575c3c2940301bacd6cc896c6bf1"}, {file = "aiohttp-3.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7c126f532caf238031c19d169cfae3c6a59129452c990a6e84d6e7b198a001dc"}, {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:926e68438f05703e500b06fe7148ef3013dd6f276de65c68558fa9974eeb59ad"}, {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434b3ab75833accd0b931d11874e206e816f6e6626fd69f643d6a8269cd9166a"}, {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d35235a44ec38109b811c3600d15d8383297a8fab8e3dec6147477ec8636712a"}, {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59c489661edbd863edb30a8bd69ecb044bd381d1818022bc698ba1b6f80e5dd1"}, {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50544fe498c81cb98912afabfc4e4d9d85e89f86238348e3712f7ca6a2f01dab"}, {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09bc79275737d4dc066e0ae2951866bb36d9c6b460cb7564f111cc0427f14844"}, {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:af4dbec58e37f5afff4f91cdf235e8e4b0bd0127a2a4fd1040e2cad3369d2f06"}, {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b22cae3c9dd55a6b4c48c63081d31c00fc11fa9db1a20c8a50ee38c1a29539d2"}, {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ba562736d3fbfe9241dad46c1a8994478d4a0e50796d80e29d50cabe8fbfcc3f"}, {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f25d6c4e82d7489be84f2b1c8212fafc021b3731abdb61a563c90e37cced3a21"}, {file = "aiohttp-3.10.3-cp39-cp39-win32.whl", hash = "sha256:b69d832e5f5fa15b1b6b2c8eb6a9fd2c0ec1fd7729cb4322ed27771afc9fc2ac"}, {file = "aiohttp-3.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:673bb6e3249dc8825df1105f6ef74e2eab779b7ff78e96c15cadb78b04a83752"}, {file = "aiohttp-3.10.3.tar.gz", hash = "sha256:21650e7032cc2d31fc23d353d7123e771354f2a3d5b05a5647fc30fea214e696"}, ] [package.dependencies] aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [[package]] name = "aioresponses" version = "0.7.6" description = "Mock out requests made by ClientSession from aiohttp package" optional = false python-versions = "*" files = [ {file = "aioresponses-0.7.6-py2.py3-none-any.whl", hash = "sha256:d2c26defbb9b440ea2685ec132e90700907fd10bcca3e85ec2f157219f0d26f7"}, {file = "aioresponses-0.7.6.tar.gz", hash = "sha256:f795d9dbda2d61774840e7e32f5366f45752d1adc1b74c9362afd017296c7ee1"}, ] [package.dependencies] aiohttp = ">=3.3.0,<4.0.0" [[package]] name = "aiosignal" version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.7" files = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] [package.dependencies] frozenlist = ">=1.1.0" [[package]] name = "aiounittest" version = "1.4.2" description = "Test asyncio code more easily." optional = false python-versions = "*" files = [ {file = "aiounittest-1.4.2-py3-none-any.whl", hash = "sha256:11634918fbbf09b954d97c74d857835750d025d1cafb675391d0099b8d83665a"}, {file = "aiounittest-1.4.2.tar.gz", hash = "sha256:1478ec8013c70046569cdc76d541083ca7da362e6513cf29c7ecb51330229bdb"}, ] [package.dependencies] wrapt = "*" [[package]] name = "alabaster" version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [[package]] name = "attrs" version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "babel" version = "2.15.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, ] [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "cbor2" version = "5.6.4" description = "CBOR (de)serializer with extensive tag support" optional = false python-versions = ">=3.8" files = [ {file = "cbor2-5.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c40c68779a363f47a11ded7b189ba16767391d5eae27fac289e7f62b730ae1fc"}, {file = "cbor2-5.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0625c8d3c487e509458459de99bf052f62eb5d773cc9fc141c6a6ea9367726d"}, {file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7137622204168c3a57882f15dd09b5135bda2bcb1cf8b56b58d26b5150dfca"}, {file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3545e1e62ec48944b81da2c0e0a736ca98b9e4653c2365cae2f10ae871e9113"}, {file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6749913cd00a24eba17406a0bfc872044036c30a37eb2fcde7acfd975317e8a"}, {file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:57db966ab08443ee54b6f154f72021a41bfecd4ba897fe108728183ad8784a2a"}, {file = "cbor2-5.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:380e0c7f4db574dcd86e6eee1b0041863b0aae7efd449d49b0b784cf9a481b9b"}, {file = "cbor2-5.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c763d50a1714e0356b90ad39194fc8ef319356b89fb001667a2e836bfde88e3"}, {file = "cbor2-5.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58a7ac8861857a9f9b0de320a4808a2a5f68a2599b4c14863e2748d5a4686c99"}, {file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d715b2f101730335e84a25fe0893e2b6adf049d6d44da123bf243b8c875ffd8"}, {file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f53a67600038cb9668720b309fdfafa8c16d1a02570b96d2144d58d66774318"}, {file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f898bab20c4f42dca3688c673ff97c2f719b1811090430173c94452603fbcf13"}, {file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e5d50fb9f47d295c1b7f55592111350424283aff4cc88766c656aad0300f11f"}, {file = "cbor2-5.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:7f9d867dcd814ab8383ad132eb4063e2b69f6a9f688797b7a8ca34a4eadb3944"}, {file = "cbor2-5.6.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e0860ca88edf8aaec5461ce0e498eb5318f1bcc70d93f90091b7a1f1d351a167"}, {file = "cbor2-5.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c38a0ed495a63a8bef6400158746a9cb03c36f89aeed699be7ffebf82720bf86"}, {file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8d8c2f208c223a61bed48dfd0661694b891e423094ed30bac2ed75032142aa"}, {file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cd2ce6136e1985da989e5ba572521023a320dcefad5d1fff57fba261de80ca"}, {file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7facce04aed2bf69ef43bdffb725446fe243594c2451921e89cc305bede16f02"}, {file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f9c8ee0d89411e5e039a4f3419befe8b43c0dd8746eedc979e73f4c06fe0ef97"}, {file = "cbor2-5.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:9b45d554daa540e2f29f1747df9f08f8d98ade65a67b1911791bc193d33a5923"}, {file = "cbor2-5.6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a5cb2c16687ccd76b38cfbfdb34468ab7d5635fb92c9dc5e07831c1816bd0a9"}, {file = "cbor2-5.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f985f531f7495527153c4f66c8c143e4cf8a658ec9e87b14bc5438e0a8d0911"}, {file = "cbor2-5.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d9c7b4bd7c3ea7e5587d4f1bbe073b81719530ddadb999b184074f064896e2"}, {file = "cbor2-5.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d06184dcdc275c389fee3cd0ea80b5e1769763df15f93ecd0bf4c281817365"}, {file = "cbor2-5.6.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e9ba7116f201860fb4c3e80ef36be63851ec7e4a18af70fea22d09cab0b000bf"}, {file = "cbor2-5.6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:341468ae58bdedaa05c907ab16e90dd0d5c54d7d1e66698dfacdbc16a31e815b"}, {file = "cbor2-5.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:bcb4994be1afcc81f9167c220645d878b608cae92e19f6706e770f9bc7bbff6c"}, {file = "cbor2-5.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41c43abffe217dce70ae51c7086530687670a0995dfc90cc35f32f2cf4d86392"}, {file = "cbor2-5.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:227a7e68ba378fe53741ed892b5b03fe472b5bd23ef26230a71964accebf50a2"}, {file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13521b7c9a0551fcc812d36afd03fc554fa4e1b193659bb5d4d521889aa81154"}, {file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4816d290535d20c7b7e2663b76da5b0deb4237b90275c202c26343d8852b8a"}, {file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1e98d370106821335efcc8fbe4136ea26b4747bf29ca0e66512b6c4f6f5cc59f"}, {file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:68743a18e16167ff37654a29321f64f0441801dba68359c82dc48173cc6c87e1"}, {file = "cbor2-5.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:7ba5e9c6ed17526d266a1116c045c0941f710860c5f2495758df2e0d848c1b6d"}, {file = "cbor2-5.6.4-py3-none-any.whl", hash = "sha256:fe411c4bf464f5976605103ebcd0f60b893ac3e4c7c8d8bc8f4a0cb456e33c60"}, {file = "cbor2-5.6.4.tar.gz", hash = "sha256:1c533c50dde86bef1c6950602054a0ffa3c376e8b0e20c7b8f5b108793f6983e"}, ] [package.extras] benchmarks = ["pytest-benchmark (==4.0.0)"] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions"] test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] name = "certifi" version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "ciso8601" version = "2.3.1" description = "Fast ISO8601 date time parser for Python written in C" optional = false python-versions = "*" files = [ {file = "ciso8601-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:57db9a28e87f9e4fccba643fb70a9ba1515adc5e1325508eb2c10dd96620314c"}, {file = "ciso8601-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c59646197ddbf84909b6c31d55f744cfeef51811e3910b61d0f58f2885823fd"}, {file = "ciso8601-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a25da209193134842cd573464a5323f46fcc3ed781b633f15a34793ba7e1064"}, {file = "ciso8601-2.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ae83f4e60fc7e260a4188e4ec4ac1bdd40bdb382eeda92fc266c5aa2f0a1ee"}, {file = "ciso8601-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2c1ef17d1ea52a39b2dce6535583631ae4bfb65c76f0ee8c99413a6861a46c9e"}, {file = "ciso8601-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3771049ba29bd1077588c0a24be1d53f7493e7cc686b2caa92f7cae129636a0e"}, {file = "ciso8601-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:55381365366dacb57207cec610d26c9a6c0d237cb65a0cf67a2baaa5299f2366"}, {file = "ciso8601-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f25647803c9a5aaaed130c53bbec7ea06a4f95ba5c7016f59e444b4ef7ac39e"}, {file = "ciso8601-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:473288cd63efe6a2cf3f4b5f90394e53095358ccb13d6128f87a2da85d0f389b"}, {file = "ciso8601-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:121d27c55f4455eaa27ba3bd602beca915df9a352f235e935636a4660321070e"}, {file = "ciso8601-2.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef44cb4dc83f37019a356c7a72692cbe17072456f4879ca6bc0339f67eee5d00"}, {file = "ciso8601-2.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:364702e338212b6c1a8643d9399ada21560cf132f363853473560625cb4207f1"}, {file = "ciso8601-2.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8acb45545e6a654310c6ef788aacb2d73686646c414ceacdd9f5f78a83165af5"}, {file = "ciso8601-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:99addd8b113f85fac549167073f317a318cd2b5841552598ceb97b97c5708a38"}, {file = "ciso8601-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f39bb5936debf21c52e5d52b89f26857c303da80c43a72883946096a6ef5e561"}, {file = "ciso8601-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:21cf83ca945bb26ecd95364ae2c9ed0276378e5fe35ce1b64d4c6d5b33038ea3"}, {file = "ciso8601-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:013410263cba46748d2de29e9894341ae41223356cde7970478c32bd0984d10c"}, {file = "ciso8601-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b26935687ef1837b56997d8c61f1d789e698be58b261410e629eda9c89812141"}, {file = "ciso8601-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0d980a2a88030d4d8b2434623c250866a75b4979d289eba69bec445c51ace99f"}, {file = "ciso8601-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87721de54e008fb1c4c3978553b05a9c417aa25b76ddf5702d6f7e8d9b109288"}, {file = "ciso8601-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f107a4c051e7c0416824279264d94f4ed3da0fbd82bd96ec3c3293426826de4"}, {file = "ciso8601-2.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:02ecbd7c8336c4e1c6bb725b898e29414ee92bdc0be6c72fb07036836b1ac867"}, {file = "ciso8601-2.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36525b1f380f4601533f4631c69911e44efb9cb50beab1da3248b0daa32bced4"}, {file = "ciso8601-2.3.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:874d20c6339e9096baaadfd1b9610bb8d5b373a0f2858cc06de8142b98d2129c"}, {file = "ciso8601-2.3.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:46a3663c2cf838f0149e1cdb8e4bdc95716e03cf2d5f803a6eb755d825896ebe"}, {file = "ciso8601-2.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e8e76825f80ce313d75bbbef1d3b8bd9e0ce31dbc157d1981e9593922c9983e7"}, {file = "ciso8601-2.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850889813f3135e0aa18f0aaec64249dd81d36a1b9bce60bb45182930c86663"}, {file = "ciso8601-2.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c690ac24ec3407f68cdfd5e032c6cb18126ef33d6c4b3db0669b9cbb8c96bd4"}, {file = "ciso8601-2.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:024c52d5d0670f15ca3dc53eff7345b6eaee22fba929675f6a408f9d1e159d98"}, {file = "ciso8601-2.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7ae2c3442d042de5330672d0d28486ed92f9d7c6dc010943aa618fd361d4638"}, {file = "ciso8601-2.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:22128f0def36fa3c4cf0c482a216e8b8ad722def08bc11c07438eff82bdcd02a"}, {file = "ciso8601-2.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:025859ec286a994aa3f2120c0f27d053b719cabc975398338374f2cc1f961125"}, {file = "ciso8601-2.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a64ff58904d4418d60fa9619014ae820ae21f7aef58da46df78a4c647f951ec"}, {file = "ciso8601-2.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d1f85c0b7fa742bbfd18177137ccbaa3f867dd06157f91595075bb959a733048"}, {file = "ciso8601-2.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ac59453664781dfddebee51f9a36e41819993823fdb09ddc0ce0e4bd3ff0c3"}, {file = "ciso8601-2.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:eaecca7e0c3ef9e8f5e963e212b083684e849f9a9bb25834d3042363223a73cd"}, {file = "ciso8601-2.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ad8f417c45eea973a694599b96f40d841215bfee352cb9963383e8d66b309981"}, {file = "ciso8601-2.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:b869396e9756a7c0696d8eb69ce1d8980bea5e25c86e5996b10d78c900a4362c"}, {file = "ciso8601-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7eb7b5ef8714d3d1fe9f3256b7a679ad783da899a0b7503a5ace78186735f840"}, {file = "ciso8601-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:02828107880848ff497971ebc98e6dc851ad7af8ec14a58089e0e11f3111cad6"}, {file = "ciso8601-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:566b4a8b2f9717e54ffcdd732a7c8051a91da30a60a4f1dafb62e303a1dbac69"}, {file = "ciso8601-2.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58a749d63f28c2eda71416c9d6014113b0748abf5fd14c502b01bd515502fedf"}, {file = "ciso8601-2.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:cb135de0e3b8feb7e74a4f7a234e8c8545957fe8d26316a1a549553f425c629d"}, {file = "ciso8601-2.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:695583810836a42945084b33621b22b0309701c6916689f6a3588fa44c5bc413"}, {file = "ciso8601-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:21204d98496cf5c0511dc21533be55c2a2d34b8c65603946a116812ffbae3b2d"}, {file = "ciso8601-2.3.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c29ea2b03dee2dc0a5d3e4a0b7d7768c597781e9fa451fe1025600f7cb55a89"}, {file = "ciso8601-2.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7533256af90724b8b7a707dcd1be4b67989447595c8e1e1c28399d4fd51dac50"}, {file = "ciso8601-2.3.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4bc9d577c0d1e57532513fc2899f5231727e28981a426767f7fa13dacb18c06"}, {file = "ciso8601-2.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4e30501eed43eea7ef64f032c81cd1d8b2020035cbdcefad40db72e2f3bc97ff"}, {file = "ciso8601-2.3.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f568de3bc269268296cb9265704dc5fcb9d4c12b1f1c67536624174df5d09"}, {file = "ciso8601-2.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:9065053c034c80c0afd74c71a4906675d07078a05cfd1cb5ff70661378cdbe60"}, {file = "ciso8601-2.3.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac00d293cdb3d1a5c78e09b3d75c7b0292ab45d5b26853b436ff5087eba2165"}, {file = "ciso8601-2.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:06941e2ee46701f083aeb21d13eb762d74d5ed6c46ff22119f27a42ed6edc8f9"}, {file = "ciso8601-2.3.1.tar.gz", hash = "sha256:3212c7ffe5d8080270548b5f2692ffd2039683b6628a8d2ad456122cc5793c4c"}, ] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "docutils" version = "0.20.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.7" files = [ {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] [[package]] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "freenub" version = "0.1.0" description = "This is a fork of pubnub when it still had an MIT license" optional = false python-versions = "<4.0,>=3.8" files = [ {file = "freenub-0.1.0-py3-none-any.whl", hash = "sha256:30a1eae368b34b502d006dedaa6e27edc47664f998cfaeeb15586b6ca727cb4d"}, {file = "freenub-0.1.0.tar.gz", hash = "sha256:56961826ba6ace9cb4b41747014b1b1443884443f6008857397d6ce44757f88b"}, ] [package.dependencies] aiohttp = ">=3.9.5,<4.0.0" cbor2 = ">=5.6.4,<6.0.0" pycryptodomex = ">=3.3" requests = ">=2.4" [[package]] name = "frozenlist" version = "1.4.1" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" files = [ {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "importlib-metadata" version = "8.0.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] code-style = ["pre-commit (>=3.0,<4.0)"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] name = "mdit-py-plugins" version = "0.4.1" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.8" files = [ {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"}, {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"}, ] [package.dependencies] markdown-it-py = ">=1.0.0,<4.0.0" [package.extras] code-style = ["pre-commit"] rtd = ["myst-parser", "sphinx-book-theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "multidict" version = "6.0.5" description = "multidict implementation" optional = false python-versions = ">=3.7" files = [ {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] [[package]] name = "myst-parser" version = "3.0.1" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false python-versions = ">=3.8" files = [ {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, ] [package.dependencies] docutils = ">=0.18,<0.22" jinja2 = "*" markdown-it-py = ">=3.0,<4.0" mdit-py-plugins = ">=0.4,<1.0" pyyaml = "*" sphinx = ">=6,<8" [package.extras] code-style = ["pre-commit (>=3.0,<4.0)"] linkify = ["linkify-it-py (>=2.0,<3.0)"] rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] [[package]] name = "packaging" version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycryptodomex" version = "3.20.0" description = "Cryptographic library for Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, ] [[package]] name = "pygments" version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.8" files = [ {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, ] [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.6" files = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-mock" version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, ] [package.dependencies] requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "sphinx" version = "7.4.3" description = "Python documentation generator" optional = false python-versions = ">=3.9" files = [ {file = "sphinx-7.4.3-py3-none-any.whl", hash = "sha256:a3c295d0e8be6277e0a5ba5c6909a308bd208876b0f4f68c7cc591f566129412"}, {file = "sphinx-7.4.3.tar.gz", hash = "sha256:bd846bcb09fd2b6e94ce3e1ad50f4618bccf03cc7c17d0f3fa87393c0bd9178b"}, ] [package.dependencies] alabaster = ">=0.7.14,<0.8.0" babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" requests = ">=2.30.0" snowballstemmer = ">=2.2" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] name = "sphinx-rtd-theme" version = "2.0.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=3.6" files = [ {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"}, {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"}, ] [package.dependencies] docutils = "<0.21" sphinx = ">=5,<8" sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.8" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "1.0.6" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.5" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, ] [package.dependencies] Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.7" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.10" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "wrapt" version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.6" files = [ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] [[package]] name = "yarl" version = "1.9.4" description = "Yet another URL library" optional = false python-versions = ">=3.7" files = [ {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" [[package]] name = "zipp" version = "3.19.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.9" content-hash = "9fb9c2c4ab7fc2f050f27a0b53d74cd1925468902ae50dd1f076e3f774337ea6" yalexs-8.0.2/pylintrc000066400000000000000000000023371465672432400146370ustar00rootroot00000000000000[MASTER] reports=no # Reasons disabled: # format - handled by black # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation # abstract-class-not-used - is flaky, should not show up but does # unused-argument - generic callbacks and setup methods create a lot of warnings # global-statement - used for the on-demand requirement installation # redefined-variable-type - this is Python, we're duck typing! # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # no-self-use - used for common between async and non-async # disable= format, locally-disabled, duplicate-code, cyclic-import, unused-argument, global-statement, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-locals, too-many-public-methods, too-many-return-statements, too-many-statements, too-many-lines, too-few-public-methods, abstract-method, missing-docstring, unspecified-encoding, consider-using-f-string [EXCEPTIONS] overgeneral-exceptions=builtins.Exception yalexs-8.0.2/pyproject.toml000066400000000000000000000054751465672432400157720ustar00rootroot00000000000000[tool.poetry] name = "yalexs" version = "8.0.2" description = "Python API for Yale Access (formerly August) Smart Lock and Doorbell" authors = ["J. Nick Koston "] license = "MIT" readme = "README.md" repository = "https://github.com/bdraco/yalexs" documentation = "https://yalexs.readthedocs.io" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] packages = [ { include = "yalexs", from = "." }, ] [tool.poetry.urls] "Bug Tracker" = "https://github.com/bdraco/yalexs/issues" "Changelog" = "https://github.com/bdraco/yalexs/blob/main/CHANGELOG.md" [tool.poetry.dependencies] python = "^3.9" ciso8601 = ">=2.1.3" pyjwt = ">=2.8.0" requests = ">=2" python-dateutil = ">=2.9.0" aiohttp = ">=3.9.5" aiofiles = ">=23" freenub = ">=0.1.0" typing-extensions = ">=4.5.0" [tool.poetry.group.dev.dependencies] pytest = "^7.0" pytest-cov = "^3.0" aioresponses = "^0.7.6" requests-mock = "^1.12.1" aiounittest = "^1.4.2" [tool.poetry.group.docs] optional = true [tool.poetry.group.docs.dependencies] myst-parser = ">=0.16" sphinx = ">=4.0" sphinx-rtd-theme = ">=1.0" [tool.semantic_release] version_toml = ["pyproject.toml:tool.poetry.version"] version_variables = [ "yalexs/__init__.py:__version__", ] build_command = "pip install poetry && poetry build" [tool.semantic_release.changelog] exclude_commit_patterns = [ "chore*", "ci*", ] [tool.semantic_release.changelog.environment] keep_trailing_newline = true [tool.semantic_release.branches.main] match = "main" [tool.semantic_release.branches.noop] match = "(?!main$)" prerelease = true [tool.pytest.ini_options] addopts = "-v -Wdefault --cov=yalexs --cov-report=term-missing:skip-covered" pythonpath = ["src"] [tool.coverage.run] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "@overload", "if TYPE_CHECKING", "raise NotImplementedError", ] [tool.isort] profile = "black" known_first_party = ["yalexs", "tests"] [tool.mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true mypy_path = "src/" no_implicit_optional = true show_error_codes = true warn_unreachable = true warn_unused_ignores = true exclude = [ 'docs/.*', 'setup.py', ] [[tool.mypy.overrides]] module = "tests.*" allow_untyped_defs = true [[tool.mypy.overrides]] module = "docs.*" ignore_errors = true [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" yalexs-8.0.2/tests/000077500000000000000000000000001465672432400142055ustar00rootroot00000000000000yalexs-8.0.2/tests/fixtures/000077500000000000000000000000001465672432400160565ustar00rootroot00000000000000yalexs-8.0.2/tests/fixtures/auto_lock_activity.json000066400000000000000000000004761465672432400226540ustar00rootroot00000000000000{ "id": "c410074a-4cb9-4552-8a4e-68d3abb9117d", "timestamp": 1665378987000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/auto_lock@3x.png", "action": "auto_lock", "deviceID": "3ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "title": "Front Door was Auto-Locked" } yalexs-8.0.2/tests/fixtures/auto_relock_activity.json000066400000000000000000000014651465672432400232020ustar00rootroot00000000000000{ "entities": { "device": "deviceid", "callingUser": "callinguser", "otherUser": "deleted", "house": "houseid", "activity": "activityid" }, "house": { "houseID": "house", "houseName": "namehouse" }, "eventID": "eventid", "dateTime": 1583535312002, "action": "lock", "deviceName": "lockname", "deviceID": "deviceid", "deviceType": "lock", "callingUser": { "FirstName": "I have no", "LastName": "picture", "UserID": "automaticrelock" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "mechanical": "success", "crypto": "success", "dateTime": "2020-03-06T22:55:12.002Z", "action": "lock", "DateLogActionID": "uni" } } yalexs-8.0.2/tests/fixtures/auto_unlock_activity.json000066400000000000000000000014461465672432400232150ustar00rootroot00000000000000{ "entities": { "device": "deviceid", "callingUser": "callinguser", "otherUser": "deleted", "house": "houseid", "activity": "activityid" }, "house": { "houseID": "house", "houseName": "namehouse" }, "eventID": "eventid", "dateTime": 1583535312002, "action": "unlock", "deviceName": "lockname", "deviceID": "deviceid", "deviceType": "lock", "callingUser": { "UserID": "userid", "FirstName": "My", "LastName": "Name" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "mechanical": "success", "crypto": "success", "dateTime": "2020-03-06T22:55:12.002Z", "action": "unlock", "DateLogActionID": "uni" } } yalexs-8.0.2/tests/fixtures/bluetooth_lock_activity.json000066400000000000000000000021431465672432400237020ustar00rootroot00000000000000{ "entities": { "device": "deviceid", "callingUser": "callinguser", "otherUser": "deleted", "house": "houseid", "activity": "activityid" }, "house": { "houseID": "house", "houseName": "namehouse" }, "eventID": "eventid", "dateTime": 1583535312002, "action": "lock", "deviceName": "lockname", "deviceID": "deviceid", "deviceType": "lock", "callingUser": { "UserID": "myuser", "FirstName": "I have a", "LastName": "picture", "imageInfo": { "original": { "width": 400, "height": 400, "format": "jpg", "secure_url": "https://image.url" }, "thumbnail": { "width": 128, "height": 128, "format": "jpg", "secure_url": "https://thumbnail.url" } } }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "mechanical": "success", "crypto": "success", "dateTime": "2020-03-06T22:55:12.002Z", "action": "lock", "DateLogActionID": "uni" } } yalexs-8.0.2/tests/fixtures/door_closed_activity.json000066400000000000000000000013301465672432400231560ustar00rootroot00000000000000{ "action": "doorclosed", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1582007217000, "deviceID": "ABC", "deviceName": "MockHouse Tech Room Door", "deviceType": "lock", "entities": { "activity": "activityId", "callingUser": "deleted", "device": "ABC", "house": "123", "otherUser": "deleted" }, "house": { "houseID": "123", "houseName": "MockHouse" }, "info": { "DateLogActionID": "ABC+Time" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/door_closed_activity_wrong_deviceid.json000066400000000000000000000013331465672432400262310ustar00rootroot00000000000000{ "action": "doorclosed", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1582007217000, "deviceID": "notABC", "deviceName": "MockHouse Tech Room Door", "deviceType": "lock", "entities": { "activity": "activityId", "callingUser": "deleted", "device": "ABC", "house": "123", "otherUser": "deleted" }, "house": { "houseID": "123", "houseName": "MockHouse" }, "info": { "DateLogActionID": "ABC+Time" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/door_closed_activity_wrong_houseid.json000066400000000000000000000013361465672432400261200ustar00rootroot00000000000000{ "action": "doorclosed", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1582007217000, "deviceID": "ABC", "deviceName": "MockHouse Tech Room Door", "deviceType": "lock", "entities": { "activity": "activityId", "callingUser": "deleted", "device": "ABC", "house": "not123", "otherUser": "deleted" }, "house": { "houseID": "not123", "houseName": "MockHouse" }, "info": { "DateLogActionID": "ABC+Time" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/door_open_activity.json000066400000000000000000000013261465672432400226530ustar00rootroot00000000000000{ "action": "dooropen", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1582007219000, "deviceID": "ABC", "deviceName": "MockHouse Tech Room Door", "deviceType": "lock", "entities": { "activity": "ActivityId", "callingUser": "deleted", "device": "ABC", "house": "123", "otherUser": "deleted" }, "house": { "houseID": "123", "houseName": "MockHouse" }, "info": { "DateLogActionID": "ABC+Time" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/doorbell_motion_activity.json000066400000000000000000000024461465672432400240620ustar00rootroot00000000000000{ "action": "doorbell_motion_detected", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1582220686158, "deviceID": "K98GiDT45GUL", "deviceName": "Front Door", "deviceType": "doorbell", "entities": { "activity": "any", "callingUser": "deleted", "device": "K98GiDT45GUL", "house": "any", "otherUser": "deleted" }, "house": { "houseID": "any", "houseName": "any" }, "info": { "dvrID": "any", "hasSubscription": false, "image": { "bytes": 42865, "created_at": "2020-02-20T17:44:45Z", "etag": "andy", "format": "jpg", "height": 576, "original_filename": "file", "placeholder": false, "public_id": "any", "resource_type": "image", "secure_url": "https://my.updated.image/image.jpg", "signature": "abc", "tags": [], "type": "upload", "url": "http://my.updated.image/image.jpg", "version": 1582220685, "width": 720 }, "videoAvailable": true, "videoUploadProgress": "complete" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/doorbell_motion_activity_no_image.json000066400000000000000000000014631465672432400257160ustar00rootroot00000000000000{ "action": "doorbell_motion_detected", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1582220686158, "deviceID": "K98GiDT45GUL", "deviceName": "Front Door", "deviceType": "doorbell", "entities": { "activity": "any", "callingUser": "deleted", "device": "K98GiDT45GUL", "house": "any", "otherUser": "deleted" }, "house": { "houseID": "any", "houseName": "any" }, "info": { "dvrID": "any", "hasSubscription": false, "videoAvailable": true, "videoUploadProgress": "complete" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/doorbell_motion_activity_old.json000066400000000000000000000025001465672432400247070ustar00rootroot00000000000000{ "action": "doorbell_motion_detected", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1482220686158, "deviceID": "K98GiDT45GUL", "deviceName": "Front Door", "deviceType": "doorbell", "entities": { "activity": "any", "callingUser": "deleted", "device": "K98GiDT45GUL", "house": "any", "otherUser": "deleted" }, "house": { "houseID": "any", "houseName": "any" }, "info": { "dvrID": "any", "hasSubscription": false, "image": { "bytes": 42865, "created_at": "2020-02-20T17:44:45Z", "etag": "andy", "format": "jpg", "height": 576, "original_filename": "file", "placeholder": false, "public_id": "any", "resource_type": "image", "secure_url": "https://this.is.the.old.url/should_not_take.jpg", "signature": "abc", "tags": [], "type": "upload", "url": "http://this.is.the.old.url/should_not_take.jpg", "version": 1482220685, "width": 720 }, "videoAvailable": true, "videoUploadProgress": "complete" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/doorbell_motion_activity_wrong.json000066400000000000000000000024721465672432400252750ustar00rootroot00000000000000{ "action": "doorbell_motion_detected", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 1582220686158, "deviceID": "thisisthewrongdeviceid", "deviceName": "Front Door", "deviceType": "doorbell", "entities": { "activity": "any", "callingUser": "deleted", "device": "thisisthewrongdeviceid", "house": "any", "otherUser": "deleted" }, "house": { "houseID": "any", "houseName": "any" }, "info": { "dvrID": "any", "hasSubscription": false, "image": { "bytes": 42865, "created_at": "2020-02-20T17:44:45Z", "etag": "andy", "format": "jpg", "height": 576, "original_filename": "file", "placeholder": false, "public_id": "any", "resource_type": "image", "secure_url": "https://my.updated.image/image.jpg", "signature": "abc", "tags": [], "type": "upload", "url": "http://my.updated.image/image.jpg", "version": 1582220685, "width": 720 }, "videoAvailable": true, "videoUploadProgress": "complete" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/get_doorbell.battery_full.json000066400000000000000000000054321465672432400241110ustar00rootroot00000000000000{ "doorbellID": "did", "serialNumber": "sr", "appID": "august-iphone-v5", "installUserID": "iid", "name": "Front Door", "installDate": "2019-02-25T05:32:11.263Z", "pubsubChannel": "pubsub", "settings": { "ABREnabled": true, "auto_contrast_mode": 0, "backlight_comp": false, "batteryChargeCurrent": 60, "batteryLowThreshold": 3.1, "batteryUseThreshold": 3.4, "batteryRun": false, "bellTimerConfig": 50, "bitrateCeiling": 512000, "brightness": 50, "contrast": 50, "directLink": true, "debug": false, "flashBrightness": 40, "flashDuringCall": true, "flashOnPIR": true, "haloBrightnessX": 25, "hue": 50, "initialBitrate": 384000, "IREnabled": true, "irConfiguration": 16836880, "irSensitivity": 70, "IVAEnabled": false, "JPGQuality": 70, "keepEncoderRunning": true, "maintainIVConnection": true, "micVolume": 100, "minACNoScaling": 40, "minimumSnapshotInterval": 30, "overlayEnabled": true, "ringSoundEnabled": true, "saturation": 50, "sharpness": 50, "speakerVolume": 92, "turnOffCamera": false, "buttonpush_notifications": true, "motion_notifications": true, "notify_when_offline": false, "buttonpush_notifications_partners": false }, "type": "mars2", "dvrSubscriptionSetupDone": true, "secChipCertSerial": null, "createdAt": "2019-02-25T05:32:11.263Z", "updatedAt": "2019-02-25T05:50:03.087Z", "status": "doorbell_call_status_online", "statusUpdatedAtMs": 1582608961596, "telemetry": { "date": "2019-02-25 08:19:56", "BSSID": "a:b:c", "SSID": "none", "wifi_freq": 2437, "ip_addr": "4.3.3.2", "link_quality": 70, "signal_level": -35, "uptime": "9499.66 7505.92", "load_average": "0.42 0.39 0.45 2/180 5959", "temperature": 49.25, "steady_ac_in": 0, "battery": 4.147, "ac_in": 0, "doorbell_low_battery": false, "mcu_version": "900aecaf", "mcu_temp": 61.470001, "mcu_bat_cur": 63.779999, "mcu_dc_in": 4.925, "chg_state": "CHARGING", "updated_at": "2019-02-25T08:19:58.678Z" }, "caps": ["reconnect", "webrtc", "no_iv"], "doorbellServerURL": "https://doorbells.august.com", "recentImage": { "public_id": "pubid", "version": 123, "signature": "sig", "width": 480, "height": 640, "format": "jpg", "resource_type": "image", "created_at": "2020-02-25T05:50:02Z", "tags": [], "bytes": 25330, "type": "upload", "etag": "etag", "placeholder": false, "url": "http://of.jpg", "secure_url": "https://of.jpg", "original_filename": "file" }, "messagingProtocol": "pubnub", "chimes": [], "firmwareVersion": "2.3.0-VULRC18+201812100947", "invitations": [], "HouseID": "houseid", "HouseName": "housename" } yalexs-8.0.2/tests/fixtures/get_doorbell.battery_low.json000066400000000000000000000054201465672432400237450ustar00rootroot00000000000000{ "doorbellID": "did", "serialNumber": "sr", "appID": "august-iphone-v5", "installUserID": "iid", "name": "Front Door", "installDate": "2019-02-25T05:32:11.263Z", "pubsubChannel": "pubsub", "settings": { "ABREnabled": true, "auto_contrast_mode": 0, "backlight_comp": false, "batteryChargeCurrent": 60, "batteryLowThreshold": 3.1, "batteryUseThreshold": 3.4, "batteryRun": false, "bellTimerConfig": 50, "bitrateCeiling": 512000, "brightness": 50, "contrast": 50, "directLink": true, "debug": false, "flashBrightness": 40, "flashDuringCall": true, "flashOnPIR": true, "haloBrightnessX": 25, "hue": 50, "initialBitrate": 384000, "IREnabled": true, "irConfiguration": 16836880, "irSensitivity": 70, "IVAEnabled": false, "JPGQuality": 70, "keepEncoderRunning": true, "maintainIVConnection": true, "micVolume": 100, "minACNoScaling": 40, "minimumSnapshotInterval": 30, "overlayEnabled": true, "ringSoundEnabled": true, "saturation": 50, "sharpness": 50, "speakerVolume": 92, "turnOffCamera": false, "buttonpush_notifications": true, "motion_notifications": true, "notify_when_offline": false, "buttonpush_notifications_partners": false }, "type": "mars2", "dvrSubscriptionSetupDone": true, "secChipCertSerial": null, "createdAt": "2019-02-25T05:32:11.263Z", "updatedAt": "2019-02-25T05:50:03.087Z", "status": "doorbell_call_status_online", "statusUpdatedAtMs": 1582608961596, "telemetry": { "date": "2019-02-25 05:50:01", "BSSID": "4:d:d", "SSID": "zip", "wifi_freq": 2437, "ip_addr": "1.2.2.4", "link_quality": 70, "signal_level": -38, "uptime": "504.12 380.94", "load_average": "0.57 0.41 0.22 1/180 1149", "temperature": 45.625, "steady_ac_in": 0, "battery": 3.023, "ac_in": 0, "doorbell_low_battery": true, "mcu_version": "900aecaf", "mcu_temp": 59.009998, "mcu_bat_cur": 65, "mcu_dc_in": 4.978, "chg_state": "CHARGING", "updated_at": "2020-02-25T05:50:03.087Z" }, "caps": ["reconnect", "webrtc", "no_iv"], "doorbellServerURL": "https://doorbells.august.com", "recentImage": { "public_id": "pubid", "version": 123, "signature": "sig", "width": 480, "height": 640, "format": "jpg", "resource_type": "image", "created_at": "2020-02-25T05:50:02Z", "tags": [], "bytes": 25330, "type": "upload", "etag": "etag", "placeholder": false, "url": "http://of.jpg", "secure_url": "https://of.jpg", "original_filename": "file" }, "messagingProtocol": "pubnub", "chimes": [], "firmwareVersion": "2.3.0-VULRC18+201812100947", "invitations": [], "HouseID": "houseid", "HouseName": "housename" } yalexs-8.0.2/tests/fixtures/get_doorbell.battery_medium.json000066400000000000000000000054311465672432400244260ustar00rootroot00000000000000{ "doorbellID": "did", "serialNumber": "sr", "appID": "august-iphone-v5", "installUserID": "iid", "name": "Front Door", "installDate": "2019-02-25T05:32:11.263Z", "pubsubChannel": "pubsub", "settings": { "ABREnabled": true, "auto_contrast_mode": 0, "backlight_comp": false, "batteryChargeCurrent": 60, "batteryLowThreshold": 3.1, "batteryUseThreshold": 3.4, "batteryRun": false, "bellTimerConfig": 50, "bitrateCeiling": 512000, "brightness": 50, "contrast": 50, "directLink": true, "debug": false, "flashBrightness": 40, "flashDuringCall": true, "flashOnPIR": true, "haloBrightnessX": 25, "hue": 50, "initialBitrate": 384000, "IREnabled": true, "irConfiguration": 16836880, "irSensitivity": 70, "IVAEnabled": false, "JPGQuality": 70, "keepEncoderRunning": true, "maintainIVConnection": true, "micVolume": 100, "minACNoScaling": 40, "minimumSnapshotInterval": 30, "overlayEnabled": true, "ringSoundEnabled": true, "saturation": 50, "sharpness": 50, "speakerVolume": 92, "turnOffCamera": false, "buttonpush_notifications": true, "motion_notifications": true, "notify_when_offline": false, "buttonpush_notifications_partners": false }, "type": "mars2", "dvrSubscriptionSetupDone": true, "secChipCertSerial": null, "createdAt": "2019-02-25T05:32:11.263Z", "updatedAt": "2019-02-25T05:50:03.087Z", "status": "doorbell_call_status_online", "statusUpdatedAtMs": 1582608961596, "telemetry": { "date": "2019-02-25 08:19:56", "BSSID": "a:b:c", "SSID": "none", "wifi_freq": 2437, "ip_addr": "4.3.3.2", "link_quality": 70, "signal_level": -35, "uptime": "9499.66 7505.92", "load_average": "0.42 0.39 0.45 2/180 5959", "temperature": 49.25, "steady_ac_in": 0, "battery": 3.82, "ac_in": 0, "doorbell_low_battery": false, "mcu_version": "900aecaf", "mcu_temp": 61.470001, "mcu_bat_cur": 63.779999, "mcu_dc_in": 4.925, "chg_state": "CHARGING", "updated_at": "2019-02-25T08:19:58.678Z" }, "caps": ["reconnect", "webrtc", "no_iv"], "doorbellServerURL": "https://doorbells.august.com", "recentImage": { "public_id": "pubid", "version": 123, "signature": "sig", "width": 480, "height": 640, "format": "jpg", "resource_type": "image", "created_at": "2020-02-25T05:50:02Z", "tags": [], "bytes": 25330, "type": "upload", "etag": "etag", "placeholder": false, "url": "http://of.jpg", "secure_url": "https://of.jpg", "original_filename": "file" }, "messagingProtocol": "pubnub", "chimes": [], "firmwareVersion": "2.3.0-VULRC18+201812100947", "invitations": [], "HouseID": "houseid", "HouseName": "housename" } yalexs-8.0.2/tests/fixtures/get_doorbell.json000066400000000000000000000046531465672432400214220ustar00rootroot00000000000000{ "doorbellID": "K98GiDT45GUL", "serialNumber": "tBXZR0Z35E", "appID": "august-iphone", "installUserID": "c3b2a94e-373e-aaaa-bbbb-36e996827777", "name": "Front Door", "type": "gen1", "installDate": "2016-11-26T22:27:11.176Z", "pubsubChannel": "7c7a6672-59c8-3333-ffff-dcd98705cccc", "settings": { "speakerVolume": 92, "micVolume": 100, "IREnabled": true, "debug": false, "initialBitrate": 384000, "bitrateCeiling": 512000, "ABREnabled": true, "JPGQuality": 70, "IVAEnabled": false, "batteryLowThreshold": 3.1, "batteryUseThreshold": 3.4, "directLink": true, "irConfiguration": 8448272, "ringSoundEnabled": true, "batteryRun": false, "videoResolution": "640x480", "minACNoScaling": 40, "turnOffCamera": false, "overlayEnabled": true, "keepEncoderRunning": true, "motion_notifications": true, "notify_when_offline": true, "buttonpush_notifications": true }, "createdAt": "2016-11-26T22:27:11.176Z", "updatedAt": "2017-12-10T08:05:13.650Z", "status": "doorbell_call_status_online", "telemetry": { "date": "2017-12-10 08:05:12", "BSSID": "88:ee:00:dd:aa:11", "SSID": "foo_ssid", "wifi_freq": 5745, "ip_addr": "10.0.1.11", "link_quality": 54, "signal_level": -56, "uptime": "16168.75 13830.49", "load_average": "0.50 0.47 0.35 1/154 9345", "battery_soc": 96, "battery_soh": 95, "temperature": 28.25, "steady_ac_in": 22.196405, "battery": 4.061763, "ac_in": 23.856874, "doorbell_low_battery": false, "updated_at": "2017-12-10T08:05:13.650Z" }, "doorbellServerURL": "https://doorbells.august.com", "caps": ["reconnect"], "recentImage": { "public_id": "qqqqt4ctmxwsysylaaaa", "version": 1512892814, "signature": "75z47ca21b5e8ffda21d2134e478a2307c4625da", "width": 480, "height": 640, "format": "jpg", "resource_type": "image", "created_at": "2017-12-10T08:01:35Z", "tags": [], "bytes": 24476, "type": "upload", "etag": "54966926be2e93f77d498a55f247661f", "placeholder": false, "url": "http://image.com/vmk16naaaa7ibuey7sar.jpg", "secure_url": "https://image.com/vmk16naaaa7ibuey7sar.jpg", "original_filename": "file" }, "dvrSubscriptionSetupDone": true, "status_timestamp": 1512811834532, "LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA", "firmwareVersion": "2.3.0-RC153+201711151527", "HouseID": "3dd2accaea08" } yalexs-8.0.2/tests/fixtures/get_doorbell.offline.json000066400000000000000000000065251465672432400230430ustar00rootroot00000000000000{ "HouseID": "houseid", "statusUpdatedAtMs": 1282243101579, "chimes": [ { "doorbellID": "231ee2168dd0", "serialNumber": "chimeserial", "firmware": "3.1.16", "createdAt": "2019-02-12T03:55:38.805Z", "name": "Living Room", "chimeID": "chime1", "_id": "idx", "type": 1, "updatedAt": "2019-02-12T03:55:38.805Z" } ], "type": "hydra1", "updatedAt": "2019-02-20T23:58:21.580Z", "caps": ["reconnect", "webrtc", "tcp_wakeup"], "firmwareVersion": "3.1.0-HYDRC75+201909251139", "doorbellServerURL": "https://doorbells.august.com", "appID": "august-iphone-v5", "serialNumber": "abcd", "invitations": [], "installDate": "2020-02-03T02:52:21.719Z", "secChipCertSerial": "", "createdAt": "2019-02-12T03:52:28.719Z", "messagingProtocol": "pubnub", "name": "My Door", "doorbellID": "231ee2168dd0", "HouseName": "zulu", "installUserID": "userid", "settings": { "irLedBrightness": 40, "directLink": true, "buttonpush_notifications": true, "auto_contrast_mode": 0, "micVolume": 50, "minimumSnapshotInterval": 30, "minACNoScaling": 40, "saturation": 50, "IVAEnabled": false, "videoResolution": "720x576", "initialBitrate": 1000000, "JPGQuality": 70, "DVRRecordingTimeout": 15, "irConfiguration": 16836880, "batteryLowThreshold": 3.1, "pirSensitivity": 20, "buttonpush_notifications_partners": false, "batteryChargeCurrent": 60, "motion_notifications": true, "keepEncoderRunning": true, "pirWindowTime": 0, "notify_when_offline": false, "brightness": 50, "sharpness": 50, "ringSoundEnabled": true, "contrast": 50, "speakerVolume": 92, "bitrateCeiling": 2000000, "debug": false, "powerProfilePreset": -1, "batteryUseThreshold": 3.4, "pirPulseCounter": 1, "overlayEnabled": true, "IREnabled": true, "overCurrentThreshold": -250, "ABREnabled": true, "pirConfiguration": 272, "backlight_comp": false, "pirBlindTime": 7, "ringRepetitions": 3, "nightModeAlsThreshold": 10, "turnOffCamera": false, "hue": 50 }, "telemetry": { "battery_temp": 22, "doorbell_low_battery": false, "battery_avg_cur": -291, "battery_soc": 81, "battery_soh": 95, "beacon_interval": 0, "BSSIDManufacturer": "happy networks", "BSSID": "7ds", "updated_at": "2019-02-20T23:53:09.586Z", "dtim_period": 0, "ip_addr": "192.168.1.1", "SSID": "dsdsd", "uptime": "96.55 70.59", "signal_level": -49, "load_average": "0.45 0.18 0.07 4/98 831", "date": "2020-02-20 11:47:36", "battery": 3.985, "wifi_freq": 2462 }, "recentImage": { "width": 720, "etag": "etag", "created_at": "2019-02-20T23:52:46Z", "height": 576, "type": "upload", "url": "http://res.cloudinary.com/x.jpg", "secure_url": "https://res.cloudinary.com/x.jpg", "placeholder": false, "original_filename": "file", "resource_type": "image", "version": 1282242763, "signature": "sig", "tags": [], "public_id": "fdsafsdfds", "bytes": 50013, "format": "jpg" }, "tcpKeepAlive": { "keepAliveUUID": "uuid", "wakeUp": { "lastUpdated": 12822427223931, "token": "wakemeup" } }, "status": "doorbell_offline", "dvrSubscriptionSetupDone": true, "pubsubChannel": "pubsub" } yalexs-8.0.2/tests/fixtures/get_doorbell_missing_image.json000066400000000000000000000035421465672432400243110ustar00rootroot00000000000000{ "doorbellID": "K98GiDT45GUL", "serialNumber": "tBXZR0Z35E", "appID": "august-iphone", "installUserID": "c3b2a94e-373e-aaaa-bbbb-36e996827777", "name": "Front Door", "installDate": "2016-11-26T22:27:11.176Z", "pubsubChannel": "7c7a6672-59c8-3333-ffff-dcd98705cccc", "settings": { "speakerVolume": 92, "micVolume": 100, "IREnabled": true, "debug": false, "initialBitrate": 384000, "bitrateCeiling": 512000, "ABREnabled": true, "JPGQuality": 70, "IVAEnabled": false, "batteryLowThreshold": 3.1, "batteryUseThreshold": 3.4, "directLink": true, "irConfiguration": 8448272, "ringSoundEnabled": true, "batteryRun": false, "videoResolution": "640x480", "minACNoScaling": 40, "turnOffCamera": false, "overlayEnabled": true, "keepEncoderRunning": true, "motion_notifications": true, "notify_when_offline": true, "buttonpush_notifications": true }, "createdAt": "2016-11-26T22:27:11.176Z", "updatedAt": "2017-12-10T08:05:13.650Z", "status": "doorbell_call_status_online", "telemetry": { "date": "2017-12-10 08:05:12", "BSSID": "88:ee:00:dd:aa:11", "SSID": "foo_ssid", "wifi_freq": 5745, "ip_addr": "10.0.1.11", "link_quality": 54, "signal_level": -56, "uptime": "16168.75 13830.49", "load_average": "0.50 0.47 0.35 1/154 9345", "battery_soc": 96, "battery_soh": 95, "temperature": 28.25, "steady_ac_in": 22.196405, "battery": 4.061763, "ac_in": 23.856874, "doorbell_low_battery": false, "updated_at": "2017-12-10T08:05:13.650Z" }, "doorbellServerURL": "https://doorbells.august.com", "caps": ["reconnect"], "dvrSubscriptionSetupDone": true, "status_timestamp": 1512811834532, "LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA", "firmwareVersion": "2.3.0-RC153+201711151527", "HouseID": "3dd2accaea08" } yalexs-8.0.2/tests/fixtures/get_doorbells.json000066400000000000000000000043031465672432400215750ustar00rootroot00000000000000{ "K98GiDT45GUL": { "_id": "epoZ87XSPqxlFdsaYyJiRRVR", "doorbellID": "K98GiDT45GUL", "serialNumber": "tBXZR0Z35E", "appID": "august-iphone", "installUserID": "c3b3a94f-473z-61a3-a8d1-a6e99482787a", "name": "Front Door", "installDate": "2016-11-26T22:27:11.176Z", "currentDoorbellAppVersion": "1.1.0-RC152+201710181449", "pubsubChannel": "7c7a6672-59c8-49f4-8faf-dcd98705c159", "settings": { "motion_notifications": true, "notify_when_offline": true, "buttonpush_notifications": true }, "createdAt": "2016-11-26T22:27:11.176Z", "updatedAt": "2017-11-23T00:42:19.470Z", "status": "doorbell_call_status_online", "telemetry": { "date": "2017-11-23 12:42:17", "BSSID": "98:44:55:66:77:88", "SSID": "foobar", "wifi_freq": 5711, "ip_addr": "10.1.1.116", "link_quality": 58, "signal_level": -52, "uptime": "52910.91 45197.84", "load_average": "0.19 0.21 0.23 2/149 27464", "temperature": 36.5, "steady_ac_in": 22.113834, "battery": 4.057726, "ac_in": 24.305649, "doorbell_low_battery": false, "updated_at": "2017-11-23T00:42:19.470Z" }, "doorbellServerURL": "https://doorbells.august.com", "caps": ["reconnect"], "recentImage": { "public_id": "GjtkxMwBKde5krHDrd7K", "version": 1511381838, "signature": "aa7a582ed85224fd330dd8c011d012f9c1a44f6a", "width": 480, "height": 640, "format": "jpg", "resource_type": "image", "created_at": "2017-11-22T20:17:19Z", "tags": [], "bytes": 41215, "type": "upload", "etag": "f35fdda689067355d4e322e1cc209b11", "placeholder": false, "url": "http://res.cloudinary.com/august-com/image/upload/v1111381888/HjtkxMwBaade5bbbbrd7K.jpg", "secure_url": "https://image.com/vmk16naaaa7ibuey7sar.jpg", "original_filename": "file" }, "dvrSubscriptionSetupDone": true, "HouseID": "3dd2accaea08" }, "1KDAbJH89XYZ": { "doorbellID": "1KDAbJH89XYZ", "serialNumber": "aaaaR08888", "status": "doorbell_call_status_offline", "name": "Back Door", "dvrSubscriptionSetupDone": false, "HouseID": "3dd2accadddd" } } yalexs-8.0.2/tests/fixtures/get_house_activities.json000066400000000000000000000205271465672432400231650ustar00rootroot00000000000000[ { "action": "lock", "callingUser": { "FirstName": "MockHouse", "LastName": "House", "UserID": "mockUserId2" }, "dateTime": 1234, "deviceID": "mockDeviceId2", "deviceName": "MockHouseTDoor", "deviceType": "lock", "entities": { "activity": "mockActivity2", "callingUser": "mockUserId2", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "unlock", "callingUser": { "FirstName": "MockHouse", "LastName": "House", "UserID": "mockUserId2" }, "dateTime": 45454, "deviceID": "mockDeviceId2", "deviceName": "MockHouseXDoor", "deviceType": "lock", "entities": { "activity": "ActivityId", "callingUser": "mockUserId2", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "lock", "callingUser": { "FirstName": "MockHouse", "LastName": "House", "UserID": "mockUserId2" }, "dateTime": 12345, "deviceID": "mockDeviceId2", "deviceName": "MockHouseRdoor", "deviceType": "lock", "entities": { "activity": "Activity", "callingUser": "mockUserId2", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "unlock", "callingUser": { "FirstName": "MockHouse", "LastName": "House", "UserID": "mockUserId2" }, "dateTime": 5678, "deviceID": "mockDeviceId2", "deviceName": "MockHouseYDoor", "deviceType": "lock", "entities": { "activity": "Activity", "callingUser": "mockUserId2", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "lock", "callingUser": { "FirstName": "MockHouse", "LastName": "House", "UserID": "mockUserId2" }, "dateTime": 114334, "deviceID": "mockDeviceId2", "deviceName": "MockHouseQDoor", "deviceType": "lock", "entities": { "activity": "Activity", "callingUser": "mockUserId2", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "doorclosed", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 545454, "deviceID": "mockDeviceId2", "deviceName": "MockHouse Tech Room Door", "deviceType": "lock", "entities": { "activity": "activityId", "callingUser": "deleted", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "dooropen", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 5454, "deviceID": "mockDeviceId2", "deviceName": "MockHouse Tech Room Door", "deviceType": "lock", "entities": { "activity": "ActivityId", "callingUser": "deleted", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "doorclosed", "callingUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "dateTime": 545435, "deviceID": "mockDeviceId2", "deviceName": "MockHouse Tech Room Door", "deviceType": "lock", "entities": { "activity": "ActivityId", "callingUser": "deleted", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDeviceId2+Time" }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } }, { "action": "unlock", "callingUser": { "FirstName": "mockFirstName", "LastName": "mockLastName", "UserID": "MockUserId", "imageInfo": { "original": { "format": "jpg", "height": 7, "secure_url": "mockurl", "url": "mockurl", "width": 7 }, "thumbnail": { "format": "jpg", "height": 3, "secure_url": "mockurl", "url": "mockurl", "width": 3 } } }, "dateTime": 44354, "deviceID": "mockDeviceId2", "deviceName": "MockHouseDoor", "deviceType": "lock", "entities": { "activity": "mockActivityId", "callingUser": "mockCallingUser5", "device": "mockDeviceId2", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "agent": "mercury", "keypad": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" }, "source": { "sourceType": "mercury" } }, { "action": "lock", "callingUser": { "FirstName": "mockFirstName1", "LastName": "House", "UserID": "mockCallingUser1" }, "dateTime": 543454, "deviceID": "mockDevice1", "deviceName": "MockHouseGDoor", "deviceType": "lock", "entities": { "activity": "mockActivityId1", "callingUser": "mockCallingUser1", "device": "mockDevice1", "house": "mock-house-id", "otherUser": "deleted" }, "house": { "houseID": "mock-house-id", "houseName": "MockHouse" }, "info": { "DateLogActionID": "mockDevice1+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } ] yalexs-8.0.2/tests/fixtures/get_lock.doorsense_init.json000066400000000000000000000052401465672432400235640ustar00rootroot00000000000000{ "LockName": "Front Door Lock", "Type": 2, "Created": "2017-12-10T03:12:09.210Z", "Updated": "2017-12-10T03:12:09.210Z", "LockID": "A6697750D607098BAE8D6BAA11EF8063", "HouseID": "000000000000", "HouseName": "My House", "Calibrated": false, "skuNumber": "AUG-SL02-M02-S02", "timeZone": "America/Vancouver", "battery": 0.88, "SerialNumber": "X2FSW05DGA", "LockStatus": { "status": "locked", "doorState": "init", "dateTime": "2017-12-10T04:48:30.272Z", "isLockStatusChanged": false, "valid": true }, "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", "homeKitEnabled": false, "zWaveEnabled": false, "isGalileo": false, "Bridge": { "_id": "aaacab87f7efxa0015884999", "mfgBridgeID": "AAGPP102XX", "deviceModel": "august-doorbell", "firmwareVersion": "2.3.0-RC153+201711151527", "operative": true }, "keypad": { "_id": "5bc65c24e6ef2a263e1450a8", "serialNumber": "K1GXB0054Z", "lockID": "92412D1B44004595B5DEB134E151A8D3", "currentFirmwareVersion": "2.27.0", "battery": {}, "batteryLevel": "Medium", "batteryRaw": 170 }, "OfflineKeys": { "created": [], "loaded": [ { "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", "slot": 1, "key": "kkk01d4300c1dcxxx1c330f794941111", "created": "2017-12-10T03:12:09.215Z", "loaded": "2017-12-10T03:12:54.391Z" } ], "deleted": [], "loadedhk": [ { "key": "kkk01d4300c1dcxxx1c330f794941222", "slot": 256, "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", "created": "2017-12-10T03:12:09.218Z", "loaded": "2017-12-10T03:12:55.563Z" } ] }, "parametersToSet": {}, "users": { "cccca94e-373e-aaaa-bbbb-333396827777": { "UserType": "superuser", "FirstName": "Foo", "LastName": "Bar", "identifiers": ["email:foo@bar.com", "phone:+177777777777"], "imageInfo": { "original": { "width": 948, "height": 949, "format": "jpg", "url": "http://www.image.com/foo.jpeg", "secure_url": "https://www.image.com/foo.jpeg" }, "thumbnail": { "width": 128, "height": 128, "format": "jpg", "url": "http://www.image.com/foo.jpeg", "secure_url": "https://www.image.com/foo.jpeg" } } } }, "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", "ruleHash": {}, "cameras": [], "geofenceLimits": { "ios": { "debounceInterval": 90, "gpsAccuracyMultiplier": 2.5, "maximumGeofence": 5000, "minimumGeofence": 100, "minGPSAccuracyRequired": 80 } } } yalexs-8.0.2/tests/fixtures/get_lock.nostatus_with_doorsense.json000066400000000000000000000020701465672432400255320ustar00rootroot00000000000000{ "Bridge": { "_id": "bridgeid", "deviceModel": "august-connect", "firmwareVersion": "2.2.1", "hyperBridge": true, "mfgBridgeID": "C5WY200WSH", "operative": true, "status": { "current": "online", "lastOffline": "2000-00-00T00:00:00.447Z", "lastOnline": "2000-00-00T00:00:00.447Z", "updated": "2000-00-00T00:00:00.447Z" } }, "Calibrated": false, "Created": "2000-00-00T00:00:00.447Z", "HouseID": "123", "HouseName": "Test", "LockID": "ABC", "LockName": "Online door with doorsense", "SerialNumber": "XY", "Type": 1001, "Updated": "2000-00-00T00:00:00.447Z", "battery": 0.922, "currentFirmwareVersion": "undefined-4.3.0-1.8.14", "homeKitEnabled": true, "hostLockInfo": { "manufacturer": "yale", "productID": 1536, "productTypeID": 32770, "serialNumber": "ABC" }, "isGalileo": false, "macAddress": "12:22", "pins": { "created": [], "loaded": [] }, "skuNumber": "AUG-MD01", "supportsEntryCodes": true, "timeZone": "Pacific/Hawaii", "zWaveEnabled": false } yalexs-8.0.2/tests/fixtures/get_lock.offline.json000066400000000000000000000026731465672432400221710ustar00rootroot00000000000000{ "Calibrated": false, "Created": "2000-00-00T00:00:00.447Z", "HouseID": "houseid", "HouseName": "MockName", "LockID": "ABC", "LockName": "Test", "LockStatus": { "status": "unknown" }, "OfflineKeys": { "created": [], "createdhk": [ { "UserID": "mock-user-id", "created": "2000-00-00T00:00:00.447Z", "key": "mockkey", "slot": 12 } ], "deleted": [], "loaded": [ { "UserID": "userid", "created": "2000-00-00T00:00:00.447Z", "key": "key", "loaded": "2000-00-00T00:00:00.447Z", "slot": 1 } ] }, "SerialNumber": "ABC", "Type": 3, "Updated": "2000-00-00T00:00:00.447Z", "battery": -1, "cameras": [], "currentFirmwareVersion": "undefined-1.59.0-1.13.2", "geofenceLimits": { "ios": { "debounceInterval": 90, "gpsAccuracyMultiplier": 2.5, "maximumGeofence": 5000, "minGPSAccuracyRequired": 80, "minimumGeofence": 100 } }, "homeKitEnabled": false, "isGalileo": false, "macAddress": "a:b:c", "parametersToSet": {}, "pubsubChannel": "mockpubsub", "ruleHash": {}, "skuNumber": "AUG-X", "supportsEntryCodes": false, "users": { "mockuserid": { "FirstName": "MockName", "LastName": "House", "UserType": "superuser", "identifiers": ["phone:+15558675309", "email:mockme@mock.org"] } }, "zWaveDSK": "1-2-3-4", "zWaveEnabled": true } yalexs-8.0.2/tests/fixtures/get_lock.online.json000066400000000000000000000052411465672432400220250ustar00rootroot00000000000000{ "LockName": "Front Door Lock", "Type": 2, "Created": "2017-12-10T03:12:09.210Z", "Updated": "2017-12-10T03:12:09.210Z", "LockID": "A6697750D607098BAE8D6BAA11EF8063", "HouseID": "000000000000", "HouseName": "My House", "Calibrated": false, "skuNumber": "AUG-SL02-M02-S02", "timeZone": "America/Vancouver", "battery": 0.88, "SerialNumber": "X2FSW05DGA", "LockStatus": { "status": "locked", "doorState": "closed", "dateTime": "2017-12-10T04:48:30.272Z", "isLockStatusChanged": true, "valid": true }, "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", "homeKitEnabled": false, "zWaveEnabled": false, "isGalileo": false, "Bridge": { "_id": "aaacab87f7efxa0015884999", "mfgBridgeID": "AAGPP102XX", "deviceModel": "august-doorbell", "firmwareVersion": "2.3.0-RC153+201711151527", "operative": true }, "keypad": { "_id": "5bc65c24e6ef2a263e1450a8", "serialNumber": "K1GXB0054Z", "lockID": "92412D1B44004595B5DEB134E151A8D3", "currentFirmwareVersion": "2.27.0", "battery": {}, "batteryLevel": "Medium", "batteryRaw": 170 }, "OfflineKeys": { "created": [], "loaded": [ { "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", "slot": 1, "key": "kkk01d4300c1dcxxx1c330f794941111", "created": "2017-12-10T03:12:09.215Z", "loaded": "2017-12-10T03:12:54.391Z" } ], "deleted": [], "loadedhk": [ { "key": "kkk01d4300c1dcxxx1c330f794941222", "slot": 256, "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", "created": "2017-12-10T03:12:09.218Z", "loaded": "2017-12-10T03:12:55.563Z" } ] }, "parametersToSet": {}, "users": { "cccca94e-373e-aaaa-bbbb-333396827777": { "UserType": "superuser", "FirstName": "Foo", "LastName": "Bar", "identifiers": ["email:foo@bar.com", "phone:+177777777777"], "imageInfo": { "original": { "width": 948, "height": 949, "format": "jpg", "url": "http://www.image.com/foo.jpeg", "secure_url": "https://www.image.com/foo.jpeg" }, "thumbnail": { "width": 128, "height": 128, "format": "jpg", "url": "http://www.image.com/foo.jpeg", "secure_url": "https://www.image.com/foo.jpeg" } } } }, "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", "ruleHash": {}, "cameras": [], "geofenceLimits": { "ios": { "debounceInterval": 90, "gpsAccuracyMultiplier": 2.5, "maximumGeofence": 5000, "minimumGeofence": 100, "minGPSAccuracyRequired": 80 } } } yalexs-8.0.2/tests/fixtures/get_lock.online_with_doorsense.json000066400000000000000000000023401465672432400251360ustar00rootroot00000000000000{ "Bridge": { "_id": "bridgeid", "deviceModel": "august-connect", "firmwareVersion": "2.2.1", "hyperBridge": true, "mfgBridgeID": "C5WY200WSH", "operative": true, "status": { "current": "online", "lastOffline": "2000-00-00T00:00:00.447Z", "lastOnline": "2000-00-00T00:00:00.447Z", "updated": "2000-00-00T00:00:00.447Z" } }, "Calibrated": false, "Created": "2000-00-00T00:00:00.447Z", "HouseID": "123", "HouseName": "Test", "LockID": "ABC", "LockName": "Online door with doorsense", "LockStatus": { "dateTime": "2017-12-10T04:48:30.272Z", "doorState": "open", "isLockStatusChanged": false, "status": "locked", "valid": true }, "SerialNumber": "XY", "Type": 1001, "Updated": "2000-00-00T00:00:00.447Z", "battery": 0.922, "currentFirmwareVersion": "undefined-4.3.0-1.8.14", "homeKitEnabled": true, "hostLockInfo": { "manufacturer": "yale", "productID": 1536, "productTypeID": 32770, "serialNumber": "ABC" }, "isGalileo": false, "macAddress": "12:22", "pins": { "created": [], "loaded": [] }, "skuNumber": "AUG-MD01", "supportsEntryCodes": true, "timeZone": "Pacific/Hawaii", "zWaveEnabled": false } yalexs-8.0.2/tests/fixtures/get_lock.online_with_doorsense_disabled.json000066400000000000000000000023451465672432400267720ustar00rootroot00000000000000{ "Bridge": { "_id": "bridgeid", "deviceModel": "august-connect", "firmwareVersion": "2.2.1", "hyperBridge": true, "mfgBridgeID": "C5WY200WSH", "operative": true, "status": { "current": "online", "lastOffline": "2000-00-00T00:00:00.447Z", "lastOnline": "2000-00-00T00:00:00.447Z", "updated": "2000-00-00T00:00:00.447Z" } }, "Calibrated": false, "Created": "2000-00-00T00:00:00.447Z", "HouseID": "123", "HouseName": "Test", "LockID": "ABC", "LockName": "Online door with doorsense disabled", "LockStatus": { "dateTime": "2017-12-10T04:48:30.272Z", "doorState": "", "isLockStatusChanged": false, "status": "locked", "valid": true }, "SerialNumber": "XY", "Type": 1001, "Updated": "2000-00-00T00:00:00.447Z", "battery": 0.922, "currentFirmwareVersion": "undefined-4.3.0-1.8.14", "homeKitEnabled": true, "hostLockInfo": { "manufacturer": "yale", "productID": 1536, "productTypeID": 32770, "serialNumber": "ABC" }, "isGalileo": false, "macAddress": "12:22", "pins": { "created": [], "loaded": [] }, "skuNumber": "AUG-MD01", "supportsEntryCodes": true, "timeZone": "Pacific/Hawaii", "zWaveEnabled": false } yalexs-8.0.2/tests/fixtures/get_lock_v2.online.json000066400000000000000000000034141465672432400224340ustar00rootroot00000000000000{ "LockName": "Front Door", "Type": 2, "Created": "2020-07-25T20:36:08.329Z", "Updated": "2020-07-25T20:36:08.329Z", "LockID": "snip", "HouseID": "snip", "HouseName": "snip", "Calibrated": false, "timeZone": "Europe/Lisbon", "battery": 0.9615767335162673, "supportsEntryCodes": false, "remoteOperateSecret": "snip", "skuNumber": "AUG-SL02-M02-S02", "macAddress": "snip", "SerialNumber": "snip", "LockStatus": { "status": "locked", "dateTime": "2022-01-14T21:03:10.231Z", "isLockStatusChanged": false, "valid": true }, "currentFirmwareVersion": "3.0.44-3.0.29", "homeKitEnabled": false, "zWaveEnabled": false, "isGalileo": false, "Bridge": { "_id": "abc", "mfgBridgeID": "C5WIC00090", "deviceModel": "august-connect", "firmwareVersion": "2.2.15", "operative": true, "status": { "current": "online", "lastOnline": "2022-01-14T07:59:07.651Z", "updated": "2022-01-14T07:59:07.651Z", "lastOffline": "2022-01-12T20:22:11.488Z" }, "locks": [ { "_id": "someid", "LockID": "lock123", "macAddress": "snip" } ], "hyperBridge": false }, "OfflineKeys": { "created": [], "loaded": [ { "created": "2022-01-14T21:14:50.153Z", "key": "XXXXXX", "slot": 1, "UserID": "XXXXXX", "loaded": "2022-01-14T21:14:54.568Z" } ], "deleted": [], "createdhk": [] }, "parametersToSet": {}, "users": {}, "pubsubChannel": "snip", "ruleHash": {}, "invitations": [], "cameras": [], "geofenceLimits": { "ios": { "debounceInterval": 90, "gpsAccuracyMultiplier": 2.5, "maximumGeofence": 5000, "minimumGeofence": 100, "minGPSAccuracyRequired": 80 } } } yalexs-8.0.2/tests/fixtures/get_locks.json000066400000000000000000000006311465672432400207230ustar00rootroot00000000000000{ "A6697750D607098BAE8D6BAA11EF8063": { "LockName": "Front Door Lock", "UserType": "superuser", "macAddress": "2E:BA:C4:14:3F:09", "HouseID": "000000000000", "HouseName": "A House" }, "A6697750D607098BAE8D6BAA11EF9999": { "LockName": "Back Door Lock", "UserType": "user", "macAddress": "2E:BA:C4:14:3F:88", "HouseID": "000000000011", "HouseName": "A House" } } yalexs-8.0.2/tests/fixtures/get_pins.json000066400000000000000000000012321465672432400205570ustar00rootroot00000000000000{ "loaded": [ { "_id": "epoZ87XSPqxlFdsaYyJiRRVR", "lockID": "A6697750D607098BAE8D6BAA11EF8063", "userID": "c3b3a94f-473z-61a3-a8d1-a6e99482787a", "state": "in-use", "pin": "123456", "slot": 646545456465161, "accessType": "one-time", "accessStartTime": "2018-01-01T01:01:01.563Z", "accessEndTime": "2018-12-01T01:01:01.563Z", "accessTimes": "2018-11-05T10:02:41.684Z", "createdAt": "2016-11-26T22:27:11.176Z", "updatedAt": "2017-11-23T00:42:19.470Z", "loadedDate": "2017-12-10T03:12:55.563Z", "firstName": "John", "lastName": "Doe", "unverified": true } ] } yalexs-8.0.2/tests/fixtures/homekey_unlock_activity_v4.json000066400000000000000000000007131465672432400243130ustar00rootroot00000000000000{ "id": "b4e50f6c-730a-4ae1-991e-e5231d727c11", "timestamp": 1665388800273, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/homekey_unlock@3x.png", "action": "homekey_unlock", "deviceID": "102A6A4F31584849971D11807272E7A8", "deviceType": "lock", "user": { "UserID": "83f02a1d-c08a-4c3d-9c30-3b66e185c7bd", "FirstName": "89", "LastName": "House" }, "title": "89 House unlocked Gate rf_unlock" } yalexs-8.0.2/tests/fixtures/keypad_lock_activity.json000066400000000000000000000013471465672432400231570ustar00rootroot00000000000000{ "entities": { "device": "deviceid", "callingUser": "callinguser", "otherUser": "deleted", "house": "houseid", "activity": "activityid" }, "house": { "houseID": "house", "houseName": "namehouse" }, "source": { "sourceType": "mercury" }, "eventID": "eventid", "dateTime": 1583535312002, "action": "lock", "deviceName": "lockname", "deviceID": "deviceid", "deviceType": "lock", "callingUser": { "UserID": "userid", "FirstName": "My", "LastName": "Name" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "agent": "mercury", "keypad": true } } yalexs-8.0.2/tests/fixtures/lock.json000066400000000000000000000011501465672432400176760ustar00rootroot00000000000000{ "resultsFromOperationCache": false, "retryCount": 1, "info": { "lockType": "lock_version_3", "lockID": "ABC123", "lockStatusChanged": true, "rssi": -87, "wlanRSSI": -42, "context": { "startDate": "2020-02-19T19:44:54.370Z", "transactionID": "transid", "retryCount": 1 }, "serialNumber": "serial", "action": "lock", "wlanSNR": 56, "duration": 3119, "startTime": "2020-02-19T19:44:54.371Z", "serial": "serial", "bridgeID": "brdigeid" }, "doorState": "kAugDoorState_Closed", "status": "kAugLockState_Locked", "totalTime": 3133 } yalexs-8.0.2/tests/fixtures/lock_accessory_motion_detect.json000066400000000000000000000005401465672432400246700ustar00rootroot00000000000000{ "action": "lock_accessory_motion_detect", "deviceID": "3ED27C5DB830439CAA4F381DC8F16444", "deviceType": "lock", "icon": "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/lock_accessory_motion_detect@3x.png", "id": "97364da9-cd43-4dc1-b20a-19567692dd21", "timestamp": 1691249378000, "title": "Someone is at the Front Door." } yalexs-8.0.2/tests/fixtures/lock_activity.json000066400000000000000000000012611465672432400216150ustar00rootroot00000000000000{ "action": "lock", "callingUser": { "FirstName": "MockHouse", "LastName": "House", "UserID": "mockUserId2" }, "dateTime": 1582007218000, "deviceID": "ABC", "deviceName": "MockHouseTDoor", "deviceType": "lock", "entities": { "activity": "mockActivity2", "callingUser": "mockUserId2", "device": "ABC", "house": "123", "otherUser": "deleted" }, "house": { "houseID": "123", "houseName": "MockHouse" }, "info": { "DateLogActionID": "ABC+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/lock_with_doorbell.online.json000066400000000000000000000052411465672432400241030ustar00rootroot00000000000000{ "LockName": "Front Door Lock", "Type": 7, "Created": "2017-12-10T03:12:09.210Z", "Updated": "2017-12-10T03:12:09.210Z", "LockID": "A6697750D607098BAE8D6BAA11EF8063", "HouseID": "000000000000", "HouseName": "My House", "Calibrated": false, "skuNumber": "AUG-SL02-M02-S02", "timeZone": "America/Vancouver", "battery": 0.88, "SerialNumber": "X2FSW05DGA", "LockStatus": { "status": "locked", "doorState": "closed", "dateTime": "2017-12-10T04:48:30.272Z", "isLockStatusChanged": true, "valid": true }, "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", "homeKitEnabled": false, "zWaveEnabled": false, "isGalileo": false, "Bridge": { "_id": "aaacab87f7efxa0015884999", "mfgBridgeID": "AAGPP102XX", "deviceModel": "august-doorbell", "firmwareVersion": "2.3.0-RC153+201711151527", "operative": true }, "keypad": { "_id": "5bc65c24e6ef2a263e1450a8", "serialNumber": "K1GXB0054Z", "lockID": "92412D1B44004595B5DEB134E151A8D3", "currentFirmwareVersion": "2.27.0", "battery": {}, "batteryLevel": "Medium", "batteryRaw": 170 }, "OfflineKeys": { "created": [], "loaded": [ { "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", "slot": 1, "key": "kkk01d4300c1dcxxx1c330f794941111", "created": "2017-12-10T03:12:09.215Z", "loaded": "2017-12-10T03:12:54.391Z" } ], "deleted": [], "loadedhk": [ { "key": "kkk01d4300c1dcxxx1c330f794941222", "slot": 256, "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", "created": "2017-12-10T03:12:09.218Z", "loaded": "2017-12-10T03:12:55.563Z" } ] }, "parametersToSet": {}, "users": { "cccca94e-373e-aaaa-bbbb-333396827777": { "UserType": "superuser", "FirstName": "Foo", "LastName": "Bar", "identifiers": ["email:foo@bar.com", "phone:+177777777777"], "imageInfo": { "original": { "width": 948, "height": 949, "format": "jpg", "url": "http://www.image.com/foo.jpeg", "secure_url": "https://www.image.com/foo.jpeg" }, "thumbnail": { "width": 128, "height": 128, "format": "jpg", "url": "http://www.image.com/foo.jpeg", "secure_url": "https://www.image.com/foo.jpeg" } } } }, "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", "ruleHash": {}, "cameras": [], "geofenceLimits": { "ios": { "debounceInterval": 90, "gpsAccuracyMultiplier": 2.5, "maximumGeofence": 5000, "minimumGeofence": 100, "minGPSAccuracyRequired": 80 } } } yalexs-8.0.2/tests/fixtures/lock_with_unlatch.online.json000066400000000000000000000051471465672432400237440ustar00rootroot00000000000000{ "LockName": "Wohnung", "Type": 17, "Created": "2024-03-14T18:03:09.003Z", "Updated": "2024-03-14T18:03:09.003Z", "LockID": "68895DD075A1444FAD4C00B273EEEF28", "HouseID": "a550ffff-a111-4e70-b18f-a70121223b92", "HouseName": "Zuhause", "Calibrated": false, "timeZone": "Europe/Berlin", "battery": 0.61, "batteryInfo": { "level": 0.61, "warningState": "lock_state_battery_warning_none", "infoUpdatedDate": "2024-04-30T17:55:09.045Z", "lastChangeDate": "2024-03-15T07:04:00.000Z", "lastChangeVoltage": 8350, "state": "Mittel", "icon": "https://app-resources.aaecosystem.com/images/lock_battery_state_medium.png" }, "hostHardwareID": "317d4333-7192-4445-afc9-b167752a5555", "supportsEntryCodes": true, "remoteOperateSecret": "6222da48eeeebb93491a8ad77cdee490", "skuNumber": "NONE", "macAddress": "DE:AD:BE:EF:0B:BC", "SerialNumber": "LPOCH0004Y", "LockStatus": { "status": "locked", "dateTime": "2024-04-30T18:41:25.673Z", "isLockStatusChanged": false, "valid": true, "doorState": "init" }, "currentFirmwareVersion": "1.0.4", "homeKitEnabled": false, "zWaveEnabled": false, "isGalileo": false, "Bridge": { "_id": "65f33445529187c78a1aa179", "mfgBridgeID": "LPOCH0004Y", "deviceModel": "august-lock", "firmwareVersion": "1.0.4", "operative": true, "status": { "current": "online", "lastOnline": "2024-04-30T18:41:27.971Z", "updated": "2024-04-30T18:41:27.971Z", "lastOffline": "2024-04-25T14:41:40.118Z" }, "locks": [ { "_id": "656858c182e6c7c555faf758", "LockID": "68895DD075A1444FAD4C00B273EEEF28", "macAddress": "DE:AD:BE:EF:0B:BC" } ], "hyperBridge": true }, "OfflineKeys": { "created": [], "loaded": [ { "created": "2024-03-14T18:03:09.034Z", "key": "055281d4aa9bd7b68c7b7bb78e2f34ca", "slot": 1, "UserID": "b4b44424-8802-4765-a073-25c224dad337", "loaded": "2024-03-14T18:03:33.470Z" } ], "deleted": [] }, "parametersToSet": {}, "users": { "b4b44424-8802-4765-a073-25c224dad337": { "UserType": "superuser", "FirstName": "m10x", "LastName": "m10x", "identifiers": ["phone:+494444444", "email:m10x@example.com"] } }, "pubsubChannel": "01884448-bc19-45fd-85b1-7e2f6074722b", "ruleHash": {}, "cameras": [], "geofenceLimits": { "ios": { "debounceInterval": 90, "gpsAccuracyMultiplier": 2.5, "maximumGeofence": 5000, "minimumGeofence": 100, "minGPSAccuracyRequired": 80 } }, "accessSchedulesAllowed": true } yalexs-8.0.2/tests/fixtures/lock_without_doorstate.json000066400000000000000000000011011465672432400235410ustar00rootroot00000000000000{ "resultsFromOperationCache": false, "retryCount": 1, "info": { "lockType": "lock_version_3", "lockID": "ABC123", "lockStatusChanged": true, "rssi": -87, "wlanRSSI": -42, "context": { "startDate": "2020-02-19T19:44:54.370Z", "transactionID": "transid", "retryCount": 1 }, "serialNumber": "serial", "action": "lock", "wlanSNR": 56, "duration": 3119, "startTime": "2020-02-19T19:44:54.371Z", "serial": "serial", "bridgeID": "brdigeid" }, "status": "kAugLockState_Locked", "totalTime": 3133 } yalexs-8.0.2/tests/fixtures/manual_lock_activity.json000066400000000000000000000005021465672432400231470ustar00rootroot00000000000000{ "id": "9fe32136-08b2-48b3-93cc-63a88c1c8236", "timestamp": 1665527802000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_lock@3x.png", "action": "manual_lock", "deviceID": "3ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "title": "Front Door locked manually" } yalexs-8.0.2/tests/fixtures/manual_unlatch_activity.json000066400000000000000000000005041465672432400236570ustar00rootroot00000000000000{ "id": "21a911fb-e1aa-4d42-8751-390f4876f3d0", "timestamp": 1710573735000, "icon": "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/unlatch@3x.png", "action": "manual_unlatch", "deviceID": "3ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "title": "Front Door unlatched manually" } yalexs-8.0.2/tests/fixtures/manual_unlock_activity.json000066400000000000000000000005101465672432400235110ustar00rootroot00000000000000{ "id": "83618895-d2d2-44b4-b210-272798906fea", "timestamp": 1665527773000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_unlock@3x.png", "action": "manual_unlock", "deviceID": "3ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "title": "Front Door unlocked manually" } yalexs-8.0.2/tests/fixtures/pin_unlock_activity.json000066400000000000000000000007311465672432400230270ustar00rootroot00000000000000{ "id": "e05b1c22-71d1-44b8-8a4c-e729174a8972", "timestamp": 1665378377000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/pin_unlock@3x.png", "action": "pin_unlock", "deviceID": "3ED27C5DB830439CAA4F381DC8F16444", "deviceType": "lock", "user": { "UserID": "xxxsasdd-230b-4c6a-8a5c-155043492f3b", "FirstName": "Sample", "LastName": "Person" }, "title": "Sample Person unlocked Front Door with entry code" } yalexs-8.0.2/tests/fixtures/pin_unlock_activity_missing_image.json000066400000000000000000000006501465672432400257220ustar00rootroot00000000000000{ "id": "2c57a32e-708f-4f2e-81ad-c92dd4eb1b6a", "timestamp": 1665465005000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/pin_unlock@3x.png", "action": "pin_unlock", "deviceID": "3ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "user": { "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", "FirstName": "Zip", "LastName": "Zoo" }, "title": "user with entry code" } yalexs-8.0.2/tests/fixtures/pin_unlock_activity_with_image.json000066400000000000000000000007601465672432400252260ustar00rootroot00000000000000{ "id": "2c57a32e-708f-4f2e-81ad-c92dd4eb1b6a", "timestamp": 1665465005000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/pin_unlock@3x.png", "action": "pin_unlock", "deviceID": "3ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "user": { "UserID": "sddsdssds-5211-4fb9-810f-c67a4d68303b", "FirstName": "Zip", "LastName": "Zoo", "thumbnail": "https://d33mytkkohwnk6.cloudfront.net/user/abc.jpg" }, "title": "user with entry code" } yalexs-8.0.2/tests/fixtures/remote_lock_activity.json000066400000000000000000000013171465672432400231720ustar00rootroot00000000000000{ "entities": { "device": "deviceid", "callingUser": "callinguser", "otherUser": "deleted", "house": "houseid", "activity": "activityid" }, "house": { "houseID": "house", "houseName": "namehouse" }, "source": { "sourceType": "mercury" }, "eventID": "eventid", "dateTime": 1583535312002, "action": "lock", "deviceName": "lockname", "deviceID": "deviceid", "deviceType": "lock", "callingUser": { "UserID": "userid", "FirstName": "My", "LastName": "Name" }, "otherUser": { "UserID": "deleted", "FirstName": "Unknown", "LastName": "User", "UserName": "deleteduser", "PhoneNo": "deleted" }, "info": { "remote": true } } yalexs-8.0.2/tests/fixtures/remote_lock_activity_v4.json000066400000000000000000000007021465672432400236000ustar00rootroot00000000000000{ "id": "b4e50f6c-730a-4ae1-991e-e5231d727c11", "timestamp": 1665388800273, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/remote_lock@3x.png", "action": "remote_lock", "deviceID": "102A6A4F31584849971D11807272E7A8", "deviceType": "lock", "user": { "UserID": "83f02a1d-c08a-4c3d-9c30-3b66e185c7bd", "FirstName": "89", "LastName": "House" }, "title": "89 House locked Gate remotely" } yalexs-8.0.2/tests/fixtures/remote_unlatch_activity_v4.json000066400000000000000000000007211465672432400243070ustar00rootroot00000000000000{ "id": "3184ccab-d22e-4bd4-8029-6f4ca0518eaf", "timestamp": 1710590181502, "icon": "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/remote_unlatch@3x.png", "action": "remote_unlatch", "deviceID": "3ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "user": { "UserID": "83f02a1d-c08a-4c3d-9c30-3b66e185c7bd", "FirstName": "89", "LastName": "House" }, "title": "89 House unlatched Front Door remotely" } yalexs-8.0.2/tests/fixtures/remote_unlock_activity_v4.json000066400000000000000000000007101465672432400241420ustar00rootroot00000000000000{ "id": "f291b38f-33c4-4462-adad-92f814ea3a72", "timestamp": 1665418845243, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/remote_unlock@3x.png", "action": "remote_unlock", "deviceID": "102A6A4F31584849971D11807272E7A8", "deviceType": "lock", "user": { "UserID": "83f02a1d-c08a-4c3d-9c30-3b66e185c7bd", "FirstName": "89", "LastName": "House" }, "title": "89 House unlocked Gate remotely" } yalexs-8.0.2/tests/fixtures/remote_unlock_activity_v4_2.json000066400000000000000000000011001465672432400243550ustar00rootroot00000000000000{ "id": "bcb7055e-01e2-4591-aff8-3293863dac4e", "timestamp": 1665529103770, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/remote_unlock@3x.png", "action": "remote_unlock", "deviceID": "2ED27C5DB8304395AA4F381DC8F167BB", "deviceType": "lock", "user": { "UserID": "a551bc9a-5211-4fb9-810f-c67a4d68303b", "FirstName": "Zipper", "LastName": "Zoomer", "thumbnail": "https://d33mytkkohwnk6.cloudfront.net/user/a45daa08-f4b0-4251-aacd-7bf5475851e5.jpg" }, "title": "Zipper Zoomer unlocked Front Door remotely" } yalexs-8.0.2/tests/fixtures/rf_unlock_activity_v4.json000066400000000000000000000007011465672432400232560ustar00rootroot00000000000000{ "id": "b4e50f6c-730a-4ae1-991e-e5231d727c11", "timestamp": 1665388800273, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/rf_unlock@3x.png", "action": "rf_unlock", "deviceID": "102A6A4F31584849971D11807272E7A8", "deviceType": "lock", "user": { "UserID": "83f02a1d-c08a-4c3d-9c30-3b66e185c7bd", "FirstName": "89", "LastName": "House" }, "title": "89 House unlocked Gate rf_unlock" } yalexs-8.0.2/tests/fixtures/unlatch.json000066400000000000000000000011561465672432400204120ustar00rootroot00000000000000{ "status": "kAugLockState_Unlatched", "info": { "action": "unlatch", "startTime": "2024-03-20T06:39:42.192Z", "context": { "transactionID": "transid", "startDate": "2024-03-20T06:39:42.189Z", "retryCount": 1 }, "lockType": "lock_version_17", "serialNumber": "serialnum", "rssi": 0, "wlanRSSI": -37, "wlanSNR": -1, "duration": 1694, "lockID": "ABC123", "bridgeID": "bridgeid", "lockStatusChanged": true, "serial": "serial" }, "doorState": "kAugDoorState_Open", "retryCount": 1, "totalTime": 1720, "resultsFromOperationCache": false } yalexs-8.0.2/tests/fixtures/unlatch_without_doorstate.json000066400000000000000000000011561465672432400242610ustar00rootroot00000000000000{ "status": "kAugLockState_Unlatched", "info": { "action": "unlatch", "startTime": "2024-03-20T06:39:42.192Z", "context": { "transactionID": "transid", "startDate": "2024-03-20T06:39:42.189Z", "retryCount": 1 }, "lockType": "lock_version_17", "serialNumber": "serialnum", "rssi": 0, "wlanRSSI": -37, "wlanSNR": -1, "duration": 1694, "lockID": "ABC123", "bridgeID": "bridgeid", "lockStatusChanged": true, "serial": "serial" }, "doorState": "kAugDoorState_Init", "retryCount": 1, "totalTime": 1720, "resultsFromOperationCache": false } yalexs-8.0.2/tests/fixtures/unlock.json000066400000000000000000000011541465672432400202450ustar00rootroot00000000000000{ "resultsFromOperationCache": false, "info": { "bridgeID": "bridgeid", "duration": 3773, "lockStatusChanged": true, "serial": "serial", "startTime": "2020-02-19T19:44:26.745Z", "lockID": "ABC", "context": { "transactionID": "transid", "retryCount": 1, "startDate": "2020-02-19T19:44:26.744Z" }, "lockType": "lock_version_3", "serialNumber": "serialnum", "wlanRSSI": -41, "action": "unlock", "rssi": -88, "wlanSNR": 58 }, "status": "kAugLockState_Unlocked", "totalTime": 3784, "retryCount": 1, "doorState": "kAugDoorState_Closed" } yalexs-8.0.2/tests/fixtures/unlock_activity.json000066400000000000000000000012601465672432400221570ustar00rootroot00000000000000{ "action": "unlock", "callingUser": { "FirstName": "MockHouse", "LastName": "House", "UserID": "mockUserId2" }, "dateTime": 1582007217000, "deviceID": "ABC", "deviceName": "MockHouseXDoor", "deviceType": "lock", "entities": { "activity": "ActivityId", "callingUser": "mockUserId2", "device": "ABC", "house": "123", "otherUser": "deleted" }, "house": { "houseID": "123", "houseName": "MockHouse" }, "info": { "DateLogActionID": "ABC+Time", "remote": true }, "otherUser": { "FirstName": "Unknown", "LastName": "User", "PhoneNo": "deleted", "UserID": "deleted", "UserName": "deleteduser" } } yalexs-8.0.2/tests/fixtures/unlock_without_doorstate.json000066400000000000000000000011101465672432400241040ustar00rootroot00000000000000{ "resultsFromOperationCache": false, "info": { "bridgeID": "bridgeid", "duration": 3773, "lockStatusChanged": true, "serial": "serial", "startTime": "2020-02-19T19:44:26.745Z", "lockID": "ABC123", "context": { "transactionID": "transid", "retryCount": 1, "startDate": "2020-02-19T19:44:26.744Z" }, "lockType": "lock_version_3", "serialNumber": "serialnum", "wlanRSSI": -41, "action": "unlock", "rssi": -88, "wlanSNR": 58 }, "status": "kAugLockState_Unlocked", "totalTime": 3784, "retryCount": 1 } yalexs-8.0.2/tests/test_activity.py000066400000000000000000000527231465672432400174630ustar00rootroot00000000000000import json import os import unittest import aiounittest from aiohttp import ClientSession from aioresponses import aioresponses from yalexs.activity import ( ACTION_BRIDGE_OFFLINE, ACTION_BRIDGE_ONLINE, ACTION_DOOR_CLOSE_2, ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN, ACTION_DOOR_OPEN_2, ACTION_DOORBELL_BUTTON_PUSHED, ACTION_DOORBELL_CALL_HANGUP, ACTION_DOORBELL_CALL_INITIATED, ACTION_DOORBELL_CALL_MISSED, ACTION_DOORBELL_IMAGE_CAPTURE, ACTION_DOORBELL_MOTION_DETECTED, ACTION_HOMEKEY_LOCK, ACTION_HOMEKEY_UNLATCH, ACTION_HOMEKEY_UNLOCK, ACTION_LOCK_AUTO_LOCK, ACTION_LOCK_BLE_LOCK, ACTION_LOCK_BLE_UNLATCH, ACTION_LOCK_BLE_UNLOCK, ACTION_LOCK_DOORBELL_BUTTON_PUSHED, ACTION_LOCK_JAMMED, ACTION_LOCK_LOCK, ACTION_LOCK_LOCKING, ACTION_LOCK_MANUAL_LOCK, ACTION_LOCK_MANUAL_UNLATCH, ACTION_LOCK_MANUAL_UNLOCK, ACTION_LOCK_ONETOUCHLOCK, ACTION_LOCK_ONETOUCHLOCK_2, ACTION_LOCK_PIN_UNLATCH, ACTION_LOCK_PIN_UNLOCK, ACTION_LOCK_REMOTE_LOCK, ACTION_LOCK_REMOTE_UNLATCH, ACTION_LOCK_REMOTE_UNLOCK, ACTION_LOCK_UNLATCH, ACTION_LOCK_UNLATCHING, ACTION_LOCK_UNLOCK, ACTION_LOCK_UNLOCKING, ACTION_RF_LOCK, ACTION_RF_SECURE, ACTION_RF_UNLATCH, ACTION_RF_UNLOCK, ACTIVITY_ACTION_STATES, ACTIVITY_ACTIONS_BRIDGE_OPERATION, ACTIVITY_ACTIONS_DOOR_OPERATION, ACTIVITY_ACTIONS_DOORBELL_DING, ACTIVITY_ACTIONS_DOORBELL_IMAGE_CAPTURE, ACTIVITY_ACTIONS_DOORBELL_MOTION, ACTIVITY_ACTIONS_DOORBELL_VIEW, ACTIVITY_ACTIONS_LOCK_OPERATION, SOURCE_LOG, SOURCE_WEBSOCKET, ActivityType, DoorbellDingActivity, LockOperationActivity, ) from yalexs.api_async import ApiAsync from yalexs.api_common import API_GET_LOCK_URL, ApiCommon from yalexs.const import DEFAULT_BRAND from yalexs.lock import LockDoorStatus, LockStatus ACCESS_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path) as fptr: return fptr.read() class TestActivity(unittest.TestCase): def test_activity_action_states(self): self.assertIs( ACTIVITY_ACTION_STATES[ACTION_LOCK_ONETOUCHLOCK], LockStatus.LOCKED ) self.assertIs(ACTIVITY_ACTION_STATES[ACTION_LOCK_LOCK], LockStatus.LOCKED) self.assertIs(ACTIVITY_ACTION_STATES[ACTION_LOCK_UNLATCH], LockStatus.UNLATCHED) self.assertIs(ACTIVITY_ACTION_STATES[ACTION_LOCK_UNLOCK], LockStatus.UNLOCKED) self.assertIs(ACTIVITY_ACTION_STATES[ACTION_DOOR_CLOSED], LockDoorStatus.CLOSED) self.assertIs(ACTIVITY_ACTION_STATES[ACTION_DOOR_OPEN], LockDoorStatus.OPEN) def test_activity_actions(self): self.assertCountEqual( ACTIVITY_ACTIONS_DOORBELL_DING, [ ACTION_DOORBELL_BUTTON_PUSHED, ACTION_DOORBELL_CALL_MISSED, ACTION_DOORBELL_CALL_HANGUP, ACTION_LOCK_DOORBELL_BUTTON_PUSHED, ], ) self.assertCountEqual( ACTIVITY_ACTIONS_DOORBELL_MOTION, [ACTION_DOORBELL_MOTION_DETECTED], ) self.assertCountEqual( ACTIVITY_ACTIONS_DOORBELL_IMAGE_CAPTURE, [ACTION_DOORBELL_IMAGE_CAPTURE], ) self.assertCountEqual( ACTIVITY_ACTIONS_DOORBELL_VIEW, [ACTION_DOORBELL_CALL_INITIATED] ) self.assertCountEqual( ACTIVITY_ACTIONS_LOCK_OPERATION, [ ACTION_RF_SECURE, ACTION_RF_LOCK, ACTION_RF_UNLATCH, ACTION_RF_UNLOCK, ACTION_LOCK_AUTO_LOCK, ACTION_LOCK_ONETOUCHLOCK, ACTION_LOCK_ONETOUCHLOCK_2, ACTION_LOCK_LOCK, ACTION_LOCK_UNLATCH, ACTION_LOCK_UNLOCK, ACTION_LOCK_LOCKING, ACTION_LOCK_UNLATCHING, ACTION_LOCK_UNLOCKING, ACTION_HOMEKEY_LOCK, ACTION_HOMEKEY_UNLATCH, ACTION_HOMEKEY_UNLOCK, ACTION_LOCK_JAMMED, ACTION_LOCK_BLE_LOCK, ACTION_LOCK_BLE_UNLATCH, ACTION_LOCK_BLE_UNLOCK, ACTION_LOCK_REMOTE_LOCK, ACTION_LOCK_REMOTE_UNLATCH, ACTION_LOCK_REMOTE_UNLOCK, ACTION_LOCK_PIN_UNLATCH, ACTION_LOCK_PIN_UNLOCK, ACTION_LOCK_MANUAL_LOCK, ACTION_LOCK_MANUAL_UNLATCH, ACTION_LOCK_MANUAL_UNLOCK, ], ) self.assertCountEqual( ACTIVITY_ACTIONS_DOOR_OPERATION, [ ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN, ACTION_DOOR_OPEN_2, ACTION_DOOR_CLOSE_2, ], ) self.assertCountEqual( ACTIVITY_ACTIONS_BRIDGE_OPERATION, [ACTION_BRIDGE_ONLINE, ACTION_BRIDGE_OFFLINE], ) def test_auto_unlock_activity(self): auto_unlock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("auto_unlock_activity.json")) ) assert auto_unlock_activity.activity_type == ActivityType.LOCK_OPERATION assert auto_unlock_activity.operated_by == "My Name" assert auto_unlock_activity.operated_remote is False assert auto_unlock_activity.operated_keypad is False assert auto_unlock_activity.operated_manual is False assert auto_unlock_activity.operated_tag is False def test_bluetooth_lock_activity(self): bluetooth_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("bluetooth_lock_activity.json")) ) assert bluetooth_lock_activity.operated_by == "I have a picture" assert bluetooth_lock_activity.operated_remote is False assert bluetooth_lock_activity.operated_keypad is False assert bluetooth_lock_activity.operated_manual is False assert bluetooth_lock_activity.operated_tag is False assert bluetooth_lock_activity.operator_image_url == "https://image.url" assert bluetooth_lock_activity.operator_thumbnail_url == "https://thumbnail.url" def test_keypad_lock_activity(self): keypad_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("keypad_lock_activity.json")) ) assert keypad_lock_activity.operated_by == "My Name" assert keypad_lock_activity.operated_remote is False assert keypad_lock_activity.operated_keypad is True assert keypad_lock_activity.operated_manual is False assert keypad_lock_activity.operated_tag is False assert keypad_lock_activity.operator_image_url is None assert keypad_lock_activity.operator_thumbnail_url is None def test_auto_lock_activity(self): auto_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("auto_lock_activity.json")) ) assert auto_lock_activity.operated_by == "Auto Lock" assert auto_lock_activity.operated_remote is False assert auto_lock_activity.operated_keypad is False assert auto_lock_activity.operated_manual is False assert auto_lock_activity.operated_tag is False assert ( auto_lock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/auto_lock@3x.png" ) assert ( auto_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/auto_lock@3x.png" ) def test_pin_unlock_activity(self): keypad_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("pin_unlock_activity.json")) ) assert keypad_lock_activity.operated_by == "Sample Person" assert keypad_lock_activity.operated_remote is False assert keypad_lock_activity.operated_keypad is True assert keypad_lock_activity.operated_manual is False assert keypad_lock_activity.operated_tag is False assert ( keypad_lock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/pin_unlock@3x.png" ) assert ( keypad_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/pin_unlock@3x.png" ) def test_pin_unlock_activity_with_image(self): keypad_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("pin_unlock_activity_with_image.json")) ) assert keypad_lock_activity.operated_by == "Zip Zoo" assert keypad_lock_activity.operated_remote is False assert keypad_lock_activity.operated_keypad is True assert keypad_lock_activity.operated_manual is False assert keypad_lock_activity.operated_tag is False assert ( keypad_lock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/user/abc.jpg" ) assert ( keypad_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/user/abc.jpg" ) def test_remote_lock_activity(self): remote_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("remote_lock_activity.json")) ) assert remote_lock_activity.operated_by == "My Name" assert remote_lock_activity.operated_remote is True assert remote_lock_activity.operated_keypad is False assert remote_lock_activity.operated_manual is False assert remote_lock_activity.operated_tag is False def test_remote_lock_activity_v4(self): remote_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("remote_lock_activity_v4.json")) ) assert remote_lock_activity.operated_by == "89 House" assert remote_lock_activity.operated_remote is True assert remote_lock_activity.operated_keypad is False assert remote_lock_activity.operated_manual is False assert remote_lock_activity.operated_tag is False assert ( remote_lock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/remote_lock@3x.png" ) assert ( remote_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/remote_lock@3x.png" ) def test_remote_unlatch_activity_v4(self): remote_unlatch_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("remote_unlatch_activity_v4.json")) ) assert remote_unlatch_activity.activity_type == ActivityType.LOCK_OPERATION assert remote_unlatch_activity.operated_by == "89 House" assert remote_unlatch_activity.operated_remote is True assert remote_unlatch_activity.operated_keypad is False assert remote_unlatch_activity.operated_manual is False assert remote_unlatch_activity.operated_tag is False assert ( remote_unlatch_activity.operator_image_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/remote_unlatch@3x.png" ) assert ( remote_unlatch_activity.operator_thumbnail_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/remote_unlatch@3x.png" ) def test_remote_unlock_activity_v4(self): remote_unlock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("remote_unlock_activity_v4.json")) ) assert remote_unlock_activity.activity_type == ActivityType.LOCK_OPERATION assert remote_unlock_activity.operated_by == "89 House" assert remote_unlock_activity.operated_remote is True assert remote_unlock_activity.operated_keypad is False assert remote_unlock_activity.operated_manual is False assert remote_unlock_activity.operated_tag is False assert ( remote_unlock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/remote_unlock@3x.png" ) assert ( remote_unlock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/remote_unlock@3x.png" ) def test_remote_unlock_activity_v4_2(self): remote_unlock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("remote_unlock_activity_v4_2.json")) ) assert remote_unlock_activity.activity_type == ActivityType.LOCK_OPERATION assert remote_unlock_activity.operated_by == "Zipper Zoomer" assert remote_unlock_activity.operated_remote is True assert remote_unlock_activity.operated_keypad is False assert remote_unlock_activity.operated_manual is False assert remote_unlock_activity.operated_tag is False assert ( remote_unlock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/user/a45daa08-f4b0-4251-aacd-7bf5475851e5.jpg" ) assert ( remote_unlock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/user/a45daa08-f4b0-4251-aacd-7bf5475851e5.jpg" ) def test_manual_lock_activity_v4(self): manual_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("manual_lock_activity.json")) ) assert manual_lock_activity.operated_by == "Manual Lock" assert manual_lock_activity.operated_remote is False assert manual_lock_activity.operated_keypad is False assert manual_lock_activity.operated_manual is True assert manual_lock_activity.operated_tag is False assert ( manual_lock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_lock@3x.png" ) assert ( manual_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_lock@3x.png" ) def test_manual_unlatch_activity_v4(self): manual_unlatch_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("manual_unlatch_activity.json")) ) assert manual_unlatch_activity.operated_by == "Manual Unlatch" assert manual_unlatch_activity.operated_remote is False assert manual_unlatch_activity.operated_keypad is False assert manual_unlatch_activity.operated_manual is True assert manual_unlatch_activity.operated_tag is False assert ( manual_unlatch_activity.operator_image_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/unlatch@3x.png" ) assert ( manual_unlatch_activity.operator_thumbnail_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/unlatch@3x.png" ) def test_manual_unlock_activity_v4(self): manual_unlock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("manual_unlock_activity.json")) ) assert manual_unlock_activity.operated_by == "Manual Unlock" assert manual_unlock_activity.operated_remote is False assert manual_unlock_activity.operated_keypad is False assert manual_unlock_activity.operated_manual is True assert manual_unlock_activity.operated_tag is False assert ( manual_unlock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_unlock@3x.png" ) assert ( manual_unlock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_unlock@3x.png" ) def test_rf_unlock_activity_v4(self): rf_unlock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("rf_unlock_activity_v4.json")) ) assert rf_unlock_activity.operated_by == "89 House" assert rf_unlock_activity.operated_remote is False assert rf_unlock_activity.operated_keypad is False assert rf_unlock_activity.operated_manual is False assert rf_unlock_activity.operated_tag is True assert ( rf_unlock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/rf_unlock@3x.png" ) assert ( rf_unlock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/rf_unlock@3x.png" ) def test_homekey_unlock_activity_v4(self): homekey_unlock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("homekey_unlock_activity_v4.json")) ) assert homekey_unlock_activity.operated_by == "89 House" assert homekey_unlock_activity.operated_remote is False assert homekey_unlock_activity.operated_keypad is False assert homekey_unlock_activity.operated_manual is False assert homekey_unlock_activity.operated_tag is True assert ( homekey_unlock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/homekey_unlock@3x.png" ) assert ( homekey_unlock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/homekey_unlock@3x.png" ) def test_lock_activity(self): lock_operation_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("lock_activity.json")) ) assert lock_operation_activity.operated_by == "MockHouse House" assert lock_operation_activity.operated_remote is True assert lock_operation_activity.operated_keypad is False assert lock_operation_activity.operated_autorelock is False def test_unlock_activity(self): unlock_operation_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("unlock_activity.json")) ) assert unlock_operation_activity.operated_by == "MockHouse House" assert unlock_operation_activity.operated_keypad is False assert unlock_operation_activity.operated_remote is True assert unlock_operation_activity.operator_image_url is None assert unlock_operation_activity.operated_autorelock is False assert unlock_operation_activity.operator_thumbnail_url is None def test_autorelock_activity(self): auto_relock_operation_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("auto_relock_activity.json")) ) assert auto_relock_operation_activity.operated_by == "I have no picture" assert auto_relock_operation_activity.operated_remote is False assert auto_relock_operation_activity.operated_autorelock is True assert auto_relock_operation_activity.operated_keypad is False def test_get_lock_button_pressed(self): doorbell_ding_activity = DoorbellDingActivity( SOURCE_LOG, json.loads(load_fixture("lock_accessory_motion_detect.json")) ) assert doorbell_ding_activity.activity_start_time.timestamp() == 1691249378.0 assert doorbell_ding_activity.activity_end_time.timestamp() == 1691249378.0 class TestActivityApiAsync(aiounittest.AsyncTestCase): @aioresponses() async def test_async_get_lock_detail_bridge_online(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="A6697750D607098BAE8D6BAA11EF8063"), body=load_fixture("get_lock.online.json"), ) api = ApiAsync(ClientSession()) await api.async_get_lock_detail( ACCESS_TOKEN, "A6697750D607098BAE8D6BAA11EF8063" ) keypad_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("pin_unlock_activity_missing_image.json")), ) assert keypad_lock_activity.operated_by == "Zip Zoo" assert keypad_lock_activity.operated_remote is False assert keypad_lock_activity.operated_keypad is True assert ( keypad_lock_activity.operator_image_url == "https://www.image.com/foo.jpeg" ) assert ( keypad_lock_activity.operator_thumbnail_url == "https://www.image.com/foo.jpeg" ) def test_websocket_activity(self): manual_unlock_activity = LockOperationActivity( SOURCE_WEBSOCKET, json.loads(load_fixture("manual_unlock_activity.json")) ) assert manual_unlock_activity.operated_by == "Manual Unlock" assert manual_unlock_activity.operated_remote is False assert manual_unlock_activity.operated_keypad is False assert manual_unlock_activity.operated_manual is True assert manual_unlock_activity.operated_tag is False assert ( manual_unlock_activity.operator_image_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_unlock@3x.png" ) assert ( manual_unlock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/manual_unlock@3x.png" ) assert manual_unlock_activity.source == SOURCE_WEBSOCKET assert manual_unlock_activity.was_pushed is True yalexs-8.0.2/tests/test_api_async.py000066400000000000000000001373061465672432400175760ustar00rootroot00000000000000import os from datetime import datetime from unittest import mock from unittest.mock import patch import aiounittest import dateutil.parser import pytest from aiohttp import ClientOSError, ClientResponse, ClientSession from aiohttp.helpers import TimerNoop from aioresponses import CallbackResult, aioresponses from dateutil.tz import tzlocal, tzutc from yarl import URL import yalexs.activity from yalexs import api_async from yalexs.api_async import ApiAsync, _raise_response_exceptions from yalexs.api_common import ( API_GET_DOORBELL_URL, API_GET_DOORBELLS_URL, API_GET_HOUSE_ACTIVITIES_URL, API_GET_HOUSES_URL, API_GET_LOCK_STATUS_URL, API_GET_LOCK_URL, API_GET_LOCKS_URL, API_GET_PINS_URL, API_GET_USER_URL, API_LOCK_ASYNC_URL, API_LOCK_URL, API_STATUS_ASYNC_URL, API_UNLATCH_ASYNC_URL, API_UNLATCH_URL, API_UNLOCK_ASYNC_URL, API_UNLOCK_URL, API_VALIDATE_VERIFICATION_CODE_URLS, HYPER_BRIDGE_PARAM, ApiCommon, ) from yalexs.bridge import BridgeDetail, BridgeStatus, BridgeStatusDetail from yalexs.const import DEFAULT_BRAND, Brand from yalexs.exceptions import AugustApiAIOHTTPError, ContentTokenExpired from yalexs.lock import LockDoorStatus, LockStatus ACCESS_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path) as fptr: return fptr.read() def utc_of(year, month, day, hour, minute, second, microsecond): return datetime(year, month, day, hour, minute, second, microsecond, tzinfo=tzutc()) class TestApiAsync(aiounittest.AsyncTestCase): @aioresponses() async def test_async_get_doorbells(self, mock): mock.get( ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_DOORBELLS_URL), body=load_fixture("get_doorbells.json"), ) api = ApiAsync(ClientSession()) doorbells = sorted( await api.async_get_doorbells(ACCESS_TOKEN), key=lambda d: d.device_id ) self.assertEqual(2, len(doorbells)) first = doorbells[0] self.assertEqual("1KDAbJH89XYZ", first.device_id) self.assertEqual("aaaaR08888", first.serial_number) self.assertEqual("Back Door", first.device_name) self.assertEqual("doorbell_call_status_offline", first.status) self.assertEqual(False, first.has_subscription) self.assertEqual(None, first.image_url) self.assertEqual("3dd2accadddd", first.house_id) second = doorbells[1] self.assertEqual("K98GiDT45GUL", second.device_id) self.assertEqual("tBXZR0Z35E", second.serial_number) self.assertEqual("Front Door", second.device_name) self.assertEqual("doorbell_call_status_online", second.status) self.assertEqual(True, second.has_subscription) self.assertEqual("https://image.com/vmk16naaaa7ibuey7sar.jpg", second.image_url) self.assertEqual("3dd2accaea08", second.house_id) @aioresponses() async def test_async_get_doorbell_detail(self, mock): expected_doorbell_image_url = "https://image.com/vmk16naaaa7ibuey7sar.jpg" mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_DOORBELL_URL) .format(doorbell_id="K98GiDT45GUL"), body=load_fixture("get_doorbell.json"), ) mock.get(expected_doorbell_image_url, body="doorbell_image_mocked") api = ApiAsync(ClientSession()) doorbell = await api.async_get_doorbell_detail(ACCESS_TOKEN, "K98GiDT45GUL") self.assertEqual("K98GiDT45GUL", doorbell.device_id) self.assertEqual("Front Door", doorbell.device_name) self.assertEqual("3dd2accaea08", doorbell.house_id) self.assertEqual("tBXZR0Z35E", doorbell.serial_number) self.assertEqual("2.3.0-RC153+201711151527", doorbell.firmware_version) self.assertEqual("doorbell_call_status_online", doorbell.status) self.assertEqual(96, doorbell.battery_level) self.assertEqual("gen1", doorbell.model) self.assertEqual(True, doorbell.is_online) self.assertEqual(False, doorbell.is_standby) self.assertEqual( dateutil.parser.parse("2017-12-10T08:01:35Z"), doorbell.image_created_at_datetime, ) self.assertEqual(True, doorbell.has_subscription) self.assertEqual(expected_doorbell_image_url, doorbell.image_url) self.assertEqual( await doorbell.async_get_doorbell_image(ClientSession()), b"doorbell_image_mocked", ) @aioresponses() async def test_async_get_doorbell_image_token_expired(self, mock): expected_doorbell_image_url = "https://image.com/vmk16naaaa7ibuey7sar.jpg" mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_DOORBELL_URL) .format(doorbell_id="K98GiDT45GUL"), body=load_fixture("get_doorbell.json"), ) mock.get(expected_doorbell_image_url, status=401) api = ApiAsync(ClientSession()) doorbell = await api.async_get_doorbell_detail(ACCESS_TOKEN, "K98GiDT45GUL") with self.assertRaises(ContentTokenExpired): await doorbell.async_get_doorbell_image(ClientSession()) @aioresponses() async def test_async_get_doorbell_detail_missing_image(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_DOORBELL_URL) .format(doorbell_id="K98GiDT45GUL"), body=load_fixture("get_doorbell_missing_image.json"), ) api = ApiAsync(ClientSession()) doorbell = await api.async_get_doorbell_detail(ACCESS_TOKEN, "K98GiDT45GUL") self.assertEqual("K98GiDT45GUL", doorbell.device_id) self.assertEqual("Front Door", doorbell.device_name) self.assertEqual("3dd2accaea08", doorbell.house_id) self.assertEqual("tBXZR0Z35E", doorbell.serial_number) self.assertEqual("2.3.0-RC153+201711151527", doorbell.firmware_version) self.assertEqual("doorbell_call_status_online", doorbell.status) self.assertEqual(96, doorbell.battery_level) self.assertEqual(True, doorbell.is_online) self.assertEqual(False, doorbell.is_standby) self.assertEqual(None, doorbell.image_created_at_datetime) self.assertEqual(True, doorbell.has_subscription) self.assertEqual(None, doorbell.image_url) @aioresponses() async def test_async_get_doorbell_offline(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_DOORBELL_URL) .format(doorbell_id="231ee2168dd0"), body=load_fixture("get_doorbell.offline.json"), ) api = ApiAsync(ClientSession()) doorbell = await api.async_get_doorbell_detail(ACCESS_TOKEN, "231ee2168dd0") self.assertEqual("231ee2168dd0", doorbell.device_id) self.assertEqual("My Door", doorbell.device_name) self.assertEqual("houseid", doorbell.house_id) self.assertEqual("abcd", doorbell.serial_number) self.assertEqual("3.1.0-HYDRC75+201909251139", doorbell.firmware_version) self.assertEqual("doorbell_offline", doorbell.status) self.assertEqual(81, doorbell.battery_level) self.assertEqual(False, doorbell.is_online) self.assertEqual(False, doorbell.is_standby) self.assertEqual( dateutil.parser.parse("2019-02-20T23:52:46Z"), doorbell.image_created_at_datetime, ) self.assertEqual(True, doorbell.has_subscription) self.assertEqual("https://res.cloudinary.com/x.jpg", doorbell.image_url) self.assertEqual("hydra1", doorbell.model) @aioresponses() async def test_async_get_doorbell_gen2_full_battery_detail(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_DOORBELL_URL) .format(doorbell_id="did"), body=load_fixture("get_doorbell.battery_full.json"), ) api = ApiAsync(ClientSession()) doorbell = await api.async_get_doorbell_detail(ACCESS_TOKEN, "did") self.assertEqual(100, doorbell.battery_level) @aioresponses() async def test_async_get_doorbell_gen2_medium_battery_detail(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_DOORBELL_URL) .format(doorbell_id="did"), body=load_fixture("get_doorbell.battery_medium.json"), ) api = ApiAsync(ClientSession()) doorbell = await api.async_get_doorbell_detail(ACCESS_TOKEN, "did") self.assertEqual(75, doorbell.battery_level) @aioresponses() async def test_async_get_doorbell_gen2_low_battery_detail(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_DOORBELL_URL) .format(doorbell_id="did"), body=load_fixture("get_doorbell.battery_low.json"), ) api = ApiAsync(ClientSession()) doorbell = await api.async_get_doorbell_detail(ACCESS_TOKEN, "did") self.assertEqual(10, doorbell.battery_level) @aioresponses() async def test_async_get_locks(self, mock): mock.get( ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_LOCKS_URL), body=load_fixture("get_locks.json"), ) api = ApiAsync(ClientSession()) locks = sorted( await api.async_get_locks(ACCESS_TOKEN), key=lambda d: d.device_id ) self.assertEqual(2, len(locks)) first = locks[0] self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", first.device_id) self.assertEqual("Front Door Lock", first.device_name) self.assertEqual("000000000000", first.house_id) self.assertEqual(True, first.is_operable) second = locks[1] self.assertEqual("A6697750D607098BAE8D6BAA11EF9999", second.device_id) self.assertEqual("Back Door Lock", second.device_name) self.assertEqual("000000000011", second.house_id) self.assertEqual(False, second.is_operable) @aioresponses() async def test_async_get_locks_yale_home_brand(self, mock): mock.get( ApiCommon(Brand.YALE_HOME).get_brand_url(API_GET_LOCKS_URL), body=load_fixture("get_locks.json"), ) api = ApiAsync(ClientSession(), brand=Brand.YALE_HOME) locks = sorted( await api.async_get_locks(ACCESS_TOKEN), key=lambda d: d.device_id ) self.assertEqual(2, len(locks)) first = locks[0] self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", first.device_id) self.assertEqual("Front Door Lock", first.device_name) self.assertEqual("000000000000", first.house_id) self.assertEqual(True, first.is_operable) second = locks[1] self.assertEqual("A6697750D607098BAE8D6BAA11EF9999", second.device_id) self.assertEqual("Back Door Lock", second.device_name) self.assertEqual("000000000011", second.house_id) self.assertEqual(False, second.is_operable) @aioresponses() async def test_async_get_operable_locks(self, mock): mock.get( ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_LOCKS_URL), body=load_fixture("get_locks.json"), ) api = ApiAsync(ClientSession()) locks = await api.async_get_operable_locks(ACCESS_TOKEN) self.assertEqual(1, len(locks)) first = locks[0] self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", first.device_id) self.assertEqual("Front Door Lock", first.device_name) self.assertEqual("000000000000", first.house_id) self.assertEqual(True, first.is_operable) @aioresponses() async def test_async_get_lock_detail_with_doorsense_bridge_online(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="ABC"), body=load_fixture("get_lock.online_with_doorsense.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail(ACCESS_TOKEN, "ABC") self.assertEqual("ABC", lock.device_id) self.assertEqual("Online door with doorsense", lock.device_name) self.assertEqual("123", lock.house_id) self.assertEqual("XY", lock.serial_number) self.assertEqual("undefined-4.3.0-1.8.14", lock.firmware_version) self.assertEqual(92, lock.battery_level) self.assertEqual("AUG-MD01", lock.model) self.assertEqual(None, lock.keypad) self.assertIsInstance(lock.bridge, BridgeDetail) self.assertIsInstance(lock.bridge.status, BridgeStatusDetail) self.assertEqual(BridgeStatus.ONLINE, lock.bridge.status.current) self.assertEqual(True, lock.bridge_is_online) self.assertEqual(True, lock.bridge.operative) self.assertEqual(True, lock.doorsense) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.OPEN, lock.door_state) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.lock_status_datetime ) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.door_state_datetime ) @aioresponses() async def test_async_get_lock_detail_with_doorsense_disabled_bridge_online( self, mock ): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="ABC"), body=load_fixture("get_lock.online_with_doorsense_disabled.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail(ACCESS_TOKEN, "ABC") self.assertEqual("ABC", lock.device_id) self.assertEqual("Online door with doorsense disabled", lock.device_name) self.assertEqual("123", lock.house_id) self.assertEqual("XY", lock.serial_number) self.assertEqual("undefined-4.3.0-1.8.14", lock.firmware_version) self.assertEqual(92, lock.battery_level) self.assertEqual("AUG-MD01", lock.model) self.assertEqual(lock.bridge.hyper_bridge, True) self.assertEqual(None, lock.keypad) self.assertIsInstance(lock.bridge, BridgeDetail) self.assertIsInstance(lock.bridge.status, BridgeStatusDetail) self.assertEqual(BridgeStatus.ONLINE, lock.bridge.status.current) self.assertEqual(True, lock.bridge_is_online) self.assertEqual(True, lock.bridge.operative) self.assertEqual(False, lock.doorsense) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.DISABLED, lock.door_state) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.lock_status_datetime ) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.door_state_datetime ) @aioresponses() async def test_async_get_lock_detail_bridge_online(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="A6697750D607098BAE8D6BAA11EF8063"), body=load_fixture("get_lock.online.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail( ACCESS_TOKEN, "A6697750D607098BAE8D6BAA11EF8063" ) assert lock.doorbell is False assert lock.unlatch_supported is False self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", lock.device_id) self.assertEqual("Front Door Lock", lock.device_name) self.assertEqual("000000000000", lock.house_id) self.assertEqual("X2FSW05DGA", lock.serial_number) self.assertEqual("109717e9-3.0.44-3.0.30", lock.firmware_version) self.assertEqual(88, lock.battery_level) self.assertEqual("AUG-SL02-M02-S02", lock.model) self.assertEqual("Medium", lock.keypad.battery_level) self.assertEqual(62, lock.keypad.battery_percentage) self.assertEqual("5bc65c24e6ef2a263e1450a8", lock.keypad.device_id) self.assertIsInstance(lock.bridge, BridgeDetail) self.assertEqual(True, lock.bridge_is_online) self.assertEqual(True, lock.bridge.operative) self.assertEqual(True, lock.doorsense) self.assertEqual(lock.bridge.hyper_bridge, False) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.CLOSED, lock.door_state) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.lock_status_datetime ) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.door_state_datetime ) @aioresponses() async def test_async_get_lock_with_doorbell(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="A6697750D607098BAE8D6BAA11EF8063"), body=load_fixture("lock_with_doorbell.online.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail( ACCESS_TOKEN, "A6697750D607098BAE8D6BAA11EF8063" ) self.assertEqual(62, lock.keypad.battery_percentage) assert lock.doorbell is True @aioresponses() async def test_async_get_lock_with_unlatch(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="68895DD075A1444FAD4C00B273EEEF28"), body=load_fixture("lock_with_unlatch.online.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail( ACCESS_TOKEN, "68895DD075A1444FAD4C00B273EEEF28" ) assert lock.unlatch_supported is True @aioresponses() async def test_async_get_v2_lock_detail_bridge_online(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="snip"), body=load_fixture("get_lock_v2.online.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail(ACCESS_TOKEN, "snip") self.assertEqual("snip", lock.device_id) self.assertEqual("Front Door", lock.device_name) self.assertEqual("snip", lock.house_id) self.assertEqual("snip", lock.serial_number) self.assertEqual("3.0.44-3.0.29", lock.firmware_version) self.assertEqual(96, lock.battery_level) self.assertEqual("AUG-SL02-M02-S02", lock.model) self.assertIsInstance(lock.bridge, BridgeDetail) self.assertEqual(True, lock.bridge_is_online) self.assertEqual(True, lock.bridge.operative) self.assertEqual(False, lock.doorsense) self.assertEqual(lock.bridge.hyper_bridge, False) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.DISABLED, lock.door_state) self.assertEqual( lock.offline_keys, { "created": [], "createdhk": [], "deleted": [], "loaded": [ { "UserID": "XXXXXX", "created": "2022-01-14T21:14:50.153Z", "key": "XXXXXX", "loaded": "2022-01-14T21:14:54.568Z", "slot": 1, } ], }, ) self.assertEqual( lock.loaded_offline_keys, [ { "UserID": "XXXXXX", "created": "2022-01-14T21:14:50.153Z", "key": "XXXXXX", "loaded": "2022-01-14T21:14:54.568Z", "slot": 1, } ], ) self.assertEqual(lock.offline_key, "XXXXXX") self.assertEqual(lock.offline_slot, 1) self.assertEqual(lock.mac_address, "SNIP") @aioresponses() async def test_async_get_lock_detail_bridge_offline(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="ABC"), body=load_fixture("get_lock.offline.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail(ACCESS_TOKEN, "ABC") self.assertEqual("ABC", lock.device_id) self.assertEqual("Test", lock.device_name) self.assertEqual("houseid", lock.house_id) self.assertEqual("ABC", lock.serial_number) self.assertEqual("undefined-1.59.0-1.13.2", lock.firmware_version) self.assertEqual(-100, lock.battery_level) self.assertEqual("AUG-X", lock.model) self.assertEqual(False, lock.bridge_is_online) self.assertEqual(None, lock.keypad) self.assertEqual(None, lock.bridge) self.assertEqual(False, lock.doorsense) self.assertEqual(LockStatus.UNKNOWN, lock.lock_status) self.assertEqual(LockDoorStatus.DISABLED, lock.door_state) self.assertEqual(None, lock.lock_status_datetime) self.assertEqual(None, lock.door_state_datetime) @aioresponses() async def test_async_get_lock_detail_doorsense_init_state(self, mock): mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_URL) .format(lock_id="A6697750D607098BAE8D6BAA11EF8063"), body=load_fixture("get_lock.doorsense_init.json"), ) api = ApiAsync(ClientSession()) lock = await api.async_get_lock_detail( ACCESS_TOKEN, "A6697750D607098BAE8D6BAA11EF8063" ) self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", lock.device_id) self.assertEqual("Front Door Lock", lock.device_name) self.assertEqual("000000000000", lock.house_id) self.assertEqual("X2FSW05DGA", lock.serial_number) self.assertEqual("109717e9-3.0.44-3.0.30", lock.firmware_version) self.assertEqual(88, lock.battery_level) self.assertEqual("Medium", lock.keypad.battery_level) self.assertEqual("5bc65c24e6ef2a263e1450a8", lock.keypad.device_id) self.assertEqual("AK-R1", lock.keypad.model) self.assertEqual("Front Door Lock Keypad", lock.keypad.device_name) self.assertIsInstance(lock.bridge, BridgeDetail) self.assertEqual(True, lock.bridge_is_online) self.assertEqual(True, lock.bridge.operative) self.assertEqual(False, lock.doorsense) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.DISABLED, lock.door_state) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.lock_status_datetime ) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.door_state_datetime ) lock.lock_status = LockStatus.UNLATCHED self.assertEqual(LockStatus.UNLATCHED, lock.lock_status) lock.lock_status = LockStatus.UNLOCKED self.assertEqual(LockStatus.UNLOCKED, lock.lock_status) lock.door_state = LockDoorStatus.OPEN self.assertEqual(LockDoorStatus.OPEN, lock.door_state) lock.lock_status_datetime = dateutil.parser.parse("2020-12-10T04:48:30.272Z") self.assertEqual( dateutil.parser.parse("2020-12-10T04:48:30.272Z"), lock.lock_status_datetime ) lock.door_state_datetime = dateutil.parser.parse("2019-12-10T04:48:30.272Z") self.assertEqual( dateutil.parser.parse("2019-12-10T04:48:30.272Z"), lock.door_state_datetime ) assert isinstance(lock.raw, dict) @aioresponses() async def test_async_get_lock_status_with_locked_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"status": "kAugLockState_Locked"}', ) api = ApiAsync(ClientSession()) status = await api.async_get_lock_status(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.LOCKED, status) @aioresponses() async def test_async_get_lock_and_door_status_with_locked_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"status": "kAugLockState_Locked"' ',"doorState": "kAugLockDoorState_Closed"}', ) api = ApiAsync(ClientSession()) status, door_status = await api.async_get_lock_status( ACCESS_TOKEN, lock_id, True ) self.assertEqual(LockStatus.LOCKED, status) self.assertEqual(LockDoorStatus.CLOSED, door_status) @aioresponses() async def test_async_get_lock_status_with_unlocked_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"status": "kAugLockState_Unlocked"}', ) api = ApiAsync(ClientSession()) status = await api.async_get_lock_status(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.UNLOCKED, status) @aioresponses() async def test_async_get_lock_status_with_unknown_status_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"status": "not_advertising"}', ) api = ApiAsync(ClientSession()) status = await api.async_get_lock_status(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.UNKNOWN, status) @aioresponses() async def test_async_get_lock_door_status_with_closed_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"doorState": "kAugLockDoorState_Closed"}', ) api = ApiAsync(ClientSession()) door_status = await api.async_get_lock_door_status(ACCESS_TOKEN, lock_id) self.assertEqual(LockDoorStatus.CLOSED, door_status) @aioresponses() async def test_async_get_lock_door_status_with_open_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"doorState": "kAugLockDoorState_Open"}', ) api = ApiAsync(ClientSession()) door_status = await api.async_get_lock_door_status(ACCESS_TOKEN, lock_id) self.assertEqual(LockDoorStatus.OPEN, door_status) @aioresponses() async def test_async_get_lock_and_door_status_with_open_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"status": "kAugLockState_Unlocked"' ',"doorState": "kAugLockDoorState_Open"}', ) api = ApiAsync(ClientSession()) door_status, status = await api.async_get_lock_door_status( ACCESS_TOKEN, lock_id, True ) self.assertEqual(LockDoorStatus.OPEN, door_status) self.assertEqual(LockStatus.UNLOCKED, status) @aioresponses() async def test_async_get_lock_door_status_with_unknown_response(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_LOCK_STATUS_URL) .format(lock_id=lock_id), body='{"doorState": "not_advertising"}', ) api = ApiAsync(ClientSession()) door_status = await api.async_get_lock_door_status(ACCESS_TOKEN, lock_id) self.assertEqual(LockDoorStatus.UNKNOWN, door_status) @aioresponses() async def test_async_lock_from_fixture(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_LOCK_URL) .format(lock_id=lock_id), body=load_fixture("lock.json"), ) api = ApiAsync(ClientSession()) status = await api.async_lock(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.LOCKED, status) @aioresponses() async def test_async_unlock_from_fixture(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLOCK_URL) .format(lock_id=lock_id), body=load_fixture("unlock.json"), ) api = ApiAsync(ClientSession()) status = await api.async_unlock(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.UNLOCKED, status) @aioresponses() async def test_async_lock_return_activities_from_fixture(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_LOCK_URL) .format(lock_id=lock_id), body=load_fixture("lock.json"), ) api = ApiAsync(ClientSession()) activities = await api.async_lock_return_activities(ACCESS_TOKEN, lock_id) expected_lock_dt = ( dateutil.parser.parse("2020-02-19T19:44:54.371Z") .astimezone(tz=tzlocal()) .replace(tzinfo=None) ) self.assertEqual(len(activities), 2) self.assertIsInstance(activities[0], yalexs.activity.LockOperationActivity) self.assertEqual(activities[0].device_id, "ABC123") self.assertEqual(activities[0].device_type, "lock") self.assertEqual(activities[0].action, "lock") self.assertEqual(activities[0].activity_start_time, expected_lock_dt) self.assertEqual(activities[0].activity_end_time, expected_lock_dt) self.assertIsInstance(activities[1], yalexs.activity.DoorOperationActivity) self.assertEqual(activities[1].device_id, "ABC123") self.assertEqual(activities[1].device_type, "lock") self.assertEqual(activities[1].action, "doorclosed") self.assertEqual(activities[0].activity_start_time, expected_lock_dt) self.assertEqual(activities[0].activity_end_time, expected_lock_dt) @aioresponses() async def test_async_unlatch_return_activities_from_fixture(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLATCH_URL) .format(lock_id=lock_id), body=load_fixture("unlatch.json"), ) api = ApiAsync(ClientSession()) activities = await api.async_unlatch_return_activities(ACCESS_TOKEN, lock_id) expected_unlatch_dt = ( dateutil.parser.parse("2024-03-20T06:39:42.192Z") .astimezone(tz=tzlocal()) .replace(tzinfo=None) ) self.assertEqual(len(activities), 2) self.assertIsInstance(activities[0], yalexs.activity.LockOperationActivity) self.assertEqual(activities[0].device_id, "ABC123") self.assertEqual(activities[0].device_type, "lock") self.assertEqual(activities[0].action, "unlatch") self.assertEqual(activities[0].activity_start_time, expected_unlatch_dt) self.assertEqual(activities[0].activity_end_time, expected_unlatch_dt) self.assertIsInstance(activities[1], yalexs.activity.DoorOperationActivity) self.assertEqual(activities[1].device_id, "ABC123") self.assertEqual(activities[1].device_type, "lock") self.assertEqual(activities[1].action, "dooropen") self.assertEqual(activities[1].activity_start_time, expected_unlatch_dt) self.assertEqual(activities[1].activity_end_time, expected_unlatch_dt) @aioresponses() async def test_async_unlock_return_activities_from_fixture(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLOCK_URL) .format(lock_id=lock_id), body=load_fixture("unlock.json"), ) api = ApiAsync(ClientSession()) activities = await api.async_unlock_return_activities(ACCESS_TOKEN, lock_id) expected_unlock_dt = ( dateutil.parser.parse("2020-02-19T19:44:26.745Z") .astimezone(tz=tzlocal()) .replace(tzinfo=None) ) self.assertEqual(len(activities), 2) self.assertIsInstance(activities[0], yalexs.activity.LockOperationActivity) self.assertEqual(activities[0].device_id, "ABC") self.assertEqual(activities[0].device_type, "lock") self.assertEqual(activities[0].action, "unlock") self.assertEqual(activities[0].activity_start_time, expected_unlock_dt) self.assertEqual(activities[0].activity_end_time, expected_unlock_dt) self.assertIsInstance(activities[1], yalexs.activity.DoorOperationActivity) self.assertEqual(activities[1].device_id, "ABC") self.assertEqual(activities[1].device_type, "lock") self.assertEqual(activities[1].action, "doorclosed") self.assertEqual(activities[1].activity_start_time, expected_unlock_dt) self.assertEqual(activities[1].activity_end_time, expected_unlock_dt) @aioresponses() async def test_async_lock_return_activities_from_fixture_with_no_doorstate( self, mock ): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_LOCK_URL) .format(lock_id=lock_id), body=load_fixture("lock_without_doorstate.json"), ) api = ApiAsync(ClientSession()) activities = await api.async_lock_return_activities(ACCESS_TOKEN, lock_id) expected_lock_dt = ( dateutil.parser.parse("2020-02-19T19:44:54.371Z") .astimezone(tz=tzlocal()) .replace(tzinfo=None) ) self.assertEqual(len(activities), 1) self.assertIsInstance(activities[0], yalexs.activity.LockOperationActivity) self.assertEqual(activities[0].device_id, "ABC123") self.assertEqual(activities[0].device_type, "lock") self.assertEqual(activities[0].action, "lock") self.assertEqual(activities[0].activity_start_time, expected_lock_dt) self.assertEqual(activities[0].activity_end_time, expected_lock_dt) @aioresponses() async def test_async_unlatch_return_activities_from_fixture_with_no_doorstate( self, mock ): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLATCH_URL) .format(lock_id=lock_id), body=load_fixture("unlatch_without_doorstate.json"), ) api = ApiAsync(ClientSession()) activities = await api.async_unlatch_return_activities(ACCESS_TOKEN, lock_id) expected_unlatch_dt = ( dateutil.parser.parse("2024-03-20T06:39:42.192Z") .astimezone(tz=tzlocal()) .replace(tzinfo=None) ) self.assertEqual(len(activities), 1) self.assertIsInstance(activities[0], yalexs.activity.LockOperationActivity) self.assertEqual(activities[0].device_id, "ABC123") self.assertEqual(activities[0].device_type, "lock") self.assertEqual(activities[0].action, "unlatch") self.assertEqual(activities[0].activity_start_time, expected_unlatch_dt) self.assertEqual(activities[0].activity_end_time, expected_unlatch_dt) @aioresponses() async def test_async_unlock_return_activities_from_fixture_with_no_doorstate( self, mock ): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLOCK_URL) .format(lock_id=lock_id), body=load_fixture("unlock_without_doorstate.json"), ) api = ApiAsync(ClientSession()) activities = await api.async_unlock_return_activities(ACCESS_TOKEN, lock_id) expected_unlock_dt = ( dateutil.parser.parse("2020-02-19T19:44:26.745Z") .astimezone(tz=tzlocal()) .replace(tzinfo=None) ) self.assertEqual(len(activities), 1) self.assertIsInstance(activities[0], yalexs.activity.LockOperationActivity) self.assertEqual(activities[0].device_id, "ABC123") self.assertEqual(activities[0].device_type, "lock") self.assertEqual(activities[0].action, "unlock") self.assertEqual(activities[0].activity_start_time, expected_unlock_dt) self.assertEqual(activities[0].activity_end_time, expected_unlock_dt) @aioresponses() async def test_async_lock(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_LOCK_URL) .format(lock_id=lock_id), body='{"status":"locked",' '"dateTime":"2017-12-10T07:43:39.056Z",' '"isLockStatusChanged":false,' '"valid":true}', ) api = ApiAsync(ClientSession()) status = await api.async_lock(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.LOCKED, status) @aioresponses() async def test_async_lock_async_old_bridge(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_LOCK_ASYNC_URL) .format(lock_id=lock_id), ) api = ApiAsync(ClientSession()) await api.async_lock_async(ACCESS_TOKEN, lock_id, hyper_bridge=False) @aioresponses() async def test_async_lock_async_new_bridge(self, mock): lock_id = 1234 base_url = ApiCommon(DEFAULT_BRAND).get_brand_url(API_LOCK_ASYNC_URL) mock.put(f"{base_url}{HYPER_BRIDGE_PARAM}".format(lock_id=lock_id)) api = ApiAsync(ClientSession()) await api.async_lock_async(ACCESS_TOKEN, lock_id) @aioresponses() async def test_async_unlatch(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLATCH_URL) .format(lock_id=lock_id), body='{"status": "unlatched"}', ) api = ApiAsync(ClientSession()) status = await api.async_unlatch(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.UNLATCHED, status) @aioresponses() async def test_async_unlatch_async_old_bridge(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLATCH_ASYNC_URL) .format(lock_id=lock_id) ) api = ApiAsync(ClientSession()) await api.async_unlatch_async(ACCESS_TOKEN, lock_id, hyper_bridge=False) @aioresponses() async def test_async_unlatch_async_new_bridge(self, mock): lock_id = 1234 base_url = ApiCommon(DEFAULT_BRAND).get_brand_url(API_UNLATCH_ASYNC_URL) mock.put(f"{base_url}{HYPER_BRIDGE_PARAM}".format(lock_id=lock_id)) api = ApiAsync(ClientSession()) await api.async_unlatch_async(ACCESS_TOKEN, lock_id, hyper_bridge=True) @aioresponses() async def test_async_unlock(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLOCK_URL) .format(lock_id=lock_id), body='{"status": "unlocked"}', ) api = ApiAsync(ClientSession()) status = await api.async_unlock(ACCESS_TOKEN, lock_id) self.assertEqual(LockStatus.UNLOCKED, status) @aioresponses() async def test_async_unlock_async_old_bridge(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_UNLOCK_ASYNC_URL) .format(lock_id=lock_id) ) api = ApiAsync(ClientSession()) await api.async_unlock_async(ACCESS_TOKEN, lock_id, hyper_bridge=False) @aioresponses() async def test_async_unlock_async_new_bridge(self, mock): lock_id = 1234 base_url = ApiCommon(DEFAULT_BRAND).get_brand_url(API_UNLOCK_ASYNC_URL) mock.put(f"{base_url}{HYPER_BRIDGE_PARAM}".format(lock_id=lock_id)) api = ApiAsync(ClientSession()) await api.async_unlock_async(ACCESS_TOKEN, lock_id, hyper_bridge=True) @aioresponses() async def test_async_status_async_old_bridge(self, mock): lock_id = 1234 mock.put( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_STATUS_ASYNC_URL) .format(lock_id=lock_id) ) api = ApiAsync(ClientSession()) await api.async_status_async(ACCESS_TOKEN, lock_id, hyper_bridge=False) @aioresponses() async def test_async_status_async_new_bridge(self, mock): lock_id = 1234 base_url = ApiCommon(DEFAULT_BRAND).get_brand_url(API_STATUS_ASYNC_URL) mock.put(f"{base_url}{HYPER_BRIDGE_PARAM}".format(lock_id=lock_id)) api = ApiAsync(ClientSession()) await api.async_status_async(ACCESS_TOKEN, lock_id) @aioresponses() async def test_async_get_pins(self, mock): lock_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_PINS_URL) .format(lock_id=lock_id), body=load_fixture("get_pins.json"), ) api = ApiAsync(ClientSession()) pins = await api.async_get_pins(ACCESS_TOKEN, lock_id) self.assertEqual(1, len(pins)) first = pins[0] self.assertEqual("epoZ87XSPqxlFdsaYyJiRRVR", first.pin_id) self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", first.lock_id) self.assertEqual("c3b3a94f-473z-61a3-a8d1-a6e99482787a", first.user_id) self.assertEqual("in-use", first.state) self.assertEqual("123456", first.pin) self.assertEqual(646545456465161, first.slot) self.assertEqual("one-time", first.access_type) self.assertEqual("John", first.first_name) self.assertEqual("Doe", first.last_name) self.assertEqual(True, first.unverified) self.assertEqual(utc_of(2016, 11, 26, 22, 27, 11, 176000), first.created_at) self.assertEqual(utc_of(2017, 11, 23, 00, 42, 19, 470000), first.updated_at) self.assertEqual(utc_of(2017, 12, 10, 3, 12, 55, 563000), first.loaded_date) self.assertEqual(utc_of(2018, 1, 1, 1, 1, 1, 563000), first.access_start_time) self.assertEqual(utc_of(2018, 12, 1, 1, 1, 1, 563000), first.access_end_time) self.assertEqual(utc_of(2018, 11, 5, 10, 2, 41, 684000), first.access_times) @aioresponses() async def test_async_get_house_activities(self, mock): house_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_HOUSE_ACTIVITIES_URL) .format(house_id=house_id) + "?limit=8", body=load_fixture("get_house_activities.json"), ) api = ApiAsync(ClientSession()) activities = await api.async_get_house_activities(ACCESS_TOKEN, house_id) self.assertEqual(10, len(activities)) self.assertIsInstance(activities[0], yalexs.activity.LockOperationActivity) self.assertIsInstance(activities[1], yalexs.activity.LockOperationActivity) self.assertIsInstance(activities[2], yalexs.activity.LockOperationActivity) self.assertIsInstance(activities[3], yalexs.activity.LockOperationActivity) self.assertIsInstance(activities[4], yalexs.activity.LockOperationActivity) self.assertIsInstance(activities[5], yalexs.activity.DoorOperationActivity) self.assertIsInstance(activities[6], yalexs.activity.DoorOperationActivity) self.assertIsInstance(activities[7], yalexs.activity.DoorOperationActivity) self.assertIsInstance(activities[8], yalexs.activity.LockOperationActivity) self.assertIsInstance(activities[9], yalexs.activity.LockOperationActivity) @aioresponses() async def test_async_get_retry_raises_our_exception_class(self, mock): house_id = 1234 mock.get( ApiCommon(DEFAULT_BRAND) .get_brand_url(API_GET_HOUSE_ACTIVITIES_URL) .format(house_id=house_id) + "?limit=8", exception=ClientOSError("any"), ) api = ApiAsync(ClientSession()) with patch.object(api_async, "API_EXCEPTION_RETRY_TIME", 0), pytest.raises( AugustApiAIOHTTPError ): await api.async_get_house_activities(ACCESS_TOKEN, house_id) @aioresponses() async def test_async_refresh_access_token(self, mock): mock.get( ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_HOUSES_URL), body="{}", headers={"x-august-access-token": "xyz"}, ) api = ApiAsync(ClientSession()) new_token = await api.async_refresh_access_token("token") assert new_token == "xyz" @aioresponses() async def test_async_validate_verification_code(self, mock): last_args = {} def response_callback(url, **kwargs): last_args.update(kwargs) return CallbackResult(status=200, body="{}") mock.post( ApiCommon(DEFAULT_BRAND).get_brand_url( API_VALIDATE_VERIFICATION_CODE_URLS["email"] ), callback=response_callback, ) api = ApiAsync(ClientSession()) await api.async_validate_verification_code( ACCESS_TOKEN, "email", "emailaddress", 123456 ) assert last_args["json"] == {"code": "123456", "email": "emailaddress"} async def test__raise_response_exceptions(self): loop = mock.Mock() request_info = mock.Mock() request_info.status.return_value = 428 session = ClientSession() four_two_eight = MockedResponse( "get", URL("http://code404.tld"), request_info=request_info, writer=mock.Mock(), continue100=None, timer=TimerNoop(), traces=[], status=404, loop=loop, session=session, ) try: _raise_response_exceptions(four_two_eight) except Exception as err: self.assertIsInstance(err, AugustApiAIOHTTPError) ERROR_MAP = { 560: "The operation failed with error code 560: 560.", 422: "The operation failed because the bridge (connect) is offline: 422", 423: "The operation failed because the bridge (connect) is in use: 423", 408: "The operation timed out because the bridge (connect) failed to respond: 408", } for status_code in ERROR_MAP: mocked_response = MockedResponse( "get", URL("http://code.any.tld"), request_info=request_info, writer=mock.Mock(), continue100=None, timer=TimerNoop(), traces=[], status=status_code, loop=loop, session=session, ) try: _raise_response_exceptions(mocked_response) except AugustApiAIOHTTPError as err: self.assertEqual(str(err), ERROR_MAP[status_code]) @aioresponses() async def test_async_get_usern(self, mock): mock.get( ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_USER_URL), body='{"UserID": "abc"}', ) api = ApiAsync(ClientSession()) user_details = await api.async_get_user("token") assert user_details == {"UserID": "abc"} class MockedResponse(ClientResponse): def __init__(self, *args, **kwargs): content = kwargs.pop("content", None) status = kwargs.pop("status", None) super().__init__(*args, **kwargs) self._mocked_content = content self._mocked_status = status @property def content(self): return self._mocked_content @property def reason(self): return self._mocked_status @property def status(self): return self._mocked_status yalexs-8.0.2/tests/test_authenticator_async.py000066400000000000000000000214341465672432400216710ustar00rootroot00000000000000import json from datetime import datetime, timedelta, timezone import aiounittest from aiohttp import ClientError, ClientSession from aioresponses import aioresponses from dateutil.tz import tzutc from yalexs.api_async import ApiAsync from yalexs.api_common import ( API_GET_HOUSES_URL, API_GET_SESSION_URL, API_SEND_VERIFICATION_CODE_URLS, API_VALIDATE_VERIFICATION_CODE_URLS, ApiCommon, ) from yalexs.const import HEADER_AUGUST_ACCESS_TOKEN from yalexs.authenticator_async import ( AuthenticationState, AuthenticatorAsync, ValidationResult, ) from yalexs.const import DEFAULT_BRAND def format_datetime(dt): return dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + "Z" class TestAuthenticatorAsync(aiounittest.AsyncTestCase): def setUp(self): """Setup things to be run when tests are started.""" async def _async_create_authenticator_async(self, mock_aioresponses): authenticator = AuthenticatorAsync( ApiAsync(ClientSession()), "phone", "user", "pass", install_id="install_id" ) await authenticator.async_setup_authentication() return authenticator def _setup_session_response( self, mock_aioresponses, v_password, v_install_id, expires_at=format_datetime(datetime.utcnow()), ): mock_aioresponses.post( ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_SESSION_URL), headers={"x-august-access-token": "access_token"}, body=json.dumps( { "expiresAt": expires_at, "vPassword": v_password, "vInstallId": v_install_id, } ), ) @aioresponses() async def test_async_should_refresh_when_token_expiry_is_after_renewal_threshold( self, mock_aioresponses ): expired_expires_at = format_datetime( datetime.now(timezone.utc) + timedelta(days=6) ) self._setup_session_response( mock_aioresponses, True, True, expires_at=expired_expires_at ) authenticator = await self._async_create_authenticator_async(mock_aioresponses) await authenticator.async_authenticate() should_refresh = authenticator.should_refresh() self.assertEqual(True, should_refresh) @aioresponses() async def test_async_should_refresh_when_token_expiry_is_before_renewal_threshold( self, mock_aioresponses ): not_expired_expires_at = format_datetime( datetime.now(timezone.utc) + timedelta(days=8) ) self._setup_session_response( mock_aioresponses, True, True, expires_at=not_expired_expires_at ) authenticator = await self._async_create_authenticator_async(mock_aioresponses) await authenticator.async_authenticate() should_refresh = authenticator.should_refresh() self.assertEqual(False, should_refresh) @aioresponses() async def test_async_refresh_token(self, mock_aioresponses): self._setup_session_response(mock_aioresponses, True, True) authenticator = await self._async_create_authenticator_async(mock_aioresponses) await authenticator.async_authenticate() token = "e30=.eyJleHAiOjEzMzd9.e30=" mock_aioresponses.get( ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_HOUSES_URL), body=token, headers={HEADER_AUGUST_ACCESS_TOKEN: token}, ) access_token = await authenticator.async_refresh_access_token(force=False) self.assertEqual(token, access_token.access_token) self.assertEqual( datetime.fromtimestamp(1337, tz=tzutc()), access_token.parsed_expiration_time(), ) @aioresponses() async def test_async_get_session_with_authenticated_response( self, mock_aioresponses ): self._setup_session_response(mock_aioresponses, True, True) authenticator = await self._async_create_authenticator_async(mock_aioresponses) authentication = await authenticator.async_authenticate() self.assertEqual("access_token", authentication.access_token) self.assertEqual("install_id", authentication.install_id) self.assertEqual(AuthenticationState.AUTHENTICATED, authentication.state) @aioresponses() async def test_async_get_session_with_bad_password_response( self, mock_aioresponses ): self._setup_session_response(mock_aioresponses, False, True) authenticator = await self._async_create_authenticator_async(mock_aioresponses) authentication = await authenticator.async_authenticate() self.assertEqual("access_token", authentication.access_token) self.assertEqual("install_id", authentication.install_id) self.assertEqual(AuthenticationState.BAD_PASSWORD, authentication.state) @aioresponses() async def test_async_get_session_with_requires_validation_response( self, mock_aioresponses ): self._setup_session_response(mock_aioresponses, True, False) authenticator = await self._async_create_authenticator_async(mock_aioresponses) authentication = await authenticator.async_authenticate() self.assertEqual("access_token", authentication.access_token) self.assertEqual("install_id", authentication.install_id) self.assertEqual(AuthenticationState.REQUIRES_VALIDATION, authentication.state) @aioresponses() async def test_async_get_session_with_already_authenticated_state( self, mock_aioresponses ): self._setup_session_response(mock_aioresponses, True, True) authenticator = await self._async_create_authenticator_async(mock_aioresponses) # this will set authentication state to AUTHENTICATED await authenticator.async_authenticate() # call authenticate() again authentication = await authenticator.async_authenticate() self.assertEqual("access_token", authentication.access_token) self.assertEqual("install_id", authentication.install_id) self.assertEqual(AuthenticationState.AUTHENTICATED, authentication.state) @aioresponses() async def test_async_send_verification_code(self, mock_aioresponses): self._setup_session_response(mock_aioresponses, True, False) authenticator = await self._async_create_authenticator_async(mock_aioresponses) mock_aioresponses.post( ApiCommon(DEFAULT_BRAND).get_brand_url( API_SEND_VERIFICATION_CODE_URLS["phone"] ), body="{}", ) await authenticator.async_authenticate() result = await authenticator.async_send_verification_code() self.assertEqual(True, result) @aioresponses() async def test_async_validate_verification_code_with_no_code( self, mock_aioresponses ): self._setup_session_response(mock_aioresponses, True, False) authenticator = await self._async_create_authenticator_async(mock_aioresponses) await authenticator.async_authenticate() mock_aioresponses.post( ApiCommon(DEFAULT_BRAND).get_brand_url( API_VALIDATE_VERIFICATION_CODE_URLS["phone"] ), body="{}", ) result = await authenticator.async_validate_verification_code("") # mock_aioresponses.async_validate_verification_code.assert_not_called() self.assertEqual(ValidationResult.INVALID_VERIFICATION_CODE, result) @aioresponses() async def test_async_validate_verification_code_with_validated_response( self, mock_aioresponses ): self._setup_session_response(mock_aioresponses, True, False) mock_aioresponses.post( ApiCommon(DEFAULT_BRAND).get_brand_url( API_VALIDATE_VERIFICATION_CODE_URLS["phone"] ), body="{}", ) authenticator = await self._async_create_authenticator_async(mock_aioresponses) await authenticator.async_authenticate() result = await authenticator.async_validate_verification_code("123456") self.assertEqual(ValidationResult.VALIDATED, result) @aioresponses() async def test_async_validate_verification_code_with_invalid_code_response( self, mock_aioresponses ): self._setup_session_response(mock_aioresponses, True, False) mock_aioresponses.post( ApiCommon(DEFAULT_BRAND).get_brand_url( API_VALIDATE_VERIFICATION_CODE_URLS["phone"] ), exception=ClientError(), ) authenticator = await self._async_create_authenticator_async(mock_aioresponses) await authenticator.async_authenticate() result = await authenticator.async_validate_verification_code("123456") self.assertEqual(ValidationResult.INVALID_VERIFICATION_CODE, result) yalexs-8.0.2/tests/test_pubnub_activity.py000066400000000000000000000504731465672432400210360ustar00rootroot00000000000000import datetime import json import os import unittest import dateutil.parser from dateutil.tz import tzlocal from yalexs.activity import ( ActivityType, BridgeOperationActivity, DoorbellDingActivity, DoorbellImageCaptureActivity, DoorbellMotionActivity, DoorOperationActivity, LockOperationActivity, ) from yalexs.doorbell import DoorbellDetail from yalexs.lock import ( DOOR_STATE_KEY, LOCK_STATUS_KEY, LockDetail, LockDoorStatus, LockStatus, ) from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.users import cache_user_info, get_user_info def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path) as fptr: return fptr.read() class TestLockDetail(unittest.TestCase): def test_update_lock_details_from_pubnub_message(self): lock = LockDetail(json.loads(load_fixture("get_lock.doorsense_init.json"))) self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", lock.device_id) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.DISABLED, lock.door_state) activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "kAugLockState_Unlatching", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:05.373Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:05.371Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "unlatching" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "kAugLockState_Unlocking", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:05.373Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:05.371Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "unlocking" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:31.273Z"), { "remoteEvent": 1, "status": "kAugLockState_Locking", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:06.374Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:06.372Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "locking" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:31.273Z"), { "remoteEvent": 1, "status": "FAILED_BRIDGE_ERROR_LOCK_JAMMED", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:06.374Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:06.372Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) assert isinstance(activities[0], LockOperationActivity) assert activities[0].activity_start_time == dateutil.parser.parse( "2021-03-20T18:19:06.372Z" ).astimezone(tz=tzlocal()).replace(tzinfo=None) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "jammed" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:31.273Z"), { "remoteEvent": 1, "status": "kAugLockState_Unlocked", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:06.374Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:06.372Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].activity_start_time == dateutil.parser.parse( "2021-03-20T18:19:06.372Z" ).astimezone(tz=tzlocal()).replace(tzinfo=None) assert activities[0].action == "unlock" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "status": "locked", "callingUserID": "8918341e-7e68-4079-ad0a-1fa8a45d855b", "doorState": "closed", }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "lock" assert activities[0].operated_by is None assert isinstance(activities[1], DoorOperationActivity) assert "DoorOperationActivity" in str(activities[1]) assert activities[1].action == "doorclosed" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "status": "locked", "callingUserID": "8918341e-7e68-4079-ad0a-1fa8a45d855b", "doorState": "open", }, ) assert isinstance(activities[0], LockOperationActivity) assert activities[0].action == "lock" assert activities[0].operated_by is None assert ( activities[0].activity_type == ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR ) assert isinstance(activities[1], DoorOperationActivity) assert activities[1].action == "dooropen" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "status": "locked", "callingUserID": "cccca94e-373e-aaaa-bbbb-333396827777", "doorState": "closed", }, ) assert isinstance(activities[0], LockOperationActivity) assert activities[0].action == "lock" assert activities[0].operated_by is None assert ( activities[0].activity_type == ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR ) assert isinstance(activities[1], DoorOperationActivity) assert activities[1].action == "doorclosed" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T11:48:30.272Z"), { LOCK_STATUS_KEY: "DoorStateChanged", "lockID": "xxx", "timeStamp": 1615087688187, }, ) assert not activities activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T12:48:30.272Z"), { DOOR_STATE_KEY: "init", "lockID": "xxx", "timeStamp": 1615087688187, }, ) assert not activities activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T11:48:30.272Z"), { LOCK_STATUS_KEY: "associated_bridge_offline", "lockID": "xxx", "timeStamp": 1615087688187, }, ) assert isinstance(activities[0], BridgeOperationActivity) assert activities[0].action == "associated_bridge_offline" activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T11:48:30.272Z"), { LOCK_STATUS_KEY: "associated_bridge_online", "lockID": "xxx", "timeStamp": 1615087688187, }, ) assert isinstance(activities[0], BridgeOperationActivity) assert activities[0].action == "associated_bridge_online" cache_user_info( "5309b78d-de0c-4ec5-b878-02784c3b598a", {"FirstName": "bob", "LastName": "smith"}, ) assert get_user_info("5309b78d-de0c-4ec5-b878-02784c3b598a") is not None activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "status": "unlocked", "callingUserID": "5309b78d-de0c-4ec5-b878-02784c3b598a", "doorState": "closed", "info": { "action": "unlock", "startTime": "2017-12-10T05:48:30.272Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2017-12-10T05:48:30.272Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "unlock" assert activities[0].operated_by == "bob smith" activities = activities_from_pubnub_message( lock, datetime.datetime.fromtimestamp(16844292526891571 / 1000000), { "status": "unlatched", "callingUserID": "manualunlatch", "doorState": "open", }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "unlatch" assert ( activities[0].activity_type is ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR ) assert activities[0].operated_by is None activities = activities_from_pubnub_message( lock, datetime.datetime.fromtimestamp(16844292526891571 / 1000000), { "status": "unlocked", "callingUserID": "manualunlock", "doorState": "open", }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "unlock" assert ( activities[0].activity_type is ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR ) assert activities[0].operated_by is None activities = activities_from_pubnub_message( lock, datetime.datetime.fromtimestamp(16844299539729015 / 1000000), { "status": "locked", "callingUserID": "manuallock", "doorState": "open", }, ) assert isinstance(activities[0], LockOperationActivity) assert "LockOperationActivity" in str(activities[0]) assert activities[0].action == "lock" assert ( activities[0].activity_type is ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR ) assert activities[0].operated_by is None # status polls should not create activities activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "kAugLockState_Locked", "info": { "action": "status", "startTime": "2024-02-15T07:33:50.804Z", "context": { "transactionID": "RP99lHGUIx", "startDate": "2024-02-15T07:33:50.793Z", "retryCount": 1, }, "lockType": "lock_version_17", "serialNumber": "L.....", "rssi": 0, "wlanRSSI": -35, "wlanSNR": -1, "duration": 991, "lockID": "AF5EFD84.....", "bridgeID": "652e35ba7e.....", "serial": "L.....", }, "doorState": "kAugDoorState_Closed", "retryCount": 1, "totalTime": 1028, "resultsFromOperationCache": False, }, ) assert len(activities) == 0 class TestDetail(unittest.TestCase): def test_update_doorbell_details_from_pubnub_message(self): doorbell = DoorbellDetail(json.loads(load_fixture("get_doorbell.json"))) self.assertEqual("K98GiDT45GUL", doorbell.device_id) self.assertEqual( dateutil.parser.parse("2017-12-10T08:01:35Z"), doorbell.image_created_at_datetime, ) self.assertEqual( "https://image.com/vmk16naaaa7ibuey7sar.jpg", doorbell.image_url ) activities = activities_from_pubnub_message( doorbell, dateutil.parser.parse("2021-03-16T01:07:08.817Z"), { "status": "imagecapture", "data": { "event": "imagecapture", "result": { "created_at": "2021-03-16T01:07:08.817Z", "secure_url": "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg", }, }, }, ) assert isinstance(activities[0], DoorbellImageCaptureActivity) assert "DoorbellImageCaptureActivity" in str(activities[0]) assert ( activities[0].image_url == "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg" ) assert activities[0].image_created_at_datetime == dateutil.parser.parse( "2021-03-16T01:07:08.817Z" ) activities = activities_from_pubnub_message( doorbell, dateutil.parser.parse("2021-03-16T01:07:08.817Z"), { "status": "imagecapture", "data": { "event": "imagecapture", "result": { "created_at": "2021-03-16T01:07:08.817Z", "secure_url": "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg", }, }, }, ) assert isinstance(activities[0], DoorbellImageCaptureActivity) assert ( activities[0].image_url == "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg" ) assert activities[0].image_created_at_datetime == dateutil.parser.parse( "2021-03-16T01:07:08.817Z" ) activities = activities_from_pubnub_message( doorbell, dateutil.parser.parse("2021-03-16T01:07:08.817Z"), { "status": "doorbell_motion_detected", "callID": None, "origin": "mars-api", "data": { "event": "doorbell_motion_detected", "image": { "height": 640, "width": 480, "format": "jpg", "created_at": "2021-03-16T02:36:26.886Z", "bytes": 14061, "secure_url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", "url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", "etag": "09e839331c4ea59eef28081f2caa0e90", }, "doorbellName": "Front Door", "callID": None, "origin": "mars-api", "mutableContent": True, }, }, ) assert isinstance(activities[0], DoorbellMotionActivity) assert ( activities[0].image_url == "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg" ) assert activities[0].image_created_at_datetime == dateutil.parser.parse( "2021-03-16T02:36:26.886Z" ) activities = activities_from_pubnub_message( doorbell, dateutil.parser.parse("2021-03-16T01:07:08.817Z"), { "status": "buttonpush", "origin": "mars-api", "data": { "doorbellID": "26593a60f5d6", "event": "buttonpush", "doorbellName": "Front Door", "origin": "mars-api", }, }, ) assert isinstance(activities[0], DoorbellDingActivity) assert "DoorbellDingActivity" in str(activities[0]) class TestBridge(unittest.TestCase): def test_update_bridge_details_from_pubnub_message(self): lock = LockDetail(json.loads(load_fixture("get_lock.doorsense_init.json"))) self.assertEqual("A6697750D607098BAE8D6BAA11EF8063", lock.device_id) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.DISABLED, lock.door_state) activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "unknown", "result": "failed", "error": { "jse_shortmsg": "", "jse_info": {}, "message": "Bridge is offline", "statusCode": 422, "body": {"code": 98, "message": "Bridge is offline"}, "restCode": 98, "name": "ERRNO_BRIDGE_OFFLINE", "code": "Error", }, "info": { "lockID": "45E3635D35B9471FAF1218885816E90D", "action": "status", }, }, ) assert isinstance(activities[0], BridgeOperationActivity) assert "BridgeOperationActivity" in str(activities[0]) assert activities[0].action == "associated_bridge_offline" yalexs-8.0.2/tests/test_util.py000066400000000000000000000366111465672432400166020ustar00rootroot00000000000000import datetime import json import os import unittest import dateutil.parser from yalexs.activity import ( SOURCE_LOG, SOURCE_PUBNUB, BridgeOperationActivity, DoorbellMotionActivity, DoorOperationActivity, LockOperationActivity, ) from yalexs.api_common import _convert_lock_result_to_activities from yalexs.const import Brand from yalexs.doorbell import DoorbellDetail from yalexs.lock import LockDetail, LockDoorStatus, LockStatus from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.util import ( as_utc_from_local, get_configuration_url, get_latest_activity, get_ssl_context, update_doorbell_image_from_activity, update_lock_detail_from_activity, ) def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path) as fptr: return fptr.read() def test_get_latest_activity(): """Test when two activities happen at the same time we prefer the one that is not moving.""" lock = LockDetail(json.loads(load_fixture("get_lock.doorsense_init.json"))) unlocking_activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "kAugLockState_Unlocking", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:05.373Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:05.371Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) unlocked_activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "kAugLockState_Unlocked", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:05.373Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:05.371Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) assert get_latest_activity(unlocking_activities[0], None) == unlocking_activities[0] assert get_latest_activity(None, unlocked_activities[0]) == unlocked_activities[0] assert ( get_latest_activity(unlocking_activities[0], unlocked_activities[0]) == unlocked_activities[0] ) assert ( get_latest_activity(unlocked_activities[0], unlocking_activities[0]) == unlocked_activities[0] ) def test_update_lock_detail_from_activity(): """Test when two activities happen at the same time we prefer the one that is not moving.""" lock = LockDetail(json.loads(load_fixture("get_lock.doorsense_init.json"))) unlocking_activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "kAugLockState_Unlocking", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:05.373Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:05.371Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) unlocked_activities = activities_from_pubnub_message( lock, dateutil.parser.parse("2017-12-10T05:48:30.272Z"), { "remoteEvent": 1, "status": "kAugLockState_Unlocked", "info": { "action": "unlock", "startTime": "2021-03-20T18:19:05.373Z", "context": { "transactionID": "_oJRZKJsx", "startDate": "2021-03-20T18:19:05.371Z", "retryCount": 1, }, "lockType": "lock_version_1001", "serialNumber": "M1FBA029QJ", "rssi": -53, "wlanRSSI": -55, "wlanSNR": 44, "duration": 2534, }, }, ) update_lock_detail_from_activity(lock, unlocking_activities[0]) assert lock.lock_status == LockStatus.UNLOCKING update_lock_detail_from_activity(lock, unlocked_activities[0]) assert lock.lock_status == LockStatus.UNLOCKED update_lock_detail_from_activity(lock, unlocking_activities[0]) assert lock.lock_status == LockStatus.UNLOCKED class TestLockDetail(unittest.TestCase): def test_update_lock_with_activity_has_no_status(self): lock = LockDetail( json.loads(load_fixture("get_lock.nostatus_with_doorsense.json")) ) self.assertEqual("ABC", lock.device_id) self.assertEqual(LockStatus.UNKNOWN, lock.lock_status) self.assertEqual(LockDoorStatus.UNKNOWN, lock.door_state) self.assertEqual(None, lock.lock_status_datetime) self.assertEqual(None, lock.door_state_datetime) unlock_operation_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("unlock_activity.json")) ) self.assertTrue( update_lock_detail_from_activity(lock, unlock_operation_activity) ) self.assertEqual(LockStatus.UNLOCKED, lock.lock_status) def test_update_lock_with_activity(self): lock = LockDetail( json.loads(load_fixture("get_lock.online_with_doorsense.json")) ) self.assertEqual("ABC", lock.device_id) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual(LockDoorStatus.OPEN, lock.door_state) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.lock_status_datetime ) self.assertEqual( dateutil.parser.parse("2017-12-10T04:48:30.272Z"), lock.door_state_datetime ) lock_operation_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("lock_activity.json")) ) unlock_operation_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("unlock_activity.json")) ) open_operation_activity = DoorOperationActivity( SOURCE_LOG, json.loads(load_fixture("door_open_activity.json")) ) closed_operation_activity = DoorOperationActivity( SOURCE_LOG, json.loads(load_fixture("door_closed_activity.json")) ) closed_operation_wrong_deviceid_activity = DoorOperationActivity( SOURCE_LOG, json.loads(load_fixture("door_closed_activity_wrong_deviceid.json")), ) closed_operation_wrong_houseid_activity = DoorOperationActivity( SOURCE_LOG, json.loads(load_fixture("door_closed_activity_wrong_houseid.json")), ) self.assertTrue( update_lock_detail_from_activity(lock, unlock_operation_activity) ) self.assertEqual(LockStatus.UNLOCKED, lock.lock_status) self.assertEqual( as_utc_from_local(datetime.datetime.fromtimestamp(1582007217000 / 1000)), lock.lock_status_datetime, ) self.assertTrue(update_lock_detail_from_activity(lock, lock_operation_activity)) self.assertEqual(LockStatus.LOCKED, lock.lock_status) self.assertEqual( as_utc_from_local(datetime.datetime.fromtimestamp(1582007218000 / 1000)), lock.lock_status_datetime, ) # returns false we send an older activity self.assertFalse( update_lock_detail_from_activity(lock, unlock_operation_activity) ) self.assertTrue( update_lock_detail_from_activity(lock, closed_operation_activity) ) self.assertEqual(LockDoorStatus.CLOSED, lock.door_state) self.assertEqual( as_utc_from_local(datetime.datetime.fromtimestamp(1582007217000 / 1000)), lock.door_state_datetime, ) self.assertTrue(update_lock_detail_from_activity(lock, open_operation_activity)) self.assertEqual(LockDoorStatus.OPEN, lock.door_state) self.assertEqual( as_utc_from_local(datetime.datetime.fromtimestamp(1582007219000 / 1000)), lock.door_state_datetime, ) # returns false we send an older activity self.assertFalse( update_lock_detail_from_activity(lock, closed_operation_activity) ) with self.assertRaises(ValueError): update_lock_detail_from_activity( lock, closed_operation_wrong_deviceid_activity ) # We do not always have the houseid so we do not throw # as long as the deviceid is correct since they are unique self.assertFalse( update_lock_detail_from_activity( lock, closed_operation_wrong_houseid_activity ) ) self.assertEqual(LockDoorStatus.OPEN, lock.door_state) self.assertEqual(LockStatus.LOCKED, lock.lock_status) activities = _convert_lock_result_to_activities( json.loads(load_fixture("unlock.json")) ) for activity in activities: self.assertTrue(update_lock_detail_from_activity(lock, activity)) self.assertEqual(LockDoorStatus.CLOSED, lock.door_state) self.assertEqual(LockStatus.UNLOCKED, lock.lock_status) bridge_offline_activity = BridgeOperationActivity( SOURCE_PUBNUB, { "action": "associated_bridge_offline", "callingUser": {"UserID": None}, "dateTime": 1512906510272.0, "deviceName": "Front Door Lock", "deviceType": "lock", "deviceID": lock.device_id, "house": "000000000000", "info": {}, }, ) assert bridge_offline_activity.source == SOURCE_PUBNUB self.assertTrue(update_lock_detail_from_activity(lock, bridge_offline_activity)) assert lock.bridge_is_online is False bridge_online_activity = BridgeOperationActivity( SOURCE_PUBNUB, { "action": "associated_bridge_online", "callingUser": {"UserID": None}, "dateTime": 1512906510272.0, "deviceName": "Front Door Lock", "deviceType": "lock", "deviceID": lock.device_id, "house": "000000000000", "info": {}, }, ) self.assertTrue(update_lock_detail_from_activity(lock, bridge_online_activity)) assert lock.bridge_is_online is True assert bridge_online_activity.source == SOURCE_PUBNUB class TestDetail(unittest.TestCase): def test_update_doorbell_image_from_activity(self): doorbell = DoorbellDetail(json.loads(load_fixture("get_doorbell.json"))) self.assertEqual("K98GiDT45GUL", doorbell.device_id) self.assertEqual( dateutil.parser.parse("2017-12-10T08:01:35Z"), doorbell.image_created_at_datetime, ) self.assertEqual( "https://image.com/vmk16naaaa7ibuey7sar.jpg", doorbell.image_url ) doorbell_motion_activity_no_image = DoorbellMotionActivity( SOURCE_LOG, json.loads(load_fixture("doorbell_motion_activity_no_image.json")), ) self.assertFalse( update_doorbell_image_from_activity( doorbell, doorbell_motion_activity_no_image ) ) doorbell_motion_activity = DoorbellMotionActivity( SOURCE_LOG, json.loads(load_fixture("doorbell_motion_activity.json")) ) self.assertTrue( update_doorbell_image_from_activity(doorbell, doorbell_motion_activity) ) self.assertEqual( dateutil.parser.parse("2020-02-20T17:44:45Z"), doorbell.image_created_at_datetime, ) self.assertEqual("https://my.updated.image/image.jpg", doorbell.image_url) old_doorbell_motion_activity = DoorbellMotionActivity( SOURCE_LOG, json.loads(load_fixture("doorbell_motion_activity_old.json")) ) # returns false we send an older activity self.assertFalse( update_doorbell_image_from_activity(doorbell, old_doorbell_motion_activity) ) self.assertEqual( dateutil.parser.parse("2020-02-20T17:44:45Z"), doorbell.image_created_at_datetime, ) self.assertEqual("https://my.updated.image/image.jpg", doorbell.image_url) wrong_doorbell_motion_activity = DoorbellMotionActivity( SOURCE_LOG, json.loads(load_fixture("doorbell_motion_activity_wrong.json")) ) with self.assertRaises(ValueError): update_doorbell_image_from_activity( doorbell, wrong_doorbell_motion_activity ) def test_update_doorbell_image_from_activity_missing_image_at_start(self): doorbell = DoorbellDetail( json.loads(load_fixture("get_doorbell_missing_image.json")) ) self.assertEqual("K98GiDT45GUL", doorbell.device_id) self.assertEqual( None, doorbell.image_created_at_datetime, ) self.assertEqual(None, doorbell.image_url) doorbell_motion_activity_no_image = DoorbellMotionActivity( SOURCE_LOG, json.loads(load_fixture("doorbell_motion_activity_no_image.json")), ) self.assertFalse( update_doorbell_image_from_activity( doorbell, doorbell_motion_activity_no_image ) ) doorbell_motion_activity = DoorbellMotionActivity( SOURCE_LOG, json.loads(load_fixture("doorbell_motion_activity.json")) ) self.assertTrue( update_doorbell_image_from_activity(doorbell, doorbell_motion_activity) ) self.assertEqual( dateutil.parser.parse("2020-02-20T17:44:45Z"), doorbell.image_created_at_datetime, ) self.assertEqual("https://my.updated.image/image.jpg", doorbell.image_url) def test_get_configuration_url(): """Test that we get the correct configuration url for the brand.""" assert get_configuration_url("august") == "https://account.august.com" assert get_configuration_url("yale_access") == "https://account.august.com" assert get_configuration_url("yale_home") == "https://account.aaecosystem.com" assert get_configuration_url(Brand.AUGUST) == "https://account.august.com" assert get_configuration_url(Brand.YALE_ACCESS) == "https://account.august.com" assert get_configuration_url(Brand.YALE_HOME) == "https://account.aaecosystem.com" def test_get_ssl_context(): """Test getting the ssl context is cached.""" assert get_ssl_context() is get_ssl_context() yalexs-8.0.2/tox.ini000066400000000000000000000024441465672432400143620ustar00rootroot00000000000000[tox] envlist = format, py36, py37, py38, py39, py310, py311, lint skip_missing_interpreters = True [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/yalexs allowlist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = py.test --basetemp={envtmpdir} --cov --cov-report term-missing deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_tests.txt [testenv:format] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/yalexs allowlist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = isort --check --diff yalexs tests black --check --diff yalexs tests deps = -r{toxinidir}/requirements_tests.txt [testenv:codecov] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/yalexs allowlist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = py.test --basetemp={envtmpdir} --cov --cov-report=xml {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_tests.txt [testenv:lint] ignore_errors = True commands = flake8 pylint yalexs [testenv:bandit] commands = bandit -r yalexs deps = -r{toxinidir}/requirements_tests.txt [flake8] max-line-length = 120 yalexs-8.0.2/yalexs/000077500000000000000000000000001465672432400143505ustar00rootroot00000000000000yalexs-8.0.2/yalexs/__init__.py000066400000000000000000000001631465672432400164610ustar00rootroot00000000000000"""Init file for yalexs.""" __author__ = """J. Nick Koston""" __email__ = "nick@koston.org" __version__ = "8.0.2" yalexs-8.0.2/yalexs/activity.py000066400000000000000000000463221465672432400165650ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from enum import Enum from typing import Any, Union from .backports.enum import StrEnum from .backports.functools import cached_property from .lock import LockDoorStatus, LockStatus from .time import epoch_to_datetime, parse_datetime from .users import YaleUser, get_user_info ACTION_LOCK_ONETOUCHLOCK = "onetouchlock" ACTION_LOCK_ONETOUCHLOCK_2 = "one_touch_lock" ACTION_LOCK_LOCK = "lock" ACTION_RF_LOCK = "rf_lock" ACTION_RF_SECURE = "rf_secure" ACTION_RF_UNLATCH = "rf_unlatch" ACTION_RF_UNLOCK = "rf_unlock" ACTION_LOCK_AUTO_LOCK = "auto_lock" ACTION_LOCK_BLE_LOCK = "ble_lock" ACTION_LOCK_BLE_UNLATCH = "ble_unlatch" ACTION_LOCK_BLE_UNLOCK = "ble_unlock" ACTION_LOCK_REMOTE_LOCK = "remote_lock" ACTION_LOCK_REMOTE_UNLATCH = "remote_unlatch" ACTION_LOCK_REMOTE_UNLOCK = "remote_unlock" ACTION_LOCK_PIN_UNLATCH = "pin_unlatch" ACTION_LOCK_PIN_UNLOCK = "pin_unlock" ACTION_LOCK_MANUAL_LOCK = "manual_lock" ACTION_LOCK_MANUAL_UNLATCH = "manual_unlatch" ACTION_LOCK_MANUAL_UNLOCK = "manual_unlock" ACTION_LOCK_LOCKING = "locking" ACTION_LOCK_UNLATCH = "unlatch" ACTION_LOCK_UNLATCHING = "unlatching" ACTION_LOCK_UNLOCK = "unlock" ACTION_LOCK_UNLOCKING = "unlocking" ACTION_LOCK_JAMMED = "jammed" ACTION_HOMEKEY_LOCK = "homekey_lock" ACTION_HOMEKEY_UNLATCH = "homekey_unlatch" ACTION_HOMEKEY_UNLOCK = "homekey_unlock" ACTION_DOOR_OPEN = "dooropen" ACTION_DOOR_OPEN_2 = "door_open" ACTION_DOOR_CLOSED = "doorclosed" ACTION_DOOR_CLOSE_2 = "door_close" ACTION_DOORBELL_CALL_INITIATED = "doorbell_call_initiated" ACTION_DOORBELL_MOTION_DETECTED = "doorbell_motion_detected" ACTION_DOORBELL_CALL_MISSED = "doorbell_call_missed" ACTION_DOORBELL_CALL_HANGUP = "doorbell_call_hangup" ACTION_LOCK_DOORBELL_BUTTON_PUSHED = "lock_accessory_motion_detect" ACTION_BRIDGE_ONLINE = "associated_bridge_online" # pubnub only ACTION_BRIDGE_OFFLINE = "associated_bridge_offline" # pubnub only ACTION_DOORBELL_IMAGE_CAPTURE = "imagecapture" # pubnub only ACTION_DOORBELL_BUTTON_PUSHED = "buttonpush" # pubnub only ACTIVITY_ACTIONS_BRIDGE_OPERATION = {ACTION_BRIDGE_ONLINE, ACTION_BRIDGE_OFFLINE} ACTIVITY_ACTIONS_DOORBELL_DING = { ACTION_DOORBELL_BUTTON_PUSHED, ACTION_DOORBELL_CALL_MISSED, ACTION_DOORBELL_CALL_HANGUP, ACTION_LOCK_DOORBELL_BUTTON_PUSHED, } ACTIVITY_ACTIONS_DOORBELL_IMAGE_CAPTURE = {ACTION_DOORBELL_IMAGE_CAPTURE} ACTIVITY_ACTIONS_DOORBELL_MOTION = {ACTION_DOORBELL_MOTION_DETECTED} ACTIVITY_ACTIONS_DOORBELL_VIEW = {ACTION_DOORBELL_CALL_INITIATED} ACTIVITY_ACTIONS_LOCK_OPERATION = { ACTION_RF_SECURE, ACTION_RF_LOCK, ACTION_RF_UNLATCH, ACTION_RF_UNLOCK, ACTION_HOMEKEY_LOCK, ACTION_HOMEKEY_UNLATCH, ACTION_HOMEKEY_UNLOCK, ACTION_LOCK_AUTO_LOCK, ACTION_LOCK_ONETOUCHLOCK, ACTION_LOCK_ONETOUCHLOCK_2, ACTION_LOCK_LOCK, ACTION_LOCK_UNLATCH, ACTION_LOCK_UNLOCK, ACTION_LOCK_LOCKING, ACTION_LOCK_UNLATCHING, ACTION_LOCK_UNLOCKING, ACTION_LOCK_JAMMED, ACTION_LOCK_BLE_LOCK, ACTION_LOCK_BLE_UNLATCH, ACTION_LOCK_BLE_UNLOCK, ACTION_LOCK_REMOTE_LOCK, ACTION_LOCK_REMOTE_UNLATCH, ACTION_LOCK_REMOTE_UNLOCK, ACTION_LOCK_PIN_UNLATCH, ACTION_LOCK_PIN_UNLOCK, ACTION_LOCK_MANUAL_LOCK, ACTION_LOCK_MANUAL_UNLATCH, ACTION_LOCK_MANUAL_UNLOCK, } ACTIVITY_TO_FIRST_LAST_NAME = { ACTION_RF_SECURE: ("Radio Frequency", "Secure"), ACTION_RF_LOCK: ("Radio Frequency", "Lock"), ACTION_RF_UNLATCH: ("Radio Frequency", "Unlatch"), ACTION_RF_UNLOCK: ("Radio Frequency", "Unlock"), ACTION_HOMEKEY_LOCK: ("Homekey", "Lock"), ACTION_HOMEKEY_UNLATCH: ("Homekey", "Unlatch"), ACTION_HOMEKEY_UNLOCK: ("Homekey", "Unlock"), ACTION_LOCK_AUTO_LOCK: ("Auto", "Lock"), ACTION_LOCK_ONETOUCHLOCK: ("One-Touch", "Lock"), ACTION_LOCK_ONETOUCHLOCK_2: ("One-Touch", "Lock"), ACTION_LOCK_BLE_LOCK: ("Bluetooth", "Lock"), ACTION_LOCK_BLE_UNLATCH: ("Bluetooth", "Unlatch"), ACTION_LOCK_BLE_UNLOCK: ("Bluetooth", "Unlock"), ACTION_LOCK_MANUAL_LOCK: ("Manual", "Lock"), ACTION_LOCK_MANUAL_UNLATCH: ("Manual", "Unlatch"), ACTION_LOCK_MANUAL_UNLOCK: ("Manual", "Unlock"), } ACTIVITY_ACTIONS_DOOR_OPERATION = { ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN, ACTION_DOOR_OPEN_2, ACTION_DOOR_CLOSE_2, } KEYPAD_ACTIONS = { ACTION_LOCK_ONETOUCHLOCK, ACTION_LOCK_ONETOUCHLOCK_2, ACTION_LOCK_PIN_UNLATCH, ACTION_LOCK_PIN_UNLOCK, } REMOTE_ACTIONS = { ACTION_LOCK_REMOTE_LOCK, ACTION_LOCK_REMOTE_UNLATCH, ACTION_LOCK_REMOTE_UNLOCK, } AUTO_RELOCK_ACTIONS = {ACTION_LOCK_AUTO_LOCK} TAG_ACTIONS = { ACTION_RF_SECURE, ACTION_RF_LOCK, ACTION_RF_UNLATCH, ACTION_RF_UNLOCK, ACTION_HOMEKEY_LOCK, ACTION_HOMEKEY_UNLATCH, ACTION_HOMEKEY_UNLOCK, } MANUAL_ACTIONS = { ACTION_LOCK_MANUAL_LOCK, ACTION_LOCK_MANUAL_UNLATCH, ACTION_LOCK_MANUAL_UNLOCK, } ACTIVITY_ACTION_STATES: dict[str, LockStatus | LockDoorStatus] = { ACTION_RF_SECURE: LockStatus.LOCKED, ACTION_RF_LOCK: LockStatus.LOCKED, ACTION_RF_UNLATCH: LockStatus.UNLATCHED, ACTION_RF_UNLOCK: LockStatus.UNLOCKED, ACTION_HOMEKEY_LOCK: LockStatus.LOCKED, ACTION_HOMEKEY_UNLATCH: LockStatus.UNLATCHED, ACTION_HOMEKEY_UNLOCK: LockStatus.UNLOCKED, ACTION_LOCK_AUTO_LOCK: LockStatus.LOCKED, ACTION_LOCK_ONETOUCHLOCK: LockStatus.LOCKED, ACTION_LOCK_ONETOUCHLOCK_2: LockStatus.LOCKED, ACTION_LOCK_LOCK: LockStatus.LOCKED, ACTION_LOCK_UNLATCH: LockStatus.UNLATCHED, ACTION_LOCK_UNLOCK: LockStatus.UNLOCKED, ACTION_LOCK_LOCKING: LockStatus.LOCKING, ACTION_LOCK_UNLATCHING: LockStatus.UNLATCHING, ACTION_LOCK_UNLOCKING: LockStatus.UNLOCKING, ACTION_LOCK_JAMMED: LockStatus.JAMMED, ACTION_DOOR_OPEN: LockDoorStatus.OPEN, ACTION_DOOR_CLOSED: LockDoorStatus.CLOSED, ACTION_DOOR_OPEN_2: LockDoorStatus.OPEN, ACTION_DOOR_CLOSE_2: LockDoorStatus.CLOSED, ACTION_LOCK_BLE_LOCK: LockStatus.LOCKED, ACTION_LOCK_BLE_UNLATCH: LockStatus.UNLATCHED, ACTION_LOCK_BLE_UNLOCK: LockStatus.UNLOCKED, ACTION_LOCK_REMOTE_LOCK: LockStatus.LOCKED, ACTION_LOCK_REMOTE_UNLATCH: LockStatus.UNLATCHED, ACTION_LOCK_REMOTE_UNLOCK: LockStatus.UNLOCKED, ACTION_LOCK_PIN_UNLATCH: LockStatus.UNLATCHED, ACTION_LOCK_PIN_UNLOCK: LockStatus.UNLOCKED, ACTION_LOCK_MANUAL_LOCK: LockStatus.LOCKED, ACTION_LOCK_MANUAL_UNLATCH: LockStatus.UNLATCHED, ACTION_LOCK_MANUAL_UNLOCK: LockStatus.UNLOCKED, } class Source(StrEnum): """Source of the activity.""" LOCK_OPERATE = "lock_operate" PUBNUB = "pubnub" LOG = "log" WEBSOCKET = "websocket" SOURCE_LOCK_OPERATE = Source.LOCK_OPERATE SOURCE_PUBNUB = Source.PUBNUB SOURCE_LOG = Source.LOG SOURCE_WEBSOCKET = Source.WEBSOCKET # If we get a lock operation activity with the same time stamp as a moving # activity we want to use the non-moving activity since its the completed state. MOVING_STATES: set[LockStatus | LockDoorStatus] = { LockStatus.UNLOCKING, LockStatus.UNLATCHING, LockStatus.LOCKING, } ACTIVITY_MOVING_STATES = { action for action, action_state in ACTIVITY_ACTION_STATES.items() if action_state in MOVING_STATES } class ActivityType(Enum): DOORBELL_MOTION = "doorbell_motion" DOORBELL_DING = "doorbell_ding" DOORBELL_VIEW = "doorbell_view" LOCK_OPERATION = "lock_operation" LOCK_OPERATION_WITHOUT_OPERATOR = "lock_operation_without_operator" DOOR_OPERATION = "door_operation" BRIDGE_OPERATION = "bridge_operation" DOORBELL_IMAGE_CAPTURE = "doorbell_image_capture" class Activity: """Base class for activities.""" def __init__(self, source: str, data: dict[str, Any]) -> None: """Initialize activity.""" self._source = source self._data = data def __repr__(self): """Return the representation.""" return ( f"<{self.__class__.__name__} action={self.action} activity_type={self.activity_type} " f"activity_start_time={self.activity_start_time} " f"device_name={self.device_name}>" ) @cached_property def _entities(self) -> dict[str, Any]: """Return the entities of the activity.""" return self._data.get("entities", {}) @cached_property def _info(self) -> dict[str, Any]: """Return the info of the activity.""" return self._data.get("info", {}) @cached_property def was_pushed(self) -> bool: """Return if the activity was pushed.""" return self._source in (SOURCE_PUBNUB, SOURCE_WEBSOCKET) @cached_property def source(self) -> str: """Return the source of the activity.""" return self._source @cached_property def activity_type(self) -> ActivityType: """Return the type of the activity.""" return self._activity_type @cached_property def activity_id(self) -> str | None: """Return the ID of the activity.""" return self._entities.get("activity") @cached_property def house_id(self) -> str | None: """Return the house ID of the activity.""" return self._entities.get("house") @cached_property def activity_start_time(self) -> datetime: """Return the start time of the activity.""" data = self._data return epoch_to_datetime(data.get("dateTime", data.get("timestamp"))) @cached_property def activity_end_time(self) -> datetime: """Return the end time of the activity.""" return self.activity_start_time @cached_property def action(self) -> str | None: """Return the action of the activity.""" return self._data.get("action") @cached_property def device_id(self) -> str | None: """Return the ID of the device.""" return self._data.get("deviceID") @cached_property def device_name(self) -> str | None: """Return the name of the device.""" return self._data.get("deviceName") @cached_property def device_type(self) -> str | None: """Return the type of the device.""" return self._data.get("deviceType") class BaseDoorbellMotionActivity(Activity): """Base class for doorbell motion activities.""" def __repr__(self): return ( f"<{self.__class__.__name__} action={self.action} activity_type={self.activity_type} " f"activity_start_time={self.activity_start_time} " f"device_name={self.device_name}" f"image_url={self.image_url}>" f"content_token={self.content_token}" ) @cached_property def _image(self) -> dict[str, Any] | None: """Return the image of the activity.""" return self._info.get("image") @cached_property def _content_token(self) -> str | None: """Return the content token of the activity.""" return self._data.get("doorbell", {}).get("contentToken") @cached_property def image_url(self): """Return the image URL of the activity.""" image = self._image return (None if image is None else image.get("secure_url")) or self._data.get( "attachment" ) @cached_property def content_token(self): """Return the contentToken for the image URL""" return self._content_token or "" @cached_property def image_created_at_datetime(self): """Return the image created at datetime.""" image = self._image if image is None: return None if "created_at" in image: return parse_datetime(image["created_at"]) return self.activity_start_time class DoorbellMotionActivity(BaseDoorbellMotionActivity): """A motion activity.""" _activity_type = ActivityType.DOORBELL_MOTION class DoorbellImageCaptureActivity(BaseDoorbellMotionActivity): """A motion activity with an image.""" _activity_type = ActivityType.DOORBELL_IMAGE_CAPTURE class DoorbellBaseActionActivity(Activity): """Base class for doorbell action activities.""" @cached_property def image_url(self): """Return the image URL of the activity.""" return self._info.get("image") or self._info.get("attachment") @cached_property def activity_start_time(self): """Return the start time of the activity.""" if started := self._info.get("started"): return epoch_to_datetime(started) return super().activity_start_time @cached_property def activity_end_time(self): """Return the end time of the activity.""" if ended := self._info.get("ended"): return epoch_to_datetime(ended) return super().activity_start_time class DoorbellDingActivity(DoorbellBaseActionActivity): """Doorbell ding activity.""" _activity_type = ActivityType.DOORBELL_DING class DoorbellViewActivity(DoorbellBaseActionActivity): """Doorbell view activity.""" _activity_type = ActivityType.DOORBELL_VIEW class LockOperationActivity(Activity): """Lock operation activity.""" _activity_type = ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR def __init__(self, source: str, data: dict[str, Any]) -> None: """Initialize lock operation activity.""" super().__init__(source, data) operated_by: str | None = None calling_user = self.calling_user first_name: str | None = calling_user.get("FirstName") last_name: str | None = calling_user.get("LastName") yale_user = self.yale_user if yale_user and first_name is None and last_name is None: first_name = yale_user.first_name last_name = yale_user.last_name # For legacy compatibility, we need to set the first_name and last_name # if its a physical or rf lock operation if ( first_name is None and last_name is None and (first_last := ACTIVITY_TO_FIRST_LAST_NAME.get(self.action)) ): first_name, last_name = first_last if first_name and last_name: operated_by = f"{first_name} {last_name}" self._activity_type = ActivityType.LOCK_OPERATION self._operated_by = operated_by @cached_property def _operator_image_urls(self) -> tuple[str | None, str | None]: """Return the image URLs of the lock operator.""" operator_image_url: str | None = None operator_thumbnail_url: str | None = None calling_user = self.calling_user image_info = calling_user.get("imageInfo") or calling_user original = image_info.get("original") if type(original) is str: # pylint: disable=unidiomatic-typecheck # noqa: E721 operator_image_url = original elif type(original) is dict: # pylint: disable=unidiomatic-typecheck # noqa: E721 operator_image_url = original.get("secure_url") else: operator_image_url = None thumbnail = image_info.get("thumbnail") if type(thumbnail) is str: # pylint: disable=unidiomatic-typecheck # noqa: E721 operator_thumbnail_url = thumbnail elif type(thumbnail) is dict: # pylint: disable=unidiomatic-typecheck # noqa: E721 operator_thumbnail_url = thumbnail.get("secure_url") else: operator_thumbnail_url = None yale_user = self.yale_user if yale_user and not operator_image_url and not operator_thumbnail_url: operator_image_url = yale_user.image_url operator_thumbnail_url = yale_user.thumbnail_url if not operator_thumbnail_url: if icon := self._data.get("icon"): operator_thumbnail_url = icon if operator_thumbnail_url and not operator_image_url: operator_image_url = operator_thumbnail_url if operator_image_url and not operator_thumbnail_url: operator_thumbnail_url = operator_image_url return operator_image_url, operator_thumbnail_url def __repr__(self): """Return the representation.""" return ( f"<{self.__class__.__name__} action={self.action} activity_type={self.activity_type} " f"activity_start_time={self.activity_start_time} " f"device_name={self.device_name} " f"operated_by={self.operated_by} " f"operated_remote={self.operated_remote} " f"operated_keypad={self.operated_keypad} " f"operated_tag={self.operated_tag} " f"operated_manual={self.operated_manual} " f"operated_autorelock={self.operated_autorelock} " f"operator_image_url={self.operator_image_url} " f"operator_thumbnail_url={self.operator_thumbnail_url}>" ) @cached_property def yale_user(self) -> YaleUser | None: """Return the Yale user.""" return get_user_info(self.user_id) @cached_property def calling_user(self) -> dict[str, Any]: """Return the the calling user.""" return self._data.get("callingUser", self._data.get("user", {})) @cached_property def user_id(self) -> str | None: """Return the ID of the user.""" return self.calling_user.get("UserID") @cached_property def operated_by(self): return self._operated_by @cached_property def operated_remote(self): """Operation was remote.""" return self._info.get("remote", self.action in REMOTE_ACTIONS) @cached_property def operated_keypad(self): """Operation used keypad.""" return self._info.get("keypad", self.action in KEYPAD_ACTIONS) @cached_property def operated_manual(self): """Operation done manually using the knob.""" return self._info.get("manual", self.action in MANUAL_ACTIONS) @cached_property def operated_tag(self): """Operation used rfid tag.""" return self._info.get("tag", self.action in TAG_ACTIONS) @cached_property def operated_autorelock(self): """Operation done by automatic relock.""" return self.user_id == "automaticrelock" or self.action in AUTO_RELOCK_ACTIONS @cached_property def operator_image_url(self): """URL to the image of the lock operator.""" return self._operator_image_urls[0] @cached_property def operator_thumbnail_url(self): """URL to the thumbnail of the lock operator.""" return self._operator_image_urls[1] class DoorOperationActivity(Activity): """Door operation activity.""" _activity_type = ActivityType.DOOR_OPERATION class BridgeOperationActivity(Activity): """Bridge operation activity.""" _activity_type = ActivityType.BRIDGE_OPERATION ActivityTypes = Union[ DoorbellDingActivity, DoorbellMotionActivity, DoorbellImageCaptureActivity, DoorbellViewActivity, LockOperationActivity, DoorOperationActivity, BridgeOperationActivity, ] ACTIONS_TO_CLASS = ( (ACTIVITY_ACTIONS_DOORBELL_DING, DoorbellDingActivity), (ACTIVITY_ACTIONS_DOORBELL_MOTION, DoorbellMotionActivity), (ACTIVITY_ACTIONS_DOORBELL_IMAGE_CAPTURE, DoorbellImageCaptureActivity), (ACTIVITY_ACTIONS_DOORBELL_VIEW, DoorbellViewActivity), (ACTIVITY_ACTIONS_LOCK_OPERATION, LockOperationActivity), (ACTIVITY_ACTIONS_DOOR_OPERATION, DoorOperationActivity), (ACTIVITY_ACTIONS_BRIDGE_OPERATION, BridgeOperationActivity), ) ACTION_TO_CLASS: dict[str, ActivityTypes] = {} for activities, klass in ACTIONS_TO_CLASS: for activity in activities: ACTION_TO_CLASS[activity] = klass yalexs-8.0.2/yalexs/api_async.py000066400000000000000000000407531465672432400167010ustar00rootroot00000000000000"""Api calls for sync.""" from __future__ import annotations import asyncio import logging from http import HTTPStatus from typing import Any from aiohttp import ( ClientConnectionError, ClientOSError, ClientResponse, ClientResponseError, ClientSession, ClientSSLError, ServerDisconnectedError, ) from .const import HEADER_ACCESS_TOKEN, HEADER_AUGUST_ACCESS_TOKEN from .activity import ActivityTypes from .api_common import ( API_EXCEPTION_RETRY_TIME, API_LOCK_ASYNC_URL, API_LOCK_URL, API_RETRY_ATTEMPTS, API_RETRY_TIME, API_STATUS_ASYNC_URL, API_UNLATCH_ASYNC_URL, API_UNLATCH_URL, API_UNLOCK_ASYNC_URL, API_UNLOCK_URL, HEADER_ACCEPT_VERSION, HYPER_BRIDGE_PARAM, ApiCommon, _api_headers, _convert_lock_result_to_activities, _process_activity_json, _process_doorbells_json, _process_locks_json, ) from .const import DEFAULT_BRAND from .doorbell import Doorbell, DoorbellDetail from .exceptions import AugustApiAIOHTTPError from .lock import ( Lock, LockDetail, LockDoorStatus, LockStatus, determine_door_state, determine_lock_status, ) from .pin import Pin _LOGGER = logging.getLogger(__name__) def _obscure_payload(payload: dict[str, Any]) -> dict[str, Any]: """Obscure the payload for logging.""" if payload is None: return None if "password" in payload: payload = payload.copy() payload["password"] = "****" # nosec return payload class ApiAsync(ApiCommon): """Async api.""" def __init__( self, aiohttp_session: ClientSession, timeout=10, command_timeout=60, brand=DEFAULT_BRAND, ) -> None: self._timeout = timeout self._command_timeout = command_timeout self._aiohttp_session = aiohttp_session super().__init__(brand) async def async_get_session( self, install_id: str, identifier: str, password: str ) -> ClientResponse: return await self._async_dict_to_api( self._build_get_session_request(install_id, identifier, password) ) async def async_send_verification_code( self, access_token: str, login_method: str, username: str ) -> ClientResponse: return await self._async_dict_to_api( self._build_send_verification_code_request( access_token, login_method, username ) ) async def async_validate_verification_code( self, access_token: str, login_method: str, username: str, verification_code: str, ) -> ClientResponse: return await self._async_dict_to_api( self._build_validate_verification_code_request( access_token, login_method, username, verification_code ) ) async def async_get_doorbells(self, access_token: str) -> list[Doorbell]: if not self.brand_supports_doorbells: return [] response = await self._async_dict_to_api( self._build_get_doorbells_request(access_token) ) return _process_doorbells_json(await response.json()) async def async_get_doorbell_detail( self, access_token: str, doorbell_id: str ) -> DoorbellDetail: response = await self._async_dict_to_api( self._build_get_doorbell_detail_request(access_token, doorbell_id) ) return DoorbellDetail(await response.json()) async def async_wakeup_doorbell( self, access_token: str, doorbell_id: str ) -> ClientResponse: await self._async_dict_to_api( self._build_wakeup_doorbell_request(access_token, doorbell_id) ) return True async def async_get_user(self, access_token: str) -> dict[str, Any]: response = await self._async_dict_to_api( self._build_get_user_request(access_token) ) return await response.json() async def async_get_houses(self, access_token: str) -> ClientResponse: return await self._async_dict_to_api( self._build_get_houses_request(access_token) ) async def async_get_house(self, access_token: str, house_id: str) -> dict[str, Any]: response = await self._async_dict_to_api( self._build_get_house_request(access_token, house_id) ) return await response.json() async def async_get_house_activities( self, access_token: str, house_id: str, limit: int = 8 ) -> list[ActivityTypes]: response = await self._async_dict_to_api( self._build_get_house_activities_request( access_token, house_id, limit=limit ) ) return _process_activity_json(await response.json()) async def async_get_locks(self, access_token: str) -> list[Lock]: response = await self._async_dict_to_api( self._build_get_locks_request(access_token) ) return _process_locks_json(await response.json()) async def async_get_operable_locks(self, access_token: str) -> list[Lock]: locks = await self.async_get_locks(access_token) return [lock for lock in locks if lock.is_operable] async def async_get_lock_detail( self, access_token: str, lock_id: str ) -> LockDetail: response = await self._async_dict_to_api( self._build_get_lock_detail_request(access_token, lock_id) ) return LockDetail(await response.json()) async def async_get_lock_status( self, access_token: str, lock_id: str, door_status=False ) -> LockStatus: response = await self._async_dict_to_api( self._build_get_lock_status_request(access_token, lock_id) ) json_dict = await response.json() if door_status: return ( determine_lock_status(json_dict.get("status")), determine_door_state(json_dict.get("doorState")), ) return determine_lock_status(json_dict.get("status")) async def async_get_lock_door_status( self, access_token: str, lock_id: str, lock_status=False ) -> LockDoorStatus | tuple[LockDoorStatus, LockStatus]: response = await self._async_dict_to_api( self._build_get_lock_status_request(access_token, lock_id) ) json_dict = await response.json() if lock_status: return ( determine_door_state(json_dict.get("doorState")), determine_lock_status(json_dict.get("status")), ) return determine_door_state(json_dict.get("doorState")) async def async_get_pins(self, access_token: str, lock_id: str) -> list[Pin]: response = await self._async_dict_to_api( self._build_get_pins_request(access_token, lock_id) ) json_dict = await response.json() return [Pin(pin_json) for pin_json in json_dict.get("loaded", [])] async def _async_call_lock_operation( self, url_str: str, access_token: str, lock_id: str ) -> dict[str, Any]: response = await self._async_dict_to_api( self._build_call_lock_operation_request( url_str, access_token, lock_id, self._command_timeout ) ) return await response.json() async def _async_call_async_lock_operation( self, url_str: str, access_token: str, lock_id: str ) -> str: """Call an operation that will queue.""" response = await self._async_dict_to_api( self._build_call_lock_operation_request( url_str, access_token, lock_id, self._command_timeout ) ) return await response.text() async def _async_lock(self, access_token: str, lock_id: str) -> str: return await self._async_call_lock_operation( API_LOCK_URL, access_token, lock_id ) async def async_lock(self, access_token: str, lock_id: str) -> str: """Execute a remote lock operation. Returns a LockStatus state. """ return determine_lock_status( (await self._async_lock(access_token, lock_id)).get("status") ) async def async_lock_async( self, access_token: str, lock_id: str, hyper_bridge=True ) -> str: """Queue a remote lock operation and get the response via pubnub.""" if hyper_bridge: return await self._async_call_async_lock_operation( f"{API_LOCK_ASYNC_URL}{HYPER_BRIDGE_PARAM}", access_token, lock_id ) return await self._async_call_async_lock_operation( API_LOCK_ASYNC_URL, access_token, lock_id ) async def async_lock_return_activities( self, access_token: str, lock_id: str ) -> list[ActivityTypes]: """Execute a remote lock operation. Returns an array of one or more yalexs.activity.Activity objects If the lock supports door sense one of the activities will include the current door state. """ return _convert_lock_result_to_activities( await self._async_lock(access_token, lock_id) ) async def _async_unlatch(self, access_token: str, lock_id: str) -> dict[str, Any]: return await self._async_call_lock_operation( API_UNLATCH_URL, access_token, lock_id ) async def async_unlatch(self, access_token: str, lock_id: str) -> LockStatus: """Execute a remote unlatch operation. Returns a LockStatus state. """ return determine_lock_status( (await self._async_unlatch(access_token, lock_id)).get("status") ) async def async_unlatch_async( self, access_token: str, lock_id: str, hyper_bridge=True ) -> str: """Queue a remote unlatch operation and get the response via pubnub.""" if hyper_bridge: return await self._async_call_async_lock_operation( f"{API_UNLATCH_ASYNC_URL}{HYPER_BRIDGE_PARAM}", access_token, lock_id ) return await self._async_call_async_lock_operation( API_UNLATCH_ASYNC_URL, access_token, lock_id ) async def async_unlatch_return_activities( self, access_token: str, lock_id: str ) -> list[ActivityTypes]: """Execute a remote lock operation. Returns an array of one or more yalexs.activity.Activity objects If the lock supports door sense one of the activities will include the current door state. """ return _convert_lock_result_to_activities( await self._async_unlatch(access_token, lock_id) ) async def _async_unlock(self, access_token: str, lock_id: str) -> dict[str, Any]: return await self._async_call_lock_operation( API_UNLOCK_URL, access_token, lock_id ) async def async_unlock(self, access_token: str, lock_id: str) -> LockStatus: """Execute a remote unlock operation. Returns a LockStatus state. """ return determine_lock_status( (await self._async_unlock(access_token, lock_id)).get("status") ) async def async_unlock_async( self, access_token: str, lock_id: str, hyper_bridge=True ) -> str: """Queue a remote unlock operation and get the response via pubnub.""" if hyper_bridge: return await self._async_call_async_lock_operation( f"{API_UNLOCK_ASYNC_URL}{HYPER_BRIDGE_PARAM}", access_token, lock_id ) return await self._async_call_async_lock_operation( API_UNLOCK_ASYNC_URL, access_token, lock_id ) async def async_unlock_return_activities( self, access_token: str, lock_id: str ) -> list[ActivityTypes]: """Execute a remote lock operation. Returns an array of one or more yalexs.activity.Activity objects If the lock supports door sense one of the activities will include the current door state. """ return _convert_lock_result_to_activities( await self._async_unlock(access_token, lock_id) ) async def async_status_async( self, access_token: str, lock_id: str, hyper_bridge=True ) -> str: """Queue a remote unlock operation and get the status via pubnub.""" if hyper_bridge: return await self._async_call_async_lock_operation( f"{API_STATUS_ASYNC_URL}{HYPER_BRIDGE_PARAM}", access_token, lock_id ) return await self._async_call_async_lock_operation( API_STATUS_ASYNC_URL, access_token, lock_id ) async def async_refresh_access_token(self, access_token: str) -> str: """Obtain a new api token.""" response = await self._async_dict_to_api( self._build_refresh_access_token_request(access_token) ) response_headers = response.headers access_token = ( response_headers.get(HEADER_ACCESS_TOKEN) or response_headers[HEADER_AUGUST_ACCESS_TOKEN] ) return access_token async def _async_dict_to_api(self, api_dict: dict[str, Any]) -> ClientResponse: url = api_dict.pop("url") method = api_dict.pop("method") access_token = api_dict.pop("access_token", None) payload = api_dict.get("params") or api_dict.get("json") if "headers" not in api_dict: api_dict["headers"] = _api_headers( access_token=access_token, brand=self.brand ) if "version" in api_dict: api_dict["headers"][HEADER_ACCEPT_VERSION] = api_dict["version"] del api_dict["version"] if "timeout" not in api_dict: api_dict["timeout"] = self._timeout debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) if debug_enabled: _LOGGER.debug( "About to call %s with header=%s and payload=%s", url, api_dict["headers"], _obscure_payload(payload), ) attempts = 0 while attempts < API_RETRY_ATTEMPTS: attempts += 1 try: response = await self._aiohttp_session.request(method, url, **api_dict) except ( ClientOSError, ClientSSLError, ServerDisconnectedError, ClientConnectionError, ) as ex: # Try again if we get disconnected # We may get [Errno 104] Connection reset by peer or a # transient disconnect/SSL error if attempts == API_RETRY_ATTEMPTS: raise AugustApiAIOHTTPError( f"Failed to connect to August API: {ex}", ex ) from ex await asyncio.sleep(API_EXCEPTION_RETRY_TIME) continue if debug_enabled: _LOGGER.debug( "Received API response from url: %s, code: %s, headers: %s, content: %s", url, response.status, response.headers, await response.read(), ) if response.status == 429: _LOGGER.debug( "August sent a 429 (attempt: %d), sleeping and trying again", attempts, ) await asyncio.sleep(API_RETRY_TIME) continue break _raise_response_exceptions(response) return response def _raise_response_exceptions(response: ClientResponse) -> None: """Raise exceptions for known error codes.""" try: response.raise_for_status() except ClientResponseError as err: if err.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): raise AugustApiAIOHTTPError( f"Authentication failed: Verify brand is correct: {err.message}", err ) from err if err.status == 422: raise AugustApiAIOHTTPError( f"The operation failed because the bridge (connect) is offline: {err.message}", err, ) from err if err.status == 423: raise AugustApiAIOHTTPError( f"The operation failed because the bridge (connect) is in use: {err.message}", err, ) from err if err.status == 408: raise AugustApiAIOHTTPError( f"The operation timed out because the bridge (connect) failed to respond: {err.message}", err, ) from err raise AugustApiAIOHTTPError( f"The operation failed with error code {err.status}: {err.message}.", err ) from err yalexs-8.0.2/yalexs/api_common.py000066400000000000000000000252131465672432400170460ustar00rootroot00000000000000"""Api functions common between sync and async.""" from __future__ import annotations import datetime import logging from typing import Any from .activity import ACTION_TO_CLASS, SOURCE_LOCK_OPERATE, SOURCE_LOG, ActivityTypes from .const import BASE_URLS, BRANDING, Brand, BRAND_CONFIG, DEFAULT_BRAND, BrandConfig from .doorbell import Doorbell from .lock import Lock, LockDoorStatus, determine_door_state, door_state_to_string from .time import parse_datetime from .backports.functools import cached_property API_EXCEPTION_RETRY_TIME = 0.1 API_RETRY_TIME = 2.5 API_RETRY_ATTEMPTS = 10 HEADER_ACCEPT_VERSION = "Accept-Version" HEADER_AUGUST_COUNTRY = "x-august-country" HEADER_CONTENT_TYPE = "Content-Type" HEADER_USER_AGENT = "User-Agent" HEADER_VALUE_CONTENT_TYPE = "application/json; charset=UTF-8" HEADER_VALUE_USER_AGENT = "August/Luna-22.17.0 (Android; SDK 31; gphone64_arm64)" HEADER_VALUE_ACCEPT_VERSION = "0.0.1" HEADER_VALUE_AUGUST_BRANDING = "august" HEADER_VALUE_AUGUST_COUNTRY = "US" API_GET_SESSION_URL = "/session" API_SEND_VERIFICATION_CODE_URLS = { "phone": "/validation/phone", "email": "/validation/email", } API_VALIDATE_VERIFICATION_CODE_URLS = { "phone": "/validate/phone", "email": "/validate/email", } API_GET_HOUSE_ACTIVITIES_URL = "/houses/{house_id}/activities" API_GET_DOORBELLS_URL = "/users/doorbells/mine" API_GET_DOORBELL_URL = "/doorbells/{doorbell_id}" API_WAKEUP_DOORBELL_URL = "/doorbells/{doorbell_id}/wakeup" API_GET_HOUSES_URL = "/users/houses/mine" API_GET_HOUSE_URL = "/houses/{house_id}" API_GET_LOCKS_URL = "/users/locks/mine" API_GET_LOCK_URL = "/locks/{lock_id}" API_GET_LOCK_STATUS_URL = "/locks/{lock_id}/status" API_GET_PINS_URL = "/locks/{lock_id}/pins" API_LOCK_URL = "/remoteoperate/{lock_id}/lock" API_UNLOCK_URL = "/remoteoperate/{lock_id}/unlock" API_UNLATCH_URL = "/remoteoperate/{lock_id}/unlatch" API_LOCK_ASYNC_URL = "/remoteoperate/{lock_id}/lock?v=2.3.1&type=async" API_UNLOCK_ASYNC_URL = "/remoteoperate/{lock_id}/unlock?v=2.3.1&type=async" API_UNLATCH_ASYNC_URL = "/remoteoperate/{lock_id}/unlatch?v=2.3.1&type=async" API_STATUS_ASYNC_URL = ( "/remoteoperate/{lock_id}/status?v=2.3.1&type=async&intent=wakeup" ) HYPER_BRIDGE_PARAM = "&connection=persistent" API_GET_USER_URL = "/users/me" _LOGGER = logging.getLogger(__name__) def _get_brand_config(brand: Brand) -> BrandConfig: return BRAND_CONFIG.get(brand, BRAND_CONFIG[DEFAULT_BRAND]) def _api_headers( access_token: str | None = None, brand: Brand | None = None ) -> dict[str, str]: brand_config = _get_brand_config(brand) headers = { HEADER_ACCEPT_VERSION: HEADER_VALUE_ACCEPT_VERSION, brand_config.api_key_header: brand_config.api_key, HEADER_CONTENT_TYPE: HEADER_VALUE_CONTENT_TYPE, HEADER_USER_AGENT: HEADER_VALUE_USER_AGENT, HEADER_AUGUST_COUNTRY: HEADER_VALUE_AUGUST_COUNTRY, } headers[brand_config.branding_header] = BRANDING.get( brand, HEADER_VALUE_AUGUST_BRANDING ) if access_token: headers[brand_config.access_token_header] = access_token return headers def _convert_lock_result_to_activities( lock_json_dict: dict[str, Any], ) -> list[ActivityTypes]: activities = [] lock_info_json_dict = lock_json_dict.get("info", {}) lock_id = lock_info_json_dict.get("lockID") lock_action_text = lock_info_json_dict.get("action") activity_epoch = _datetime_string_to_epoch(lock_info_json_dict.get("startTime")) activity_lock_dict = _map_lock_result_to_activity( lock_id, activity_epoch, lock_action_text ) activities.append(activity_lock_dict) door_state = determine_door_state(lock_json_dict.get("doorState")) if door_state not in (LockDoorStatus.UNKNOWN, LockDoorStatus.DISABLED): activity_door_dict = _map_lock_result_to_activity( lock_id, activity_epoch, door_state_to_string(door_state) ) activities.append(activity_door_dict) return activities def _activity_from_dict( source: str, activity_dict: dict[str, Any], debug: bool = False ) -> ActivityTypes | None: """Convert an activity dict to and Activity object.""" if debug: _LOGGER.debug("Processing activity: %s", activity_dict) if (action := activity_dict.get("action")) and ( klass := ACTION_TO_CLASS.get(action) ): return klass(source, activity_dict) if debug: _LOGGER.debug("Unknown activity: %s", activity_dict) return None def _map_lock_result_to_activity( lock_id: str, activity_epoch: float, action_text: str ) -> ActivityTypes | None: """Create an yale access activity from a lock result.""" mapped_dict = { "dateTime": activity_epoch, "deviceID": lock_id, "deviceType": "lock", "action": action_text, } return _activity_from_dict( SOURCE_LOCK_OPERATE, mapped_dict, _LOGGER.isEnabledFor(logging.DEBUG) ) def _datetime_string_to_epoch(datetime_string: str) -> datetime.datetime: return parse_datetime(datetime_string).timestamp() * 1000 def _process_activity_json(json_dict: dict[str, Any]) -> list[ActivityTypes]: if "events" in json_dict: json_dict = json_dict["events"] debug = _LOGGER.isEnabledFor(logging.DEBUG) activities: list[ActivityTypes] = [] for activity_json in json_dict: if activity := _activity_from_dict(SOURCE_LOG, activity_json, debug): activities.append(activity) return activities def _process_doorbells_json(json_dict: dict[str, Any]) -> list[Doorbell]: return [Doorbell(device_id, data) for device_id, data in json_dict.items()] def _process_locks_json(json_dict: dict[str, Any]) -> list[Lock]: return [Lock(device_id, data) for device_id, data in json_dict.items()] class ApiCommon: """Api dict shared between async and sync.""" def __init__(self, brand: Brand) -> None: """Init.""" self._base_url = BASE_URLS[brand] self.brand = brand self.brand_config = _get_brand_config(brand) @cached_property def brand_supports_doorbells(self) -> bool: """Return if the brand supports doorbells.""" return self.brand_config.supports_doorbells def get_brand_url(self, url_str: str) -> str: """Get url.""" return self._base_url + url_str def _build_get_session_request(self, install_id, identifier, password): return { "method": "post", "url": self.get_brand_url(API_GET_SESSION_URL), "json": { "installId": install_id, "identifier": identifier, "password": password, }, } def _build_send_verification_code_request( self, access_token, login_method, username ): if login_method == "phone": json = {"smsHashString": "anY0ZsRmXw+", "value": username} else: json = {"value": username} return { "method": "post", "url": self.get_brand_url(API_SEND_VERIFICATION_CODE_URLS[login_method]), "access_token": access_token, "json": json, } def _build_validate_verification_code_request( self, access_token, login_method, username, verification_code ): return { "method": "post", "url": self.get_brand_url( API_VALIDATE_VERIFICATION_CODE_URLS[login_method] ), "access_token": access_token, "json": {login_method: username, "code": str(verification_code)}, } def _build_get_doorbells_request(self, access_token): return { "method": "get", "url": self.get_brand_url(API_GET_DOORBELLS_URL), "access_token": access_token, } def _build_get_doorbell_detail_request(self, access_token, doorbell_id): return { "method": "get", "url": self.get_brand_url( API_GET_DOORBELL_URL.format(doorbell_id=doorbell_id) ), "access_token": access_token, } def _build_wakeup_doorbell_request(self, access_token, doorbell_id): return { "method": "put", "url": self.get_brand_url( API_WAKEUP_DOORBELL_URL.format(doorbell_id=doorbell_id) ), "access_token": access_token, } def _build_get_houses_request(self, access_token): return {"method": "get", "access_token": access_token} def _build_get_house_request(self, access_token, house_id): return { "method": "get", "url": self.get_brand_url(API_GET_HOUSE_URL.format(house_id=house_id)), "access_token": access_token, } def _build_get_house_activities_request(self, access_token, house_id, limit=8): return { "method": "get", "url": self.get_brand_url( API_GET_HOUSE_ACTIVITIES_URL.format(house_id=house_id) ), "version": "4.0.0", "access_token": access_token, "params": {"limit": limit}, } def _build_get_locks_request(self, access_token): return { "method": "get", "url": self.get_brand_url(API_GET_LOCKS_URL), "access_token": access_token, } def _build_get_user_request(self, access_token): return { "method": "get", "url": self.get_brand_url(API_GET_USER_URL), "access_token": access_token, } def _build_get_lock_detail_request(self, access_token, lock_id): return { "method": "get", "url": self.get_brand_url(API_GET_LOCK_URL.format(lock_id=lock_id)), "access_token": access_token, } def _build_get_lock_status_request(self, access_token, lock_id): return { "method": "get", "url": self.get_brand_url(API_GET_LOCK_STATUS_URL.format(lock_id=lock_id)), "access_token": access_token, } def _build_get_pins_request(self, access_token, lock_id): return { "method": "get", "url": self.get_brand_url(API_GET_PINS_URL.format(lock_id=lock_id)), "access_token": access_token, } def _build_refresh_access_token_request(self, access_token): return { "method": "get", "url": self.get_brand_url(API_GET_HOUSES_URL), "access_token": access_token, } def _build_call_lock_operation_request( self, url_str: str, access_token: str, lock_id: str, timeout ) -> dict[str, Any]: return { "method": "put", "url": self.get_brand_url(url_str.format(lock_id=lock_id)), "access_token": access_token, "timeout": timeout, } yalexs-8.0.2/yalexs/authenticator_async.py000066400000000000000000000121221465672432400207670ustar00rootroot00000000000000from __future__ import annotations import json import logging from datetime import datetime, timedelta, timezone import aiofiles from aiohttp import ClientError from .api_async import ApiAsync from .authenticator_common import ( Authentication, AuthenticationState, AuthenticatorCommon, ValidationResult, from_authentication_json, to_authentication_json, ) from .exceptions import AugustApiAIOHTTPError _LOGGER = logging.getLogger(__name__) class AuthenticatorAsync(AuthenticatorCommon): """Class to manage authentication with the August API.""" _api: ApiAsync async def _read_access_token_file( self, access_token_cache_file: str, file: aiofiles.threadpool.binary.AsyncBufferedIOBase, ) -> None: contents = await file.read() self._authentication = from_authentication_json(json.loads(contents)) # If token is to expire within 7 days then print a warning. if self._authentication.is_expired(): _LOGGER.error("Token has expired.") self._authentication = Authentication( AuthenticationState.REQUIRES_AUTHENTICATION, install_id=self._install_id, ) # If token is not expired but less then 7 days before it # will. elif ( self._authentication.parsed_expiration_time() - datetime.now(timezone.utc) ) < timedelta(days=7): exp_time = self._authentication.access_token_expires _LOGGER.warning( "API Token is going to expire at %s " "hours. Deleting file %s will result " "in a new token being requested next" " time", exp_time, access_token_cache_file, ) async def async_setup_authentication(self) -> None: if access_token_cache_file := self._access_token_cache_file: try: async with aiofiles.open(access_token_cache_file, "r") as file: await self._read_access_token_file(access_token_cache_file, file) return except FileNotFoundError: _LOGGER.debug("Cache file not found: %s", access_token_cache_file) except json.decoder.JSONDecodeError as error: _LOGGER.error( "Unable to read cache file (%s): %s", access_token_cache_file, error, ) self._authentication = Authentication( AuthenticationState.REQUIRES_AUTHENTICATION, install_id=self._install_id ) async def async_authenticate(self) -> Authentication: if self._authentication.state is AuthenticationState.AUTHENTICATED: return self._authentication identifier = self._login_method + ":" + self._username install_id = self._authentication.install_id response = await self._api.async_get_session( install_id, identifier, self._password ) json_dict = await response.json() authentication = self._authentication_from_session_response( install_id, response.headers, json_dict ) if authentication.state is AuthenticationState.AUTHENTICATED: await self._async_cache_authentication(authentication) return authentication async def async_validate_verification_code( self, verification_code: str ) -> ValidationResult: if not verification_code: return ValidationResult.INVALID_VERIFICATION_CODE try: await self._api.async_validate_verification_code( self._authentication.access_token, self._login_method, self._username, verification_code, ) except (AugustApiAIOHTTPError, ClientError): return ValidationResult.INVALID_VERIFICATION_CODE return ValidationResult.VALIDATED async def async_send_verification_code(self) -> bool: await self._api.async_send_verification_code( self._authentication.access_token, self._login_method, self._username ) return True async def async_refresh_access_token(self, force=False) -> Authentication | None: if not self.should_refresh() and not force: return self._authentication if self._authentication.state is not AuthenticationState.AUTHENTICATED: _LOGGER.warning("Tried to refresh access token when not authenticated") return self._authentication refreshed_token = await self._api.async_refresh_access_token( self._authentication.access_token ) authentication = self._process_refreshed_access_token(refreshed_token) await self._async_cache_authentication(authentication) return authentication async def _async_cache_authentication(self, authentication: Authentication) -> None: if self._access_token_cache_file is not None: async with aiofiles.open(self._access_token_cache_file, "w") as file: await file.write(to_authentication_json(authentication)) yalexs-8.0.2/yalexs/authenticator_common.py000066400000000000000000000125131465672432400211460ustar00rootroot00000000000000from __future__ import annotations import json import logging import uuid from datetime import datetime, timedelta, timezone from enum import Enum from typing import Any import jwt from .api_common import ApiCommon from .const import HEADER_ACCESS_TOKEN, HEADER_AUGUST_ACCESS_TOKEN from .time import parse_datetime # The default time before expiration to refresh a token DEFAULT_RENEWAL_THRESHOLD = timedelta(days=7) _LOGGER = logging.getLogger(__name__) def to_authentication_json(authentication): if authentication is None: return json.dumps({}) return json.dumps( { "install_id": authentication.install_id, "access_token": authentication.access_token, "access_token_expires": authentication.access_token_expires, "state": authentication.state.value, } ) def from_authentication_json(data): if data is None: return None install_id = data["install_id"] access_token = data["access_token"] access_token_expires = data["access_token_expires"] state = AuthenticationState(data["state"]) return Authentication(state, install_id, access_token, access_token_expires) class Authentication: def __init__( self, state, install_id=None, access_token=None, access_token_expires=None ): self._state = state self._install_id = str(uuid.uuid4()) if install_id is None else install_id self._access_token = access_token self._access_token_expires = access_token_expires self._parsed_expiration_time = None if access_token_expires: self._parsed_expiration_time = parse_datetime(access_token_expires) @property def install_id(self): return self._install_id @property def access_token(self): return self._access_token @property def access_token_expires(self): return self._access_token_expires @property def state(self): return self._state @state.setter def state(self, value): self._state = value def parsed_expiration_time(self): return self._parsed_expiration_time def is_expired(self): return self._parsed_expiration_time < datetime.now(timezone.utc) class AuthenticationState(Enum): REQUIRES_AUTHENTICATION = "requires_authentication" REQUIRES_VALIDATION = "requires_validation" AUTHENTICATED = "authenticated" BAD_PASSWORD = "bad_password" # nosec class ValidationResult(Enum): VALIDATED = "validated" INVALID_VERIFICATION_CODE = "invalid_verification_code" class AuthenticatorCommon: def __init__( self, api: ApiCommon, login_method: str | None, username: str | None, password: str | None, install_id: str | None = None, access_token_cache_file: str | None = None, access_token_renewal_threshold: timedelta = DEFAULT_RENEWAL_THRESHOLD, ) -> None: self._api = api self._login_method = login_method self._username = username self._password = password self._install_id = install_id self._access_token_cache_file = access_token_cache_file self._access_token_renewal_threshold = access_token_renewal_threshold self._authentication = None def _authentication_from_session_response( self, install_id: str, response_headers: dict[str, Any], json_dict: dict[str, Any], ) -> Authentication: access_token = ( response_headers.get(HEADER_ACCESS_TOKEN) or response_headers[HEADER_AUGUST_ACCESS_TOKEN] ) access_token_expires = json_dict["expiresAt"] v_password = json_dict["vPassword"] v_install_id = json_dict["vInstallId"] if not v_password: state = AuthenticationState.BAD_PASSWORD elif not v_install_id: state = AuthenticationState.REQUIRES_VALIDATION else: state = AuthenticationState.AUTHENTICATED self._authentication = Authentication( state, install_id, access_token, access_token_expires ) return self._authentication def should_refresh(self): return self._authentication.state == AuthenticationState.AUTHENTICATED and ( (self._authentication.parsed_expiration_time() - datetime.now(timezone.utc)) < self._access_token_renewal_threshold ) def _process_refreshed_access_token(self, refreshed_token): jwt_claims = jwt.decode(refreshed_token, options={"verify_signature": False}) if "exp" not in jwt_claims: _LOGGER.warning("Did not find expected `exp' claim in JWT") return self._authentication new_expiration = datetime.utcfromtimestamp(jwt_claims["exp"]) # The yale access api always returns expiresAt in the format # '%Y-%m-%dT%H:%M:%S.%fZ' # from the get_session api call # It is important we store access_token_expires formatted # the same way for compatibility self._authentication = Authentication( self._authentication.state, install_id=self._authentication.install_id, access_token=refreshed_token, access_token_expires=new_expiration.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), ) _LOGGER.info("Successfully refreshed access token") return self._authentication yalexs-8.0.2/yalexs/backports/000077500000000000000000000000001465672432400163405ustar00rootroot00000000000000yalexs-8.0.2/yalexs/backports/__init__.py000066400000000000000000000000001465672432400204370ustar00rootroot00000000000000yalexs-8.0.2/yalexs/backports/enum.py000066400000000000000000000017751465672432400176700ustar00rootroot00000000000000"""Enum backports from standard lib.""" from __future__ import annotations from enum import Enum from typing import Any from typing_extensions import Self class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" def __new__(cls, value: str, *args: Any, **kwargs: Any) -> Self: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") return super().__new__(cls, value, *args, **kwargs) def __str__(self) -> str: """Return self.value.""" return str(self.value) @staticmethod def _generate_next_value_( name: str, start: int, count: int, last_values: list[Any] ) -> Any: """Make `auto()` explicitly unsupported. We may revisit this when it's very clear that Python 3.11's `StrEnum.auto()` behavior will no longer change. """ raise TypeError("auto() is not supported by this implementation") yalexs-8.0.2/yalexs/backports/functools.py000066400000000000000000000045051465672432400207320ustar00rootroot00000000000000"""Functools backports from standard lib.""" from __future__ import annotations from collections.abc import Callable from types import GenericAlias from typing import Any, Generic, TypeVar, overload _T = TypeVar("_T") class cached_property(Generic[_T]): # pylint: disable=invalid-name """Backport of Python 3.12's cached_property. Includes https://github.com/python/cpython/pull/101890/files """ def __init__(self, func: Callable[[Any], _T]) -> None: """Initialize.""" self.func: Callable[[Any], _T] = func self.attrname: str | None = None self.__doc__ = func.__doc__ def __set_name__(self, owner: type[Any], name: str) -> None: """Set name.""" if self.attrname is None: self.attrname = name elif name != self.attrname: raise TypeError( "Cannot assign the same cached_property to two different names " f"({self.attrname!r} and {name!r})." ) @overload def __get__( # noqa: E704 self, instance: None, owner: type[Any] | None = None ) -> Any: ... @overload def __get__( # noqa: E704 self, instance: Any, owner: type[Any] | None = None ) -> _T: ... def __get__(self, instance: Any | None, owner: type[Any] | None = None) -> _T | Any: """Get.""" if instance is None: return self if self.attrname is None: raise TypeError( "Cannot use cached_property instance without calling __set_name__ on it." ) try: cache = instance.__dict__ # not all objects have __dict__ (e.g. class defines slots) except AttributeError: msg = ( f"No '__dict__' attribute on {type(instance).__name__!r} " f"instance to cache {self.attrname!r} property." ) raise TypeError(msg) from None val = self.func(instance) try: cache[self.attrname] = val except TypeError: msg = ( f"The '__dict__' attribute on {type(instance).__name__!r} instance " f"does not support item assignment for caching {self.attrname!r} property." ) raise TypeError(msg) from None return val __class_getitem__ = classmethod(GenericAlias) yalexs-8.0.2/yalexs/backports/tasks.py000066400000000000000000000017031465672432400200400ustar00rootroot00000000000000from __future__ import annotations import sys from asyncio import AbstractEventLoop, Task, get_running_loop from collections.abc import Coroutine from typing import Any, TypeVar _T = TypeVar("_T") if sys.version_info >= (3, 12, 0): def create_eager_task( coro: Coroutine[Any, Any, _T], *, name: str | None = None, loop: AbstractEventLoop | None = None, ) -> Task[_T]: """Create a task from a coroutine and schedule it to run immediately.""" return Task(coro, loop=loop or get_running_loop(), name=name, eager_start=True) else: def create_eager_task( coro: Coroutine[Any, Any, _T], *, name: str | None = None, loop: AbstractEventLoop | None = None, ) -> Task[_T]: """Create a task from a coroutine and schedule it to run immediately.""" return Task( coro, loop=loop or get_running_loop(), name=name, ) yalexs-8.0.2/yalexs/bridge.py000066400000000000000000000042561465672432400161650ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from typing import Any from .backports.functools import cached_property from .device import DeviceDetail class BridgeStatus(Enum): OFFLINE = "offline" ONLINE = "online" UNKNOWN = "unknown" class BridgeDetail(DeviceDetail): """Represents a bridge device.""" def __init__(self, house_id: str, data: dict[str, Any]) -> None: """Initialize the bridge device.""" super().__init__( data["_id"], None, house_id, None, data["firmwareVersion"], None, data ) self._hyper_bridge = data.get("hyperBridge", False) self._operative = data["operative"] if "status" in data: self._status = BridgeStatusDetail(data["status"]) else: self._status = None @property def status(self): return self._status @cached_property def hyper_bridge(self): return self._hyper_bridge @cached_property def operative(self): return self._operative def set_online(self, state): """Called when the bridge online state changes.""" self._status.set_online(state) class BridgeStatusDetail: """Represents the status of a bridge device.""" def __init__(self, data: dict[str, Any]) -> None: """Initialize the bridge status.""" self._current = BridgeStatus.UNKNOWN if "current" in data and data["current"] == "online": self._current = BridgeStatus.ONLINE self._updated = data["updated"] if "updated" in data else None self._last_online = data["lastOnline"] if "lastOnline" in data else None self._last_offline = data["lastOffline"] if "lastOffline" in data else None @property def current(self): return self._current def set_online(self, state): """Called when the bridge online state changes.""" self._current = BridgeStatus.ONLINE if state else BridgeStatus.OFFLINE @cached_property def updated(self): return self._updated @cached_property def last_online(self): return self._last_online @cached_property def last_offline(self): return self._last_offline yalexs-8.0.2/yalexs/const.py000066400000000000000000000075171465672432400160620ustar00rootroot00000000000000"""Constants.""" from .backports.enum import StrEnum from dataclasses import dataclass class Brand(StrEnum): AUGUST = "august" YALE_ACCESS = "yale_access" YALE_HOME = "yale_home" YALE_GLOBAL = "yale_global" DEFAULT_BRAND = Brand.AUGUST BASE_URLS = { Brand.AUGUST: "https://api-production.august.com", Brand.YALE_ACCESS: "https://api-production.august.com", Brand.YALE_HOME: "https://api.aaecosystem.com", Brand.YALE_GLOBAL: "https://api.aaecosystem.com", } BRANDS = { Brand.AUGUST: "August", Brand.YALE_ACCESS: "Yale Access", Brand.YALE_HOME: "Yale Home", Brand.YALE_GLOBAL: "Yale Global", } BRANDING = { Brand.AUGUST: "august", Brand.YALE_ACCESS: "yale", Brand.YALE_HOME: "yale", Brand.YALE_GLOBAL: "yale", } @dataclass class BrandConfig: """Brand configuration.""" name: str branding: str access_token_header: str api_key_header: str branding_header: str api_key: str supports_doorbells: bool HEADER_VALUE_API_KEY_OLD = "7cab4bbd-2693-4fc1-b99b-dec0fb20f9d4" HEADER_VALUE_API_KEY = "d9984f29-07a6-816e-e1c9-44ec9d1be431" HEADER_AUGUST_ACCESS_TOKEN = "x-august-access-token" # nosec HEADER_AUGUST_API_KEY = "x-august-api-key" # nosec HEADER_AUGUST_BRANDING = "x-august-branding" HEADER_ACCESS_TOKEN = "x-access-token" # nosec HEADER_API_KEY = "x-api-key" # nosec HEADER_BRANDING = "x-branding" BRAND_CONFIG: dict[Brand, BrandConfig] = { Brand.AUGUST: BrandConfig( name="August", branding="august", access_token_header=HEADER_AUGUST_ACCESS_TOKEN, api_key_header=HEADER_AUGUST_API_KEY, branding_header=HEADER_AUGUST_BRANDING, api_key=HEADER_VALUE_API_KEY, supports_doorbells=True, ), Brand.YALE_ACCESS: BrandConfig( name="Yale Access", branding="yale", access_token_header=HEADER_AUGUST_ACCESS_TOKEN, api_key_header=HEADER_AUGUST_API_KEY, branding_header=HEADER_AUGUST_BRANDING, api_key=HEADER_VALUE_API_KEY, supports_doorbells=True, ), Brand.YALE_HOME: BrandConfig( name="Yale Home", branding="yale", access_token_header=HEADER_ACCESS_TOKEN, api_key_header=HEADER_API_KEY, branding_header=HEADER_BRANDING, api_key=HEADER_VALUE_API_KEY, supports_doorbells=True, ), Brand.YALE_GLOBAL: BrandConfig( name="Yale Global", branding="yale", access_token_header=HEADER_ACCESS_TOKEN, api_key_header=HEADER_API_KEY, branding_header=HEADER_BRANDING, # Sadly we currently do not have a way to avoid # having the API key in the code because it must # run on the user's device api_key="d16a1029-d823-4b55-a4ce-a769a9b56f0e", supports_doorbells=False, ), } PUBNUB_TOKENS = { Brand.AUGUST: { "subscribe": "sub-c-1030e062-0ebe-11e5-a5c2-0619f8945a4f", "publish": "pub-c-567d7f2d-270a-438a-a785-f0af12ad8312", }, Brand.YALE_ACCESS: { "subscribe": "sub-c-1030e062-0ebe-11e5-a5c2-0619f8945a4f", "publish": "pub-c-567d7f2d-270a-438a-a785-f0af12ad8312f", }, Brand.YALE_HOME: { "subscribe": "sub-c-c9c38d4d-5796-46c9-9262-af20cf6a1d42", "publish": "pub-c-353e8881-cf58-4b26-9baf-96f296de0677", }, # YALE_GLOBAL will eventually WebSockets for push updates # but for now we will use the same as YALE_HOME until we # can finish the WebSockets implementation Brand.YALE_GLOBAL: { "subscribe": "sub-c-c9c38d4d-5796-46c9-9262-af20cf6a1d42", "publish": "pub-c-353e8881-cf58-4b26-9baf-96f296de0677", }, } CONFIGURATION_URLS = { Brand.AUGUST: "https://account.august.com", Brand.YALE_ACCESS: "https://account.august.com", Brand.YALE_HOME: "https://account.aaecosystem.com", Brand.YALE_GLOBAL: "https://account.aaecosystem.com", } yalexs-8.0.2/yalexs/device.py000066400000000000000000000032651465672432400161670ustar00rootroot00000000000000from __future__ import annotations from typing import Any from .backports.functools import cached_property class Device: """Base class for all devices.""" def __init__(self, device_id: str, device_name: str, house_id: str) -> None: self._device_id = device_id self._device_name = device_name self._house_id = house_id @cached_property def device_id(self) -> str: return self._device_id @cached_property def device_name(self) -> str: return self._device_name @cached_property def house_id(self) -> str: return self._house_id class DeviceDetail: def __init__( self, device_id: str, device_name: str, house_id: str, serial_number: str, firmware_version: str, pubsub_channel: str, data: dict[str, Any], ) -> None: self._device_id = device_id self._device_name = device_name self._house_id = house_id self._serial_number = serial_number self._firmware_version = firmware_version self._pubsub_channel = pubsub_channel self._data = data @cached_property def raw(self): return self._data @cached_property def device_id(self): return self._device_id @cached_property def device_name(self): return self._device_name @cached_property def house_id(self): return self._house_id @cached_property def serial_number(self): return self._serial_number @cached_property def firmware_version(self): return self._firmware_version @cached_property def pubsub_channel(self): return self._pubsub_channel yalexs-8.0.2/yalexs/doorbell.py000066400000000000000000000141401465672432400165240ustar00rootroot00000000000000from __future__ import annotations import datetime import logging from typing import Any import requests from aiohttp import ClientSession from yalexs.exceptions import ContentTokenExpired from .backports.functools import cached_property from .device import Device, DeviceDetail from .time import parse_datetime _LOGGER = logging.getLogger(__name__) DOORBELL_STATUS_KEY = "status" class Doorbell(Device): """Class to hold details about a doorbell.""" def __init__(self, device_id: str, data: dict[str, Any]) -> None: _LOGGER.info("Doorbell init - %s", data["name"]) super().__init__(device_id, data["name"], data["HouseID"]) self._serial_number = data["serialNumber"] self._status = data["status"] recent_image = data.get("recentImage", {}) self._image_url = recent_image.get("secure_url", None) self._has_subscription = data.get("dvrSubscriptionSetupDone", False) self._content_token = data.get("contentToken", "") @cached_property def serial_number(self): return self._serial_number @cached_property def status(self): return self._status @cached_property def is_standby(self): return self.status == "standby" @cached_property def is_online(self): return self.status == "doorbell_call_status_online" @cached_property def image_url(self): return self._image_url @cached_property def has_subscription(self): return self._has_subscription @cached_property def content_token(self): return self._content_token def __repr__(self): return "Doorbell(id={}, name={}, house_id={})".format( self.device_id, self.device_name, self.house_id ) class DoorbellDetail(DeviceDetail): """Class to hold details about a doorbell.""" def __init__(self, data: dict[str, Any]) -> None: super().__init__( data["doorbellID"], data["name"], data["HouseID"], data["serialNumber"], data["firmwareVersion"], data.get("pubsubChannel"), data, ) self._status: str = data["status"] recent_image: dict[str, Any] = data.get("recentImage", {}) self._image_url: str | None = recent_image.get("secure_url") self._has_subscription: bool = data.get("dvrSubscriptionSetupDone", False) self._image_created_at_datetime: datetime.datetime | None = None self._model: str | int | None = None self._content_token: str = data.get("contentToken", "") if "type" in data: self._model = data["type"] if "created_at" in recent_image: self._image_created_at_datetime: datetime.datetime = parse_datetime( recent_image["created_at"] ) self._battery_level: int | None = None if "telemetry" in data: telemetry = data["telemetry"] if "battery_soc" in telemetry: self._battery_level = telemetry.get("battery_soc", None) elif telemetry.get("doorbell_low_battery"): self._battery_level = 10 elif "battery" in telemetry: battery = telemetry["battery"] if battery >= 4: self._battery_level = 100 elif battery >= 3.75: self._battery_level = 75 elif battery >= 3.50: self._battery_level = 50 else: self._battery_level = 25 @cached_property def status(self) -> str: return self._status @cached_property def model(self) -> str | int | None: return self._model @cached_property def is_online(self) -> bool: return self.status == "doorbell_call_status_online" @cached_property def is_standby(self) -> bool: return self.status == "standby" @property def image_created_at_datetime(self) -> datetime.datetime | datetime.date | None: return self._image_created_at_datetime @image_created_at_datetime.setter def image_created_at_datetime(self, var: datetime.date): """Update the doorbell image created_at datetime (usually form the activity log).""" if not isinstance(var, datetime.date): raise ValueError self._image_created_at_datetime = var @property def image_url(self) -> str | None: return self._image_url @property def content_token(self) -> str: return self._content_token @image_url.setter def image_url(self, var: str | None) -> None: """Update the doorbell image url (usually form the activity log).""" _LOGGER.debug("image_url updated for %s", self.device_name) self._image_url = var @content_token.setter def content_token(self, var: str) -> None: _LOGGER.debug("content_token updated for %s", self.device_name) self._content_token = var or "" @cached_property def battery_level(self) -> int | None: """Return an approximation of the battery percentage.""" return self._battery_level @cached_property def has_subscription(self) -> bool: return self._has_subscription async def async_get_doorbell_image( self, aiohttp_session: ClientSession, timeout: float = 10.0 ) -> bytes: _LOGGER.debug("async_get_doorbell_image %s", self.device_name) response = await aiohttp_session.request( "get", self._image_url, timeout=timeout, headers={"Authorization": self._content_token or ""}, ) if response.status == 401: _LOGGER.debug( "snapshot get error %s, may need new content token", response.status ) raise ContentTokenExpired return await response.read() def get_doorbell_image(self, timeout: float = 10.0) -> bytes: _LOGGER.debug("get_doorbell_image sync %s", self.device_name) return requests.get( self._image_url, timeout=timeout, headers={"Authorization": self._content_token or ""}, ).content yalexs-8.0.2/yalexs/exceptions.py000066400000000000000000000016271465672432400171110ustar00rootroot00000000000000from __future__ import annotations from http import HTTPStatus from aiohttp import ClientError, ClientResponseError from requests.exceptions import HTTPError class AugustApiAIOHTTPError(Exception): """An yale access api error with a friendly user consumable string.""" def __init__(self, message: str, aiohttp_client_error: ClientError) -> None: """Initialize the error.""" super().__init__(message) self.status = ( isinstance(aiohttp_client_error, ClientResponseError) and aiohttp_client_error.status ) self.auth_failed = self.status in ( HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, ) class AugustApiHTTPError(HTTPError): """An yale access api error with a friendly user consumable string.""" class ContentTokenExpired(Exception): """Token required for accessing this resource is not valid.""" yalexs-8.0.2/yalexs/keypad.py000066400000000000000000000024111465672432400161750ustar00rootroot00000000000000from .device import DeviceDetail from .backports.functools import cached_property BATTERY_LEVEL_FULL = "Full" BATTERY_LEVEL_MEDIUM = "Medium" BATTERY_LEVEL_LOW = "Low" _LEVEL_TO_VALUE = { BATTERY_LEVEL_FULL: 100, BATTERY_LEVEL_MEDIUM: 60, BATTERY_LEVEL_LOW: 10, } _MAX_LEVEL = 200 _MIN_LEVEL = 120 _RATIO = 100 / (_MAX_LEVEL - _MIN_LEVEL) class KeypadDetail(DeviceDetail): def __init__(self, house_id, keypad_name, data): super().__init__( data["_id"], keypad_name, house_id, data["serialNumber"], data["currentFirmwareVersion"], None, data, ) self._battery_raw = data.get("batteryRaw") self._battery_level = data["batteryLevel"] @cached_property def model(self): return "AK-R1" @cached_property def battery_level(self): return self._battery_level @cached_property def battery_percentage(self): """Return an approximation of the battery percentage.""" if self._battery_raw is not None: return int(max(0, min(100, (self._battery_raw - _MIN_LEVEL) * _RATIO))) if self._battery_level is None: return None return _LEVEL_TO_VALUE.get(self._battery_level, 0) yalexs-8.0.2/yalexs/lock.py000066400000000000000000000210631465672432400156540ustar00rootroot00000000000000from __future__ import annotations import datetime from enum import Enum from .backports.functools import cached_property from .bridge import BridgeDetail, BridgeStatus from .device import Device, DeviceDetail from .keypad import KeypadDetail from .time import parse_datetime from .users import cache_user_info LOCKED_STATUS = ("locked", "kAugLockState_Locked", "kAugLockState_SecureMode") LOCKING_STATUS = ("kAugLockState_Locking",) UNLATCHED_STATUS = ("unlatched", "kAugLockState_Unlatched") UNLATCHING_STATUS = ("kAugLockState_Unlatching",) UNLOCKED_STATUS = ("unlocked", "kAugLockState_Unlocked") UNLOCKING_STATUS = ("kAugLockState_Unlocking",) JAMMED_STATUS = ( "kAugLockState_UnknownStaticPosition", "FAILED_BRIDGE_ERROR_LOCK_JAMMED", ) CLOSED_STATUS = ("closed", "kAugLockDoorState_Closed", "kAugDoorState_Closed") OPEN_STATUS = ("open", "kAugLockDoorState_Open", "kAugDoorState_Open") DISABLE_STATUS = ("init", "kAugDoorState_Init", "", None) LOCK_STATUS_KEY = "status" DOOR_STATE_KEY = "doorState" DOORMAN_MODEL_TYPES = {7, 10} UNLATCH_MODEL_TYPES = {17} # Type 7 is a doorman # Type 10 is a doorman in the Swedish market # Type 17 is a Linus L2 class Lock(Device): def __init__(self, device_id, data): super().__init__( device_id, data["LockName"], data["HouseID"], ) self._user_type = data["UserType"] @cached_property def is_operable(self): return self._user_type == "superuser" def __repr__(self): return "Lock(id={}, name={}, house_id={})".format( self.device_id, self.device_name, self.house_id ) class LockDetail(DeviceDetail): def __init__(self, data): super().__init__( data["LockID"], data["LockName"], data["HouseID"], data["SerialNumber"], data["currentFirmwareVersion"], data.get("pubsubChannel"), data, ) if "Bridge" in data: self._bridge = BridgeDetail(self.house_id, data["Bridge"]) else: self._bridge = None self._doorsense = False self._lock_status = LockStatus.UNKNOWN self._door_state = LockDoorStatus.UNKNOWN self._lock_status_datetime = None self._door_state_datetime = None self._model = None if "LockStatus" in data: lock_status = data["LockStatus"] self._lock_status = determine_lock_status(lock_status.get(LOCK_STATUS_KEY)) self._door_state = determine_door_state(lock_status.get(DOOR_STATE_KEY)) if "dateTime" in lock_status: self._lock_status_datetime = parse_datetime(lock_status["dateTime"]) self._door_state_datetime = self._lock_status_datetime if ( DOOR_STATE_KEY in lock_status and self._door_state != LockDoorStatus.DISABLED ): self._doorsense = True if "keypad" in data: keypad_name = data["LockName"] + " Keypad" self._keypad_detail = KeypadDetail( self.house_id, keypad_name, data["keypad"] ) else: self._keypad_detail = None self._battery_level = int(100 * data["battery"]) if "users" in data: for uuid, user_data in data["users"].items(): cache_user_info(uuid, user_data) if "skuNumber" in data: self._model = data["skuNumber"] self._data = data @cached_property def model(self): return self._model @cached_property def doorbell(self) -> bool: return self._data["Type"] in DOORMAN_MODEL_TYPES @cached_property def unlatch_supported(self) -> bool: return self._data["Type"] in UNLATCH_MODEL_TYPES @cached_property def battery_level(self): return self._battery_level @cached_property def keypad(self): return self._keypad_detail @cached_property def bridge(self): return self._bridge @property def bridge_is_online(self): if self._bridge is None: return False # Old style bridge that does not report current status # This may have been updated but I do not have a Gen2 # doorbell to test with yet. if self._bridge.status is None and self._bridge.operative: return True if ( self._bridge.status is not None and self._bridge.status.current == BridgeStatus.ONLINE ): return True return False @cached_property def doorsense(self): return self._doorsense @property def lock_status(self): return self._lock_status @lock_status.setter def lock_status(self, var): """Update the lock status (usually form the activity log).""" if var not in LockStatus: raise ValueError self._lock_status = var @property def lock_status_datetime(self): return self._lock_status_datetime @lock_status_datetime.setter def lock_status_datetime(self, var): """Update the lock status datetime (usually form the activity log).""" if not isinstance(var, datetime.date): raise ValueError self._lock_status_datetime = var @property def door_state(self): return self._door_state @door_state.setter def door_state(self, var): """Update the door state (usually form the activity log).""" if var not in LockDoorStatus: raise ValueError self._door_state = var if var != LockDoorStatus.UNKNOWN: self._doorsense = True @property def door_state_datetime(self): return self._door_state_datetime @door_state_datetime.setter def door_state_datetime(self, var): """Update the door state datetime (usually form the activity log).""" if not isinstance(var, datetime.date): raise ValueError self._door_state_datetime = var def set_online(self, state): """Called when the lock comes back online or goes offline.""" if not self._bridge: return self._bridge.set_online(state) def get_user(self, user_id): """Lookup user data by id.""" return self._data.get("users", {}).get(user_id) @cached_property def offline_keys(self) -> dict: return self._data.get("OfflineKeys", {}) @cached_property def loaded_offline_keys(self) -> list[dict]: return self.offline_keys.get("loaded", []) @cached_property def offline_key(self) -> str | None: loaded_offline_keys = self.loaded_offline_keys if loaded_offline_keys and "key" in loaded_offline_keys[0]: return loaded_offline_keys[0]["key"] return None @cached_property def offline_slot(self) -> int | None: loaded_offline_keys = self.loaded_offline_keys if loaded_offline_keys and "slot" in loaded_offline_keys[0]: return loaded_offline_keys[0]["slot"] return None @cached_property def mac_address(self) -> str | None: mac = self._data.get("macAddress") return mac.upper() if mac else None class LockStatus(Enum): LOCKED = "locked" UNLATCHED = "unlatched" UNLOCKED = "unlocked" UNKNOWN = "unknown" LOCKING = "locking" UNLATCHING = "unlatching" UNLOCKING = "unlocking" JAMMED = "jammed" class LockDoorStatus(Enum): CLOSED = "closed" OPEN = "open" UNKNOWN = "unknown" DISABLED = "disabled" def determine_lock_status(status): if status in LOCKED_STATUS: return LockStatus.LOCKED if status in UNLATCHED_STATUS: return LockStatus.UNLATCHED if status in UNLOCKED_STATUS: return LockStatus.UNLOCKED if status in UNLATCHING_STATUS: return LockStatus.UNLATCHING if status in UNLOCKING_STATUS: return LockStatus.UNLOCKING if status in LOCKING_STATUS: return LockStatus.LOCKING if status in JAMMED_STATUS: return LockStatus.JAMMED return LockStatus.UNKNOWN def determine_door_state(status): if status in CLOSED_STATUS: return LockDoorStatus.CLOSED if status in OPEN_STATUS: return LockDoorStatus.OPEN if status in DISABLE_STATUS: return LockDoorStatus.DISABLED return LockDoorStatus.UNKNOWN def door_state_to_string(door_status): """Returns the normalized value that determine_door_state represents.""" if door_status == LockDoorStatus.OPEN: return "dooropen" if door_status == LockDoorStatus.CLOSED: return "doorclosed" raise ValueError yalexs-8.0.2/yalexs/manager/000077500000000000000000000000001465672432400157625ustar00rootroot00000000000000yalexs-8.0.2/yalexs/manager/__init__.py000066400000000000000000000000431465672432400200700ustar00rootroot00000000000000from __future__ import annotations yalexs-8.0.2/yalexs/manager/activity.py000066400000000000000000000207421465672432400201750ustar00rootroot00000000000000"""Consume the august activity stream.""" from __future__ import annotations import asyncio import logging from collections import defaultdict from time import monotonic from aiohttp import ClientError from ..activity import Activity, ActivityType from ..api_async import ApiAsync from ..backports.tasks import create_eager_task from ..pubnub_async import AugustPubNub from ..util import get_latest_activity from .const import ACTIVITY_UPDATE_INTERVAL from .gateway import Gateway from .subscriber import SubscriberMixin _LOGGER = logging.getLogger(__name__) ACTIVITY_STREAM_FETCH_LIMIT = 10 ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 INITIAL_LOCK_RESYNC_TIME = 60 # If there is a storm of activity (ie lock, unlock, door open, door close, etc) # we want to debounce the updates so we don't hammer the activity api too much. ACTIVITY_DEBOUNCE_COOLDOWN = 4 def _async_cancel_future_scheduled_updates(cancels: list[asyncio.TimerHandle]) -> None: """Cancel future scheduled updates.""" for cancel in cancels: cancel.cancel() cancels.clear() class ActivityStream(SubscriberMixin): """August activity stream handler.""" def __init__( self, api: ApiAsync, august_gateway: Gateway, house_ids: set[str], pubnub: AugustPubNub, ) -> None: """Init activity stream object.""" super().__init__(ACTIVITY_UPDATE_INTERVAL) self._schedule_updates: dict[str, list[asyncio.TimerHandle]] = defaultdict(list) self._august_gateway = august_gateway self._api = api self._house_ids = house_ids self._latest_activities: defaultdict[ str, dict[ActivityType, Activity | None] ] = defaultdict(lambda: defaultdict(lambda: None)) self._did_first_update = False self.pubnub = pubnub self._update_tasks: dict[str, asyncio.Task] = {} self._start_time: float | None = None self._loop = asyncio.get_running_loop() self._shutdown: bool = False async def async_setup(self) -> None: """Token refresh check and catch up the activity stream.""" self._start_time = monotonic() await self._async_refresh() self._did_first_update = True def async_stop(self) -> None: """Cleanup any debounces.""" self._shutdown = True for task in self._update_tasks.values(): task.cancel() self._update_tasks.clear() self._async_cancel_future_updates() def _async_cancel_future_updates(self) -> None: """Cancel future updates.""" for cancels in self._schedule_updates.values(): _async_cancel_future_scheduled_updates(cancels) def get_latest_device_activity( self, device_id: str, activity_types: set[ActivityType] ) -> Activity | None: """Return latest activity that is one of the activity_types.""" if not (latest_device_activities := self._latest_activities.get(device_id)): return None latest_activity: Activity | None = None for activity_type in activity_types: if activity := latest_device_activities.get(activity_type): if ( latest_activity and activity.activity_start_time <= latest_activity.activity_start_time ): continue latest_activity = activity return latest_activity @property def push_updates_connected(self) -> bool: """Return if the push updates are connected.""" return self.pubnub.connected async def _async_refresh(self) -> None: """Update the activity stream from August.""" # This is the only place we refresh the api token await self._august_gateway.async_refresh_access_token_if_needed() if self.push_updates_connected: _LOGGER.debug("Skipping update because pubnub is connected") return _LOGGER.debug("Start retrieving device activities") # Await in sequence to avoid hammering the API for house_id in self._house_ids: if ( current_task := self._update_tasks.get(house_id) ) and not current_task.done(): continue await self._async_update_house_id(house_id) def _async_future_update(self, house_id: str) -> None: """Update the activity stream from August in the future.""" if self._shutdown: return if ( current_task := self._update_tasks.get(house_id) ) and not current_task.done(): self._loop.call_later( ACTIVITY_DEBOUNCE_COOLDOWN, self._async_future_update, house_id ) return self._update_tasks[house_id] = create_eager_task( self._async_update_house_id(house_id), loop=self._loop ) def async_schedule_house_id_refresh(self, house_id: str) -> None: """Update for a house activities now and once in the future.""" self._async_cancel_future_updates() self._async_future_update(house_id) # Schedule two updates past the debounce time # to ensure we catch the case where the activity # api does not update right away and we need to poll # it again. Sometimes the lock operator or a doorbell # will not show up in the activity stream right away. # Only do additional polls if we are past # the initial lock resync time to avoid a storm # of activity at setup. if ( not self._start_time or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME ): _LOGGER.debug( "Skipping additional updates due to ongoing initial lock resync time" ) return _LOGGER.debug("Scheduling additional updates for house id %s", house_id) self._schedule_updates[house_id].extend( self._loop.call_later( (step * ACTIVITY_DEBOUNCE_COOLDOWN) + 0.1, self._async_future_update, house_id, ) for step in (1, 2) ) async def _async_update_house_id(self, house_id: str) -> None: """Update device activities for a house.""" if self._shutdown: return if self._did_first_update: limit = ACTIVITY_STREAM_FETCH_LIMIT else: limit = ACTIVITY_CATCH_UP_FETCH_LIMIT _LOGGER.debug("Updating device activity for house id %s", house_id) try: activities = await self._api.async_get_house_activities( await self._august_gateway.async_get_access_token(), house_id, limit=limit, ) except ClientError as ex: _LOGGER.error( "Request error trying to retrieve activity for house id %s: %s", house_id, ex, ) # Make sure we process the next house if one of them fails return _LOGGER.debug( "Completed retrieving device activities for house id %s", house_id ) for device_id in self.async_process_newer_device_activities(activities): _LOGGER.debug( "async_signal_device_id_update (from activity stream): %s", device_id, ) self.async_signal_device_id_update(device_id) def async_process_newer_device_activities( self, activities: list[Activity] ) -> set[str]: """Process activities if they are newer than the last one.""" updated_device_ids: set[str] = set() latest_activities = self._latest_activities for activity in activities: device_id = activity.device_id activity_type = activity.activity_type device_activities = latest_activities[device_id] # Ignore activities that are older than the latest one unless it is a non # locking or unlocking activity with the exact same start time. last_activity = device_activities[activity_type] # The activity stream can have duplicate activities. So we need # to call get_latest_activity to figure out if if the activity # is actually newer than the last one. latest_activity = get_latest_activity(activity, last_activity) if latest_activity != activity: continue device_activities[activity_type] = activity updated_device_ids.add(device_id) return updated_device_ids yalexs-8.0.2/yalexs/manager/const.py000066400000000000000000000015221465672432400174620ustar00rootroot00000000000000"""Constants.""" from __future__ import annotations from datetime import timedelta from typing import Final DEFAULT_TIMEOUT = 25 CONF_USERNAME: Final = "username" CONF_PASSWORD: Final = "password" CONF_TIMEOUT: Final = "timeout" CONF_ACCESS_TOKEN_CACHE_FILE: Final = "access_token_cache_file" CONF_BRAND: Final = "brand" CONF_LOGIN_METHOD: Final = "login_method" CONF_INSTALL_ID: Final = "install_id" VERIFICATION_CODE_KEY: Final = "verification_code" DEFAULT_AUGUST_CONFIG_FILE: Final = ".august.conf" # Activity needs to be checked more frequently as the # doorbell motion and rings are included here ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10) # Limit battery, online, and hardware updates to hourly # in order to reduce the number of api requests and # avoid hitting rate limits MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24) yalexs-8.0.2/yalexs/manager/data.py000066400000000000000000000435171465672432400172570ustar00rootroot00000000000000"""Support for August devices.""" from __future__ import annotations import asyncio import logging from abc import abstractmethod from collections.abc import Callable, Coroutine, Iterable, ValuesView from contextlib import suppress from datetime import datetime from itertools import chain from typing import Any, ParamSpec, TypeVar from aiohttp import ClientError, ClientResponseError, ClientSession from ..activity import ActivityTypes from ..backports.tasks import create_eager_task from ..const import Brand from ..doorbell import ContentTokenExpired, Doorbell, DoorbellDetail from ..exceptions import AugustApiAIOHTTPError from ..lock import Lock, LockDetail from ..pubnub_activity import activities_from_pubnub_message from ..pubnub_async import AugustPubNub, async_create_pubnub from .activity import ActivityStream from .const import MIN_TIME_BETWEEN_DETAIL_UPDATES from .exceptions import CannotConnect, YaleXSError from .gateway import Gateway from .subscriber import SubscriberMixin _LOGGER = logging.getLogger(__name__) API_CACHED_ATTRS = { "door_state", "door_state_datetime", "lock_status", "lock_status_datetime", } YALEXS_BLE_DOMAIN = "yalexs_ble" _R = TypeVar("_R") _P = ParamSpec("_P") def _save_live_attrs(lock_detail: DoorbellDetail | LockDetail) -> dict[str, Any]: """Store the attributes that the lock detail api may have an invalid cache for. Since we are connected to pubnub we may have more current data then the api so we want to restore the most current data after updating battery state etc. """ return {attr: getattr(lock_detail, attr) for attr in API_CACHED_ATTRS} def _restore_live_attrs( lock_detail: DoorbellDetail | LockDetail, attrs: dict[str, Any] ) -> None: """Restore the non-cache attributes after a cached update.""" for attr, value in attrs.items(): setattr(lock_detail, attr, value) class YaleXSData(SubscriberMixin): """YaleXS Data coordinator object.""" def __init__( self, gateway: Gateway, error_exception_class: Exception = YaleXSError ) -> None: """Init August data object.""" super().__init__(MIN_TIME_BETWEEN_DETAIL_UPDATES) self._gateway = gateway self.activity_stream: ActivityStream = None self._api = gateway.api self._device_detail_by_id: dict[str, LockDetail | DoorbellDetail] = {} self._doorbells_by_id: dict[str, Doorbell] = {} self._locks_by_id: dict[str, Lock] = {} self._house_ids: set[str] = set() self._pubnub_unsub: Callable[[], Coroutine[Any, Any, None]] | None = None self._initial_sync_task: asyncio.Task | None = None self._error_exception_class = error_exception_class @property def brand(self) -> Brand: """Return the brand of the API.""" return self._gateway.api.brand async def async_setup(self) -> None: """Async setup of august device data and activities.""" token = await self._gateway.async_get_access_token() # This used to be a gather but it was less reliable with august's recent api changes. locks: list[Lock] = await self._api.async_get_operable_locks(token) or [] doorbells: list[Doorbell] = await self._api.async_get_doorbells(token) or [] self._doorbells_by_id = {device.device_id: device for device in doorbells} self._locks_by_id = {device.device_id: device for device in locks} self._house_ids = {device.house_id for device in chain(locks, doorbells)} await self._async_refresh_device_detail_by_ids( [device.device_id for device in chain(locks, doorbells)] ) # We remove all devices that we are missing # detail as we cannot determine if they are usable. # This also allows us to avoid checking for # detail being None all over the place self._remove_inoperative_locks() self._remove_inoperative_doorbells() await self.async_setup_activity_stream() if self._locks_by_id: # Do not prevent setup as the sync can timeout # but it is not a fatal error as the lock # will recover automatically when it comes back online. self._initial_sync_task = create_eager_task( self._async_initial_sync(), name="august-initial-sync" ) async def async_setup_activity_stream(self) -> None: """Set up the activity stream.""" token = await self._gateway.async_get_access_token() user_data = await self._api.async_get_user(token) pubnub = AugustPubNub() for device in self._device_detail_by_id.values(): pubnub.register_device(device) self.activity_stream = ActivityStream( self._api, self._gateway, self._house_ids, pubnub ) await self.activity_stream.async_setup() pubnub.subscribe(self.async_pubnub_message) self._pubnub_unsub = async_create_pubnub( user_data["UserID"], pubnub, self.brand ) async def _async_initial_sync(self) -> None: """Attempt to request an initial sync.""" # We don't care if this fails because we only want to wake # locks that are actually online anyways and they will be # awake when they come back online for result in await asyncio.gather( *[ create_eager_task( self.async_status_async( device_id, bool(detail.bridge and detail.bridge.hyper_bridge) ) ) for device_id, detail in self._device_detail_by_id.items() if device_id in self._locks_by_id ], return_exceptions=True, ): if isinstance(result, Exception) and not isinstance( result, (TimeoutError, ClientResponseError, CannotConnect) ): _LOGGER.warning( "Unexpected exception during initial sync: %s", result, exc_info=result, ) def async_pubnub_message( self, device_id: str, date_time: datetime, message: dict[str, Any] ) -> None: """Process a pubnub message.""" device = self.get_device_detail(device_id) activities = activities_from_pubnub_message(device, date_time, message) activity_stream = self.activity_stream if activities and activity_stream.async_process_newer_device_activities( activities ): self.async_signal_device_id_update(device.device_id) activity_stream.async_schedule_house_id_refresh(device.house_id) async def async_stop(self, *args: Any) -> None: """Stop the subscriptions.""" if self._pubnub_unsub: await self._pubnub_unsub() if self.activity_stream: self.activity_stream.async_stop() if self._initial_sync_task: self._initial_sync_task.cancel() with suppress(asyncio.CancelledError): await self._initial_sync_task @property def doorbells(self) -> ValuesView[Doorbell]: """Return a list of py-august Doorbell objects.""" return self._doorbells_by_id.values() @property def locks(self) -> ValuesView[Lock]: """Return a list of py-august Lock objects.""" return self._locks_by_id.values() def get_device_detail(self, device_id: str) -> DoorbellDetail | LockDetail: """Return the py-august LockDetail or DoorbellDetail object for a device.""" return self._device_detail_by_id[device_id] async def _async_refresh(self, time: datetime) -> None: await self._async_refresh_device_detail_by_ids(self._subscriptions.keys()) async def _async_refresh_device_detail_by_ids( self, device_ids_list: Iterable[str] ) -> None: """Refresh each device in sequence. This used to be a gather but it was less reliable with august's recent api changes. The august api has been timing out for some devices so we want the ones that it isn't timing out for to keep working. """ for device_id in device_ids_list: try: await self._async_refresh_device_detail_by_id(device_id) except TimeoutError: _LOGGER.warning( "Timed out calling august api during refresh of device: %s", device_id, ) except (ClientResponseError, CannotConnect) as err: _LOGGER.warning( "Error from august api during refresh of device: %s", device_id, exc_info=err, ) async def refresh_camera_by_id(self, device_id: str) -> None: """Re-fetch doorbell/camera data from API.""" await self._async_update_device_detail( self._doorbells_by_id[device_id], self._api.async_get_doorbell_detail, ) @property def push_updates_connected(self) -> bool: """Return if the push updates are connected.""" return ( self.activity_stream is not None and self.activity_stream.push_updates_connected ) async def _async_refresh_device_detail_by_id(self, device_id: str) -> None: if device_id in self._locks_by_id: if self.activity_stream and self.push_updates_connected: saved_attrs = _save_live_attrs(self._device_detail_by_id[device_id]) await self._async_update_device_detail( self._locks_by_id[device_id], self._api.async_get_lock_detail ) if self.activity_stream and self.push_updates_connected: _restore_live_attrs(self._device_detail_by_id[device_id], saved_attrs) # keypads are always attached to locks if ( device_id in self._device_detail_by_id and self._device_detail_by_id[device_id].keypad is not None ): keypad = self._device_detail_by_id[device_id].keypad self._device_detail_by_id[keypad.device_id] = keypad elif device_id in self._doorbells_by_id: await self._async_update_device_detail( self._doorbells_by_id[device_id], self._api.async_get_doorbell_detail, ) _LOGGER.debug( "async_signal_device_id_update (from detail updates): %s", device_id ) self.async_signal_device_id_update(device_id) async def _async_update_device_detail( self, device: Doorbell | Lock, api_call: Callable[ [str, str], Coroutine[Any, Any, DoorbellDetail | LockDetail] ], ) -> None: device_id = device.device_id device_name = device.device_name _LOGGER.debug("Started retrieving detail for %s (%s)", device_name, device_id) try: detail = await api_call( await self._gateway.async_get_access_token(), device_id ) except ClientError as ex: _LOGGER.error( "Request error trying to retrieve %s details for %s. %s", device_id, device_name, ex, ) _LOGGER.debug("Completed retrieving detail for %s (%s)", device_name, device_id) # If the key changes after startup we need to trigger a # discovery to keep it up to date if isinstance(detail, LockDetail) and detail.offline_key: self.async_offline_key_discovered(detail) self._device_detail_by_id[device_id] = detail @abstractmethod def async_offline_key_discovered(self, detail: LockDetail) -> None: """Handle offline key discovery.""" def get_device(self, device_id: str) -> Doorbell | Lock | None: """Get a device by id.""" return self._locks_by_id.get(device_id) or self._doorbells_by_id.get(device_id) def _get_device_name(self, device_id: str) -> str | None: """Return doorbell or lock name as August has it stored.""" if device := self.get_device(device_id): return device.device_name return None async def async_lock(self, device_id: str) -> list[ActivityTypes]: """Lock the device.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_lock_return_activities, await self._gateway.async_get_access_token(), device_id, ) async def async_status_async(self, device_id: str, hyper_bridge: bool) -> str: """Request status of the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_status_async, await self._gateway.async_get_access_token(), device_id, hyper_bridge, ) async def async_lock_async(self, device_id: str, hyper_bridge: bool) -> str: """Lock the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_lock_async, await self._gateway.async_get_access_token(), device_id, hyper_bridge, ) async def async_unlatch(self, device_id: str) -> list[ActivityTypes]: """Open/unlatch the device.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_unlatch_return_activities, await self._gateway.async_get_access_token(), device_id, ) async def async_unlatch_async(self, device_id: str, hyper_bridge: bool) -> str: """Open/unlatch the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_unlatch_async, await self._gateway.async_get_access_token(), device_id, hyper_bridge, ) async def async_unlock(self, device_id: str) -> list[ActivityTypes]: """Unlock the device.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_unlock_return_activities, await self._gateway.async_get_access_token(), device_id, ) async def async_unlock_async(self, device_id: str, hyper_bridge: bool) -> str: """Unlock the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_unlock_async, await self._gateway.async_get_access_token(), device_id, hyper_bridge, ) async def _async_call_api_op_requires_bridge( self, device_id: str, func: Callable[_P, Coroutine[Any, Any, _R]], *args: _P.args, **kwargs: _P.kwargs, ) -> _R: """Call an API that requires the bridge to be online and will change the device state.""" try: ret = await func(*args, **kwargs) except AugustApiAIOHTTPError as err: device_name = self._get_device_name(device_id) if device_name is None: device_name = f"DeviceID: {device_id}" raise self._error_exception_class(f"{device_name}: {err}") from err return ret async def async_get_doorbell_image( self, device_id: str, aiohttp_session: ClientSession, timeout: float = 10.0, ) -> bytes: """Get the latest image from the doorbell.""" doorbell = self.get_device_detail(device_id) try: return await doorbell.async_get_doorbell_image(aiohttp_session, timeout) except ContentTokenExpired: if self.brand != Brand.YALE_HOME: raise _LOGGER.debug( "Error fetching camera image, updating content-token from api to retry" ) await self.refresh_camera_by_id(device_id) doorbell = self.get_device_detail(device_id) return await doorbell.async_get_doorbell_image(aiohttp_session, timeout) def _remove_inoperative_doorbells(self) -> None: for doorbell in list(self.doorbells): device_id = doorbell.device_id if self._device_detail_by_id.get(device_id): continue _LOGGER.info( ( "The doorbell %s could not be setup because the system could not" " fetch details about the doorbell" ), doorbell.device_name, ) del self._doorbells_by_id[device_id] def _remove_inoperative_locks(self) -> None: # Remove non-operative locks as there must # be a bridge (August Connect) for them to # be usable for lock in list(self.locks): device_id = lock.device_id lock_detail = self._device_detail_by_id.get(device_id) if lock_detail is None: _LOGGER.info( ( "The lock %s could not be setup because the system could not" " fetch details about the lock" ), lock.device_name, ) elif lock_detail.bridge is None: _LOGGER.info( ( "The lock %s could not be setup because it does not have a" " bridge (Connect)" ), lock.device_name, ) del self._device_detail_by_id[device_id] # Bridge may come back online later so we still add the device since we will # have a pubnub subscription to tell use when it recovers else: continue del self._locks_by_id[device_id] yalexs-8.0.2/yalexs/manager/exceptions.py000066400000000000000000000005721465672432400205210ustar00rootroot00000000000000"""Exceptions for errors.""" from __future__ import annotations class RequireValidation(Exception): """Error to indicate we require validation (2fa).""" class CannotConnect(Exception): """Error to indicate we cannot connect.""" class InvalidAuth(Exception): """Error to indicate there is invalid auth.""" class YaleXSError(Exception): """Base error.""" yalexs-8.0.2/yalexs/manager/gateway.py000066400000000000000000000135531465672432400200040ustar00rootroot00000000000000"""Handle connection setup and authentication.""" from __future__ import annotations import asyncio import logging import os from http import HTTPStatus from pathlib import Path from typing import TypedDict from aiohttp import ClientError, ClientResponseError, ClientSession from ..api_async import ApiAsync from ..authenticator_async import AuthenticationState, AuthenticatorAsync from ..authenticator_common import Authentication from ..const import DEFAULT_BRAND from ..exceptions import AugustApiAIOHTTPError from .const import ( CONF_ACCESS_TOKEN_CACHE_FILE, CONF_BRAND, CONF_INSTALL_ID, CONF_LOGIN_METHOD, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, DEFAULT_AUGUST_CONFIG_FILE, DEFAULT_TIMEOUT, VERIFICATION_CODE_KEY, ) from .exceptions import CannotConnect, InvalidAuth, RequireValidation _LOGGER = logging.getLogger(__name__) class Config(TypedDict): """Config for the gateway.""" username: str password: str login_method: str access_token_cache_file: str install_id: str brand: str timeout: int class Gateway: """Handle the connection to yale.""" api: ApiAsync authenticator: AuthenticatorAsync authentication: Authentication _access_token_cache_file: str def __init__(self, config_path: Path, aiohttp_session: ClientSession) -> None: """Init the connection.""" self._aiohttp_session = aiohttp_session self._token_refresh_lock = asyncio.Lock() self._config_path = config_path self._config: Config | None = None self._loop = asyncio.get_running_loop() async def async_get_access_token(self) -> str: """Get the access token.""" return self.authentication.access_token def async_configure_access_token_cache_file( self, username: str, access_token_cache_file: str | None ) -> str: """Configure the access token cache file.""" file = access_token_cache_file or f".{username}{DEFAULT_AUGUST_CONFIG_FILE}" self._access_token_cache_file = file return self._config_path.joinpath(file) async def async_setup( self, conf: Config, authenticator_class: type[AuthenticatorAsync] | None = None ) -> None: """Create the api and authenticator objects.""" if conf.get(VERIFICATION_CODE_KEY): return self._config = conf self.api = ApiAsync( self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), brand=self._config.get(CONF_BRAND, DEFAULT_BRAND), ) klass = authenticator_class or AuthenticatorAsync username = conf.get(CONF_USERNAME) access_token_cache_file_path: str | None = None if username: access_token_cache_file_path = self.async_configure_access_token_cache_file( conf[CONF_USERNAME], conf.get(CONF_ACCESS_TOKEN_CACHE_FILE) ) self.authenticator = klass( self.api, self._config.get(CONF_LOGIN_METHOD), username, self._config.get(CONF_PASSWORD, ""), install_id=self._config.get(CONF_INSTALL_ID), access_token_cache_file=access_token_cache_file_path, ) await self.authenticator.async_setup_authentication() async def async_authenticate(self) -> Authentication: """Authenticate with the details provided to setup.""" try: self.authentication = await self.authenticator.async_authenticate() auth_state = self.authentication.state if auth_state is AuthenticationState.AUTHENTICATED: # Call the locks api to verify we are actually # authenticated because we can be authenticated # by have no access await self.api.async_get_operable_locks( await self.async_get_access_token() ) except AugustApiAIOHTTPError as ex: if ex.auth_failed: raise InvalidAuth from ex raise CannotConnect from ex except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: raise InvalidAuth from ex raise CannotConnect from ex except ClientError as ex: _LOGGER.error("Unable to connect to August service: %s", str(ex)) raise CannotConnect from ex if auth_state is AuthenticationState.BAD_PASSWORD: raise InvalidAuth if auth_state is AuthenticationState.REQUIRES_VALIDATION: raise RequireValidation if auth_state is not AuthenticationState.AUTHENTICATED: _LOGGER.error("Unknown authentication state: %s", auth_state) raise InvalidAuth return self.authentication async def async_reset_authentication(self) -> None: """Remove the cache file.""" await self._loop.run_in_executor(None, self._reset_authentication) def _reset_authentication(self) -> None: """Remove the cache file.""" path = self._config_path.joinpath(self._access_token_cache_file) if os.path.exists(path): os.unlink(path) async def async_refresh_access_token_if_needed(self) -> None: """Refresh the august access token if needed.""" if not self.authenticator.should_refresh(): return async with self._token_refresh_lock: refreshed_authentication = ( await self.authenticator.async_refresh_access_token(force=False) ) _LOGGER.info( ( "Refreshed august access token. The old token expired at %s, and" " the new token expires at %s" ), self.authentication.access_token_expires, refreshed_authentication.access_token_expires, ) self.authentication = refreshed_authentication yalexs-8.0.2/yalexs/manager/subscriber.py000066400000000000000000000060251465672432400205020ustar00rootroot00000000000000"""yalexs subscribers.""" from __future__ import annotations import asyncio from abc import ABC, abstractmethod from collections import defaultdict from datetime import timedelta from functools import partial from typing import Any, Callable from ..backports.tasks import create_eager_task class SubscriberMixin(ABC): """Base implementation for a subscriber.""" def __init__(self, update_interval: timedelta) -> None: """Initialize an subscriber.""" super().__init__() self._update_interval_seconds = update_interval.total_seconds() self._subscriptions: defaultdict[str, set[Callable[[], None]]] = defaultdict( set ) self._unsub_interval: asyncio.TimerHandle | None = None self._loop = asyncio.get_running_loop() self._refresh_task: asyncio.Task | None = None def async_subscribe_device_id( self, device_id: str, update_callback: Callable[[], None] ) -> Callable[[], None]: """Add an callback subscriber. Returns a callable that can be used to unsubscribe. """ if not self._subscriptions: self._async_setup_listeners() self._subscriptions[device_id].add(update_callback) return partial(self.async_unsubscribe_device_id, device_id, update_callback) @abstractmethod async def _async_refresh(self) -> None: """Refresh data.""" def _async_scheduled_refresh(self) -> None: """Call the refresh method.""" self._unsub_interval = self._loop.call_later( self._update_interval_seconds, self._async_scheduled_refresh, ) self._refresh_task = create_eager_task( self._async_refresh(), loop=self._loop, name=f"{self} schedule refresh" ) def _async_cancel_update_interval(self) -> None: """Cancel the scheduled update.""" if self._unsub_interval: self._unsub_interval.cancel() self._unsub_interval = None def _async_setup_listeners(self) -> None: """Create interval and stop listeners.""" self._async_cancel_update_interval() self._unsub_interval = self._loop.call_later( self._update_interval_seconds, self._async_scheduled_refresh, ) def async_stop(self, *args: Any) -> None: """Cleanup on shutdown.""" self._refresh_task.cancel() self._async_cancel_update_interval() def async_unsubscribe_device_id( self, device_id: str, update_callback: Callable[[], None] ) -> None: """Remove a callback subscriber.""" self._subscriptions[device_id].remove(update_callback) if not self._subscriptions[device_id]: del self._subscriptions[device_id] if self._subscriptions: return self._async_cancel_update_interval() def async_signal_device_id_update(self, device_id: str) -> None: """Call the callbacks for a device_id.""" for update_callback in self._subscriptions.get(device_id, ()): update_callback() yalexs-8.0.2/yalexs/pin.py000066400000000000000000000046071465672432400155170ustar00rootroot00000000000000from __future__ import annotations from .time import parse_datetime class Pin: def __init__(self, data): self._pin_id = data["_id"] self._lock_id = data["lockID"] self._user_id = data["userID"] self._state = data["state"] self._pin = data["pin"] self._slot = data["slot"] self._access_type = data["accessType"] self._first_name = data["firstName"] self._last_name = data["lastName"] self._unverified = data["unverified"] self._created_at = data["createdAt"] self._updated_at = data["updatedAt"] self._loaded_date = data["loadedDate"] # Times for temporary access codes self._access_start_time = data.get("accessStartTime") self._access_end_time = data.get("accessEndTime") self._access_times = data.get("accessTimes") @property def pin_id(self): return self._pin_id @property def lock_id(self): return self._lock_id @property def user_id(self): return self._user_id @property def state(self): return self._state @property def pin(self): return self._pin @property def slot(self): return self._slot @property def access_type(self): return self._access_type @property def first_name(self): return self._first_name @property def last_name(self): return self._last_name @property def unverified(self): return self._unverified @property def created_at(self): return parse_datetime(self._created_at) @property def updated_at(self): return parse_datetime(self._updated_at) @property def loaded_date(self): return parse_datetime(self._loaded_date) @property def access_start_time(self): if not self._access_start_time: return None return parse_datetime(self._access_start_time) @property def access_end_time(self): if not self._access_end_time: return None return parse_datetime(self._access_end_time) @property def access_times(self): if not self._access_times: return None return parse_datetime(self._access_times) def __repr__(self): return "Pin(id={} firstName={}, lastName={})".format( self.pin_id, self.first_name, self.last_name ) yalexs-8.0.2/yalexs/pubnub_activity.py000066400000000000000000000113771465672432400201420ustar00rootroot00000000000000import logging from datetime import datetime from typing import Any from .activity import ( ACTION_BRIDGE_OFFLINE, ACTION_BRIDGE_ONLINE, ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN, ACTION_DOORBELL_BUTTON_PUSHED, ACTION_DOORBELL_IMAGE_CAPTURE, ACTION_DOORBELL_MOTION_DETECTED, ACTION_LOCK_JAMMED, ACTION_LOCK_LOCK, ACTION_LOCK_LOCKING, ACTION_LOCK_UNLATCH, ACTION_LOCK_UNLATCHING, ACTION_LOCK_UNLOCK, ACTION_LOCK_UNLOCKING, SOURCE_PUBNUB, ActivityTypes, ) from .api_common import _activity_from_dict, _datetime_string_to_epoch from .device import Device from .doorbell import DOORBELL_STATUS_KEY, DoorbellDetail from .lock import ( DOOR_STATE_KEY, LOCK_STATUS_KEY, LockDetail, LockDoorStatus, LockStatus, determine_door_state, determine_lock_status, ) LOCK_STATUS_TO_ACTION = { LockStatus.LOCKED: ACTION_LOCK_LOCK, LockStatus.UNLATCHED: ACTION_LOCK_UNLATCH, LockStatus.UNLOCKED: ACTION_LOCK_UNLOCK, LockStatus.LOCKING: ACTION_LOCK_LOCKING, LockStatus.UNLATCHING: ACTION_LOCK_UNLATCHING, LockStatus.UNLOCKING: ACTION_LOCK_UNLOCKING, LockStatus.JAMMED: ACTION_LOCK_JAMMED, } _BRIDGE_ACTIONS = {ACTION_BRIDGE_ONLINE, ACTION_BRIDGE_OFFLINE} _LOGGER = logging.getLogger(__name__) def activities_from_pubnub_message( device: Device, date_time: datetime, message: dict[str, Any] ) -> list[ActivityTypes]: """Create activities from pubnub.""" activities: list[ActivityTypes] = [] activity_dict = { "deviceID": device.device_id, "house": device.house_id, "deviceName": device.device_name, } info = message.get("info", {}) context = info.get("context", {}) accept_user = False if "startDate" in context: activity_dict["dateTime"] = _datetime_string_to_epoch(context["startDate"]) accept_user = True elif "startTime" in info: activity_dict["dateTime"] = _datetime_string_to_epoch(info["startTime"]) accept_user = True else: activity_dict["dateTime"] = date_time.timestamp() * 1000 if isinstance(device, LockDetail): activity_dict["deviceType"] = "lock" activity_dict["info"] = info calling_user_id = message.get("callingUserID") # Some locks sometimes send lots of status messages, triggered by the app. Ignore these. if ( info.get("action") == "status" and not message.get("error") and not message.get("result") == "failed" ): _LOGGER.debug("Not creating lock activity from status pubnub") return activities # Only accept a UserID if we have a date/time # as otherwise it is a duplicate of the previous # activity if accept_user and calling_user_id: activity_dict["callingUser"] = {"UserID": calling_user_id} if "remoteEvent" in message: activity_dict["info"]["remote"] = True error = message.get("error") or {} if error.get("restCode") == 98 or error.get("name") == "ERRNO_BRIDGE_OFFLINE": _add_activity(activities, activity_dict, ACTION_BRIDGE_OFFLINE) elif status := message.get(LOCK_STATUS_KEY): if status in _BRIDGE_ACTIONS: _add_activity(activities, activity_dict, status) if action := LOCK_STATUS_TO_ACTION.get(determine_lock_status(status)): _add_activity(activities, activity_dict, action) if door_state_raw := message.get(DOOR_STATE_KEY): door_state = determine_door_state(door_state_raw) if door_state == LockDoorStatus.OPEN: _add_activity(activities, activity_dict, ACTION_DOOR_OPEN) elif door_state == LockDoorStatus.CLOSED: _add_activity(activities, activity_dict, ACTION_DOOR_CLOSED) elif isinstance(device, DoorbellDetail): activity_dict["deviceType"] = "doorbell" info = activity_dict["info"] = message.get("data", {}) info.setdefault("image", info.get("result", {})) info.setdefault("started", activity_dict["dateTime"]) info.setdefault("ended", activity_dict["dateTime"]) if (status := message.get(DOORBELL_STATUS_KEY)) and status in ( ACTION_DOORBELL_MOTION_DETECTED, ACTION_DOORBELL_IMAGE_CAPTURE, ACTION_DOORBELL_BUTTON_PUSHED, ): _add_activity(activities, activity_dict, status) return activities def _add_activity( activities: list[ActivityTypes], activity_dict: dict[str, Any], action: str ) -> None: """Add an activity.""" activity_dict = activity_dict.copy() activity_dict["action"] = action activities.append( _activity_from_dict( SOURCE_PUBNUB, activity_dict, _LOGGER.isEnabledFor(logging.DEBUG) ) ) yalexs-8.0.2/yalexs/pubnub_async.py000066400000000000000000000115421465672432400174150ustar00rootroot00000000000000"""Connect to pubnub.""" import asyncio import datetime import logging from collections.abc import Coroutine from functools import partial from typing import Any, Callable from pubnub.callbacks import SubscribeCallback from pubnub.enums import PNReconnectionPolicy, PNStatusCategory from pubnub.models.consumer.common import PNStatus from pubnub.models.consumer.pubsub import PNMessageResult from pubnub.pnconfiguration import PNConfiguration from pubnub.pubnub_asyncio import AsyncioSubscriptionManager, PubNubAsyncio from .const import PUBNUB_TOKENS, Brand from .device import DeviceDetail _LOGGER = logging.getLogger(__name__) UpdateCallbackType = Callable[[str, datetime.datetime, dict[str, Any]], None] SHOULD_RECONNECT_CATEGORIES = { PNStatusCategory.PNUnknownCategory, PNStatusCategory.PNUnexpectedDisconnectCategory, PNStatusCategory.PNNetworkIssuesCategory, PNStatusCategory.PNTimeoutCategory, } class AugustPubNub(SubscribeCallback): def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the AugustPubNub.""" super().__init__(*args, **kwargs) self.connected = False self._device_channels: dict[str, str] = {} self._subscriptions: set[UpdateCallbackType] = set() def presence(self, pubnub: AsyncioSubscriptionManager, presence): _LOGGER.debug("Received new presence: %s", presence) def status(self, pubnub: AsyncioSubscriptionManager, status: PNStatus) -> None: if not pubnub: self.connected = False return _LOGGER.debug( "Received new status: category=%s error_data=%s error=%s status_code=%s operation=%s", status.category, status.error_data, status.error, status.status_code, status.operation, ) if status.category in SHOULD_RECONNECT_CATEGORIES: self.connected = False pubnub.reconnect() elif status.category == PNStatusCategory.PNReconnectedCategory: self.connected = True now = datetime.datetime.utcnow() # Callback with an empty message to force a refresh for callback in self._subscriptions: for device_id in self._device_channels.values(): callback(device_id, now, {}) elif status.category == PNStatusCategory.PNConnectedCategory: self.connected = True def message( self, pubnub: AsyncioSubscriptionManager, message: PNMessageResult ) -> None: # Handle new messages device_id = self._device_channels[message.channel] _LOGGER.debug( "Received new messages on channel %s for device_id: %s with timetoken: %s: %s", message.channel, device_id, message.timetoken, message.message, ) dt = datetime.datetime.fromtimestamp( int(message.timetoken) / 10000000, tz=datetime.timezone.utc ) for callback in self._subscriptions: callback(device_id, dt, message.message) def subscribe(self, update_callback: UpdateCallbackType) -> Callable[[], None]: """Add an callback subscriber. Returns a callable that can be used to unsubscribe. """ self._subscriptions.add(update_callback) return partial(self._unsubscribe, update_callback) def _unsubscribe(self, update_callback: UpdateCallbackType) -> None: self._subscriptions.remove(update_callback) def register_device(self, device_detail: DeviceDetail) -> None: """Register a device to get updates.""" if device_detail.pubsub_channel is None: return self._device_channels[device_detail.pubsub_channel] = device_detail.device_id @property def channels(self): """Return a list of registered channels.""" return self._device_channels.keys() def async_create_pubnub( user_uuid: str, subscriptions: AugustPubNub, brand: Brand = Brand.AUGUST ) -> Callable[[], Coroutine[Any, Any, None]]: """Create a pubnub subscription.""" tokens = PUBNUB_TOKENS[brand] pnconfig = PNConfiguration() pnconfig.subscribe_key = tokens["subscribe"] pnconfig.publish_key = tokens["publish"] pnconfig.uuid = f"pn-{str(user_uuid).upper()}" pnconfig.reconnect_policy = PNReconnectionPolicy.EXPONENTIAL pubnub = PubNubAsyncio(pnconfig) pubnub.add_listener(subscriptions) pubnub.subscribe().channels(subscriptions.channels).execute() async def _async_unsub(): _LOGGER.debug("Removing listeners PubNub") pubnub.remove_listener(subscriptions) _LOGGER.debug("Unsubscribing from PubNub") pubnub.unsubscribe_all() await asyncio.sleep(0.1) # Allow the unsubscribe to complete _LOGGER.debug("Stopping PubNub") await pubnub.stop() _LOGGER.debug("PubNub stopped") return _async_unsub yalexs-8.0.2/yalexs/time.py000066400000000000000000000010651465672432400156620ustar00rootroot00000000000000from __future__ import annotations import datetime from functools import lru_cache import ciso8601 import dateutil.parser @lru_cache(maxsize=512) def epoch_to_datetime(epoch: str | int | float) -> datetime.datetime: """Convert epoch to datetime.""" return datetime.datetime.fromtimestamp(float(epoch) / 1000.0) def parse_datetime(datetime_string: str) -> datetime.datetime: """Parse a datetime string.""" try: return ciso8601.parse_datetime(datetime_string) except ValueError: return dateutil.parser.parse(datetime_string) yalexs-8.0.2/yalexs/users.py000066400000000000000000000022271465672432400160660ustar00rootroot00000000000000from __future__ import annotations from typing import Any from .backports.functools import cached_property USER_CACHE = {} class YaleUser: """Represent a yale access user.""" def __init__(self, uuid: str, data: dict[str, Any]) -> None: """Initialize the YaleUser.""" self._uuid = uuid self._data = data @cached_property def thumbnail_url(self) -> str | None: return self._data.get("imageInfo", {}).get("thumbnail", {}).get("secure_url") @cached_property def image_url(self) -> str | None: return self._data.get("imageInfo", {}).get("original", {}).get("secure_url") @cached_property def first_name(self) -> str | None: return self._data.get("FirstName") @cached_property def last_name(self) -> str | None: return self._data.get("LastName") @cached_property def user_type(self) -> str | None: return self._data.get("UserType") def get_user_info(uuid: str) -> YaleUser | None: return USER_CACHE.get(uuid) def cache_user_info(uuid: str, data: dict[str, Any]) -> None: if uuid not in USER_CACHE: USER_CACHE[uuid] = YaleUser(uuid, data) yalexs-8.0.2/yalexs/util.py000066400000000000000000000106541465672432400157050ustar00rootroot00000000000000import datetime import random import ssl from functools import cache from typing import Optional, Union, TYPE_CHECKING from .activity import ( ACTION_BRIDGE_OFFLINE, ACTION_BRIDGE_ONLINE, ACTIVITY_ACTION_STATES, MOVING_STATES, ACTIVITY_MOVING_STATES, BridgeOperationActivity, DoorbellImageCaptureActivity, DoorbellMotionActivity, DoorOperationActivity, LockOperationActivity, ) from .const import CONFIGURATION_URLS, Brand from .lock import LockDetail LockActivityTypes = Union[ LockOperationActivity, DoorOperationActivity, BridgeOperationActivity ] DoorbellActivityTypes = Union[ DoorbellImageCaptureActivity, DoorbellMotionActivity, BridgeOperationActivity ] if TYPE_CHECKING: from .doorbell import DoorbellDetail def get_latest_activity( activity1: Optional[LockActivityTypes], activity2: Optional[LockActivityTypes] ) -> Optional[LockActivityTypes]: """Return the latest activity.""" return ( activity2 if ( not activity1 or ( activity2 and activity2.action not in ACTIVITY_MOVING_STATES and activity1.activity_start_time <= activity2.activity_start_time ) ) else activity1 ) def update_lock_detail_from_activity( lock_detail: LockDetail, activity: LockActivityTypes, ) -> bool: """Update the LockDetail from an activity.""" activity_end_time_utc = as_utc_from_local(activity.activity_end_time) if activity.device_id != lock_detail.device_id: raise ValueError if isinstance(activity, LockOperationActivity): if lock_detail.lock_status_datetime and ( lock_detail.lock_status_datetime > activity_end_time_utc or ( lock_detail.lock_status_datetime == activity_end_time_utc and lock_detail.lock_status not in MOVING_STATES ) ): return False lock_detail.lock_status = ACTIVITY_ACTION_STATES[activity.action] lock_detail.lock_status_datetime = activity_end_time_utc elif isinstance(activity, DoorOperationActivity): if ( lock_detail.door_state_datetime and lock_detail.door_state_datetime >= activity_end_time_utc ): return False lock_detail.door_state = ACTIVITY_ACTION_STATES[activity.action] lock_detail.door_state_datetime = activity_end_time_utc elif isinstance(activity, BridgeOperationActivity): if activity.action == ACTION_BRIDGE_ONLINE: lock_detail.set_online(True) elif activity.action == ACTION_BRIDGE_OFFLINE: lock_detail.set_online(False) else: raise ValueError return True def update_doorbell_image_from_activity( doorbell_detail: "DoorbellDetail", activity: DoorbellActivityTypes ) -> bool: """Update the DoorDetail from an activity with a new image.""" if activity.device_id != doorbell_detail.device_id: raise ValueError if isinstance(activity, (DoorbellImageCaptureActivity, DoorbellMotionActivity)): if activity.image_created_at_datetime is None: return False if ( doorbell_detail.image_created_at_datetime is None or doorbell_detail.image_created_at_datetime < activity.image_created_at_datetime ): doorbell_detail.image_url = activity.image_url doorbell_detail.image_created_at_datetime = ( activity.image_created_at_datetime ) doorbell_detail.content_token = ( activity.content_token or doorbell_detail.content_token ) else: return False else: raise ValueError return True def as_utc_from_local(dtime: datetime.datetime) -> datetime.datetime: """Converts the datetime returned from an activity to UTC.""" return dtime.astimezone(tz=datetime.timezone.utc) def get_configuration_url(brand: Brand) -> str: """Return the configuration URL for the brand.""" return CONFIGURATION_URLS[brand] @cache def get_ssl_context() -> ssl.SSLContext: """Return an SSL context for cloudflare.""" context = ssl.create_default_context() ciphers = [cipher["name"] for cipher in context.get_ciphers()] default_ciphers = ciphers[:3] backup_ciphers = ciphers[3:] random.shuffle(backup_ciphers) context.set_ciphers(":".join((*default_ciphers, *backup_ciphers))) return context