pax_global_header00006660000000000000000000000064147435155520014525gustar00rootroot0000000000000052 comment=51f9b5130a08b8f06461fedc8cbd6b646a94f330 yalexs-8.11.0/000077500000000000000000000000001474351555200131215ustar00rootroot00000000000000yalexs-8.11.0/.github/000077500000000000000000000000001474351555200144615ustar00rootroot00000000000000yalexs-8.11.0/.github/dependabot.yml000066400000000000000000000010151474351555200173060ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" yalexs-8.11.0/.github/workflows/000077500000000000000000000000001474351555200165165ustar00rootroot00000000000000yalexs-8.11.0/.github/workflows/ci.yml000066400000000000000000000052311474351555200176350ustar00rootroot00000000000000name: 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" - "3.13" 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.11.0/.gitignore000066400000000000000000000022341474351555200151120ustar00rootroot00000000000000# 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.11.0/.pre-commit-config.yaml000066400000000000000000000033401474351555200174020ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks exclude: "CHANGELOG.md" default_stages: [pre-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: v4.1.0 hooks: - id: commitizen stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.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.19.1 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.1 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.8.2 hooks: - id: bandit args: [-x, tests] yalexs-8.11.0/CHANGELOG.md000066400000000000000000001756621474351555200147530ustar00rootroot00000000000000# CHANGELOG ## v8.11.0 (2025-01-20) ### Feature * feat: adds alarm support to Yale home (#208) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> ([`473fd0e`](https://github.com/bdraco/yalexs/commit/473fd0e0dd87a9296ff2ae48a00b2d6ce4b7d2ef)) ## v8.10.0 (2024-10-06) ### Feature * feat: add support for propcache v1.0.0+ (#192) ([`feceab8`](https://github.com/bdraco/yalexs/commit/feceab840a67a346eaecbb5eff6d3df9f8097597)) ## v8.9.0 (2024-10-03) ### Feature * feat: add support for pin lock activities (#189) Co-authored-by: Thomas Nelson <me@example.com> ([`e13e977`](https://github.com/bdraco/yalexs/commit/e13e97720795e5730b309dc1e9a90c3d15204c58)) ## v8.8.0 (2024-10-03) ### Feature * feat: switch to using propcache cached_property to improve performance (#191) ([`b27dc91`](https://github.com/bdraco/yalexs/commit/b27dc9179a1ef46b33bfdc02b3d40b954a48e7aa)) ## v8.7.1 (2024-09-24) ### Fix * fix: add default first & last names for pin_* actions (#188) Co-authored-by: Thomas Nelson <me@example.com> ([`f2cae5b`](https://github.com/bdraco/yalexs/commit/f2cae5bc2041ee7653cc65c84823991a378365ad)) ## v8.7.0 (2024-09-23) ### Feature * feat: add support for finger unlock activities (#186) Co-authored-by: Thomas Nelson <me@example.com> ([`fb5ef13`](https://github.com/bdraco/yalexs/commit/fb5ef137404121fe7761ae74950c32b584728e3d)) ## v8.6.4 (2024-09-06) ### Fix * fix: debounce activity updates to handle rapid websocket updates (#182) ([`bf3b0a1`](https://github.com/bdraco/yalexs/commit/bf3b0a141f130dc5c32d590b1fbdcf28250cc887)) ## v8.6.3 (2024-09-03) ### Fix * fix: battery state not refreshing (#181) ([`78cf869`](https://github.com/bdraco/yalexs/commit/78cf869c5d0a184e0f58aa5ba7ba76adda99116b)) ## v8.6.2 (2024-09-03) ### Fix * fix: remove yale global pubnub tokens (#180) ([`f715ae1`](https://github.com/bdraco/yalexs/commit/f715ae1d8abb878f5707017b6da66f4f91423f89)) ## v8.6.1 (2024-09-03) ### Fix * fix: restore doorbell support for yale global brand (#179) ([`219d176`](https://github.com/bdraco/yalexs/commit/219d176b5f8e9c0f414f19e49a08f1f679c7b2a7)) ## v8.6.0 (2024-09-02) ### Feature * feat: add support for linked lock activities (#177) ([`5eed87b`](https://github.com/bdraco/yalexs/commit/5eed87bda1f6fa0f1f422105a265f8d8af884b83)) ## v8.5.5 (2024-08-29) ### Fix * fix: improve handling of 502 errors (#174) ([`e19264b`](https://github.com/bdraco/yalexs/commit/e19264bd5aa7e8e51fdde1337c38c917918f7f96)) ## v8.5.4 (2024-08-27) ### Fix * fix: bump socketio to 5+ (#173) ([`ab5a6c8`](https://github.com/bdraco/yalexs/commit/ab5a6c842b413a134e52558c10570cdcdc8c0079)) ## v8.5.3 (2024-08-27) ### Fix * fix: doorbell image retry with yale global brand (#172) ([`4a41de5`](https://github.com/bdraco/yalexs/commit/4a41de5b09fd1381d9c1e76140d5138f859f15b1)) ## v8.5.2 (2024-08-27) ### Fix * fix: disable excessive logging (#171) ([`7e01490`](https://github.com/bdraco/yalexs/commit/7e014908f28f0a0d642bd923029400f402927f85)) ## v8.5.1 (2024-08-27) ### Fix * fix: lower socketio to 4 for ha compat (#170) ([`1ae7c88`](https://github.com/bdraco/yalexs/commit/1ae7c88b97d64a9ab88ea74911ea044c1b74c1a0)) ## v8.5.0 (2024-08-27) ### Feature * feat: add socketio support for websockets (#165) ([`f260c82`](https://github.com/bdraco/yalexs/commit/f260c82d9c7c396fcca584ebfa51e5f808a802c3)) ## v8.4.2 (2024-08-25) ### Fix * fix: removing polling fallback (#168) ([`f346fa3`](https://github.com/bdraco/yalexs/commit/f346fa30f777426b9b507985f0dc75ad7d0325ff)) ## v8.4.1 (2024-08-25) ### Fix * fix: ensure init sync does not check rate limit (#166) ([`9548850`](https://github.com/bdraco/yalexs/commit/9548850eb09f7c79d7ad2619b0181ff5c079bc93)) ## v8.4.0 (2024-08-24) ### Feature * feat: add constant for brands without oauth required (#164) ([`7c472c0`](https://github.com/bdraco/yalexs/commit/7c472c02beba7d4ba998c0ff9ae3e191b8b96b2d)) ## v8.3.3 (2024-08-23) ### Fix * fix: shutdown faster by doing pubnub shutdown last (#162) * fix: shutdown faster by doing pubnub shutdown last * more guards * fix: move rate limit check sooner to avoid getting blocked * fix: move rate limit check sooner to avoid getting blocked * fix: ensure rate limit kicks in on failure as well * fix: register point ([`735ba94`](https://github.com/bdraco/yalexs/commit/735ba9421ea211c43bb72fc7043e2e408108f949)) ## v8.3.2 (2024-08-23) ### Fix * fix: check for oauth required in async_authenticate to avoid auth being cleared (#161) ([`a849351`](https://github.com/bdraco/yalexs/commit/a849351d130c1781544c50236115f386252b20e1)) ## v8.3.1 (2024-08-23) ### Fix * fix: always send brand header (#160) ([`c0b1fad`](https://github.com/bdraco/yalexs/commit/c0b1fadde651d24ada25f54ef8d0c70ff04bc992)) ## v8.3.0 (2024-08-23) ### Feature * feat: add require oauth flag (#159) ([`a336e47`](https://github.com/bdraco/yalexs/commit/a336e47abee337700ba4f851fb1e70be75d8c263)) ## v8.2.0 (2024-08-23) ### Feature * feat: implement rate limit checker to avoid getting blocked (#158) ([`7dfb92b`](https://github.com/bdraco/yalexs/commit/7dfb92b93b591e8e54c7158048c1ccbdd4a3a3ad)) ## v8.1.4 (2024-08-22) ### Fix * fix: add missing commitlint config (#157) ([`26d7a4a`](https://github.com/bdraco/yalexs/commit/26d7a4a0f011ca8e5ca2c18d1c6ec2876bccd0da)) ## v8.1.3 (2024-08-22) ### Fix * fix: timestamp division for datetime generation in tests (#156) ([`446d3ec`](https://github.com/bdraco/yalexs/commit/446d3eceb74f9838b55299d310a39d6ee061cadc)) ## v8.1.2 (2024-08-20) ### Fix * fix: ensure subclassed AugustApiAIOHTTPError is back-compat (#155) ([`8304c3b`](https://github.com/bdraco/yalexs/commit/8304c3bca9ab6260662c87e1b51698b48bef9bf5)) ## v8.1.1 (2024-08-20) ### Fix * fix: handle 429 error was yale error (#154) ([`e450a76`](https://github.com/bdraco/yalexs/commit/e450a7667250d3c4b69bd5459628fa45346b15e2)) ## v8.1.0 (2024-08-20) ### Feature * feat: cleanup exceptions to avoid duplication (#153) ([`51af44e`](https://github.com/bdraco/yalexs/commit/51af44e64abb0a804179cef12b1dda903cbff7c5)) ## 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.11.0/LICENSE000066400000000000000000000020471474351555200141310ustar00rootroot00000000000000MIT 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.11.0/MANIFEST000066400000000000000000000006161474351555200142550ustar00rootroot00000000000000# 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.11.0/README.md000066400000000000000000000116231474351555200144030ustar00rootroot00000000000000# 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 import asyncio from aiohttp import ClientSession from yalexs.api_async import ApiAsync from yalexs.authenticator_async import AuthenticatorAsync from yalexs.const import Brand from yalexs.alarm import ArmState async def main(): api = ApiAsync(ClientSession(), timeout=20, brand=Brand.YALE_HOME) authenticator = AuthenticatorAsync(api, "email", "EMAIL_ADDRESS", "PASSWORD}", access_token_cache_file="auth.txt",install_id="UUID") await authenticator.async_setup_authentication() authentication = await authenticator.async_authenticate() access_token = authentication.access_token # if(authentication.state == AuthenticationState.REQUIRES_VALIDATION) : # await authenticator.async_send_verification_code() # await authenticator.async_validate_verification_code("12345") # DO STUFF HERE LIKE GET THE ALARMS, LOCS, ETC.... alarms = await api.async_get_alarms(access_token) locks = api.get_locks(access_token) # OR ARM YOUR ALARM await api.async_arm_alarm(access_token, alarms[0], ArmState.Away) asyncio.run(main()) ``` yalexs-8.11.0/build.sh000066400000000000000000000001021474351555200145450ustar00rootroot00000000000000#!/bin/sh python setup.py sdist bdist_wheel twine upload dist/* yalexs-8.11.0/commitlint.config.mjs000066400000000000000000000003621474351555200172600ustar00rootroot00000000000000export default { extends: ["@commitlint/config-conventional"], rules: { "header-max-length": [0, "always", Infinity], "body-max-line-length": [0, "always", Infinity], "footer-max-line-length": [0, "always", Infinity], }, }; yalexs-8.11.0/known_activities.md000066400000000000000000000102761474351555200170310ustar00rootroot00000000000000# 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.11.0/poetry.lock000066400000000000000000005245301474351555200153260ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.5 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.11" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"}, {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"}, {file = "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177"}, {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217"}, {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a"}, {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a"}, {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115"}, {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a"}, {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3"}, {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038"}, {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519"}, {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc"}, {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d"}, {file = "aiohttp-3.10.11-cp310-cp310-win32.whl", hash = "sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120"}, {file = "aiohttp-3.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674"}, {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07"}, {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695"}, {file = "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24"}, {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382"}, {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa"}, {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625"}, {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9"}, {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac"}, {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a"}, {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b"}, {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16"}, {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730"}, {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8"}, {file = "aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9"}, {file = "aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f"}, {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710"}, {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d"}, {file = "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97"}, {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725"}, {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636"}, {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385"}, {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087"}, {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f"}, {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03"}, {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d"}, {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a"}, {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e"}, {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4"}, {file = "aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb"}, {file = "aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27"}, {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127"}, {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413"}, {file = "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461"}, {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288"}, {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067"}, {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e"}, {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1"}, {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006"}, {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f"}, {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6"}, {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31"}, {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d"}, {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00"}, {file = "aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71"}, {file = "aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e"}, {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2"}, {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339"}, {file = "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95"}, {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92"}, {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7"}, {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d"}, {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca"}, {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa"}, {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b"}, {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658"}, {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39"}, {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9"}, {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7"}, {file = "aiohttp-3.10.11-cp38-cp38-win32.whl", hash = "sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4"}, {file = "aiohttp-3.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec"}, {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106"}, {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6"}, {file = "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01"}, {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e"}, {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829"}, {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8"}, {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc"}, {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa"}, {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b"}, {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138"}, {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777"}, {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261"}, {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f"}, {file = "aiohttp-3.10.11-cp39-cp39-win32.whl", hash = "sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9"}, {file = "aiohttp-3.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb"}, {file = "aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7"}, ] [package.dependencies] aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.12.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [[package]] name = "aioresponses" version = "0.7.8" description = "Mock out requests made by ClientSession from aiohttp package" optional = false python-versions = "*" files = [ {file = "aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94"}, {file = "aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11"}, ] [package.dependencies] aiohttp = ">=3.3.0,<4.0.0" packaging = ">=22.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 = "bidict" version = "0.23.1" description = "The bidirectional mapping library for Python." optional = false python-versions = ">=3.8" files = [ {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, ] [[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.2" description = "Fast ISO8601 date time parser for Python written in C" optional = false python-versions = "*" files = [ {file = "ciso8601-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1bb2d4d20d7ed65fcc7137652d7d980c6eb2aa19c935579309170137d33064ce"}, {file = "ciso8601-2.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:3039f11ced0bc971341ab63be222860eb2cc942d51a7aa101b1809b633ad2288"}, {file = "ciso8601-2.3.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:d64634b02cfb194e54569d8de3ace89cec745644cab38157aea0b03d32031eda"}, {file = "ciso8601-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0dcb8dc5998bc50346cec9d3b8b5deda8ddabeda70a923c110efb5100cd9754"}, {file = "ciso8601-2.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a3ca99eadbee4a9bb7dfb2bcf266a21828033853cd99803a9893d3473ac0e9"}, {file = "ciso8601-2.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d61daee5e8daee87eba34151b9952ec8c3327ad9e54686b6247dcb9b2b135312"}, {file = "ciso8601-2.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2f20654de6b0374eade96d8dcb0642196632067b6dd2e24068c563ac6b8551c6"}, {file = "ciso8601-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:0283884c33dbe0555f9a24749ac947f93eac7b131fdfeeee110ad999947d1680"}, {file = "ciso8601-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0e856903cb6019ab26849af7270ef183b2314f87fd17686a8c98315eff794df"}, {file = "ciso8601-2.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d99297a5925ef3c9ac316cab082c1b1623d976acdb5056fbb8cb12a854116351"}, {file = "ciso8601-2.3.2-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:2e740d2dcac81b5adb0cff641706d5a9e54ff4f3bb7e24437cdacdab3937c0a3"}, {file = "ciso8601-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e883a08b294694313bd3a85c1a136f4326ca26050552742c489159c52e296060"}, {file = "ciso8601-2.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6994b393b1e1147dbc2f13d6d508f6e95b96d7f770299a4af70b7c1d380242c1"}, {file = "ciso8601-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d31a04bea97f21b797fd414b465c00283b70d9523e8e51bc303bec04195a278"}, {file = "ciso8601-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce014a3559592320a2a7a7205257e57dd1277580038a30f153627c5d30ed7a07"}, {file = "ciso8601-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:b069800ea5613eea7d323716133a74bd0fba4a781286167a20639b6628a7e068"}, {file = "ciso8601-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75870a1e496a17e9e8d2ac90125600e1bafe51679d2836b2f6cb66908fef7ad6"}, {file = "ciso8601-2.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:c117c415c43aa3db68ee16a2446cb85c5e88459650421d773f6f6444ce5e5819"}, {file = "ciso8601-2.3.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:ce5f76297b6138dc5c085d4c5a0a631afded99f250233fe583dc365f67fe8a8d"}, {file = "ciso8601-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e3205e4cfd63100f454ea67100c7c6123af32da0022bdc6e81058e95476a8ad"}, {file = "ciso8601-2.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5308a14ac72898f91332ccfded2f18a6c558ccd184ccff84c4fb36c7e4c2a0e6"}, {file = "ciso8601-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e825cb5ecd232775a94ef3c456ab19752ee8e66eaeb20562ea45472eaa8614ec"}, {file = "ciso8601-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a8f96f91bdeabee7ebca2c6e48185bea45e195f406ff748c87a3c9ecefb25cc"}, {file = "ciso8601-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:3fe497819e50a245253a3b2d62ec4c68f8cf337d79dc18e2f3b0a74d24dc5e93"}, {file = "ciso8601-2.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:361a49da3e53811ddc371ff2183d32ee673321899e4653c4d55ed06d0a81ef3d"}, {file = "ciso8601-2.3.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:af26301e0e0cfc6cda225fd2a8b1888bf3828a7d24756774325bda7d29ab2468"}, {file = "ciso8601-2.3.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:af399c2671dfe8fead4f34908a6e6ef3689db9606f2028269b578afd2326b96e"}, {file = "ciso8601-2.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8978a69a6061380b352442160d468915d102c18b0b805a950311e6e0f3b821"}, {file = "ciso8601-2.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:347db58040ad1cb3d2175f5699f0fb1abcb9e894ad744e3460b01bd101bb78a1"}, {file = "ciso8601-2.3.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7860ad2b52007becfd604cfe596f0b7ffa8ffe4f7336b58ef1a2234dc53fa10"}, {file = "ciso8601-2.3.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:91dab638ffaff1da12e0a6de4cfca520430426a1c0eaba5841b1311f45516d49"}, {file = "ciso8601-2.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:2e9a072465ecdbaa3bd2b17e26cc7a0376f9729021c8000656dd97a9343f8723"}, {file = "ciso8601-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fbbe659093d4aef1e66de0ee9a10487439527be4b2f6a6710960f98a41e2cc5"}, {file = "ciso8601-2.3.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:8ccb16db7ca83cc39df3c73285e9ab4920a90f0dbef566f60f0c6cca44becaba"}, {file = "ciso8601-2.3.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:dac06a1bd3c12ab699c29024c5f052e7016cb904e085a5e2b26e6b92fd2dd1dc"}, {file = "ciso8601-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a323aa0143ad8e99d7a0b0ac3005419c505e073e6f850f0443b5994b31a52d14"}, {file = "ciso8601-2.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e9290e7e1b1c3a6df3967e3f1b22c334c980e841f5a1967ab6ef92b30a540d8"}, {file = "ciso8601-2.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc2a6bb31030b875c7706554b99e1d724250e0fc8160aa2f3ae32520b8dccbc5"}, {file = "ciso8601-2.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e137cf862c724a9477b62d89fb8190f141ed6d036f6c4cf824be6d9a7b819e"}, {file = "ciso8601-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:6591d8f191b0a12fa5ac53e1bc0e799f6f2068d0fa5684815706c59a4831f412"}, {file = "ciso8601-2.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecc2f7090e7b8427288b9528fa9571682426f2c7d45d39cf940321192d8796c8"}, {file = "ciso8601-2.3.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c585a05d745c36f974030d1831ed899f8b00afd760f6eff6b8de7eef72cb1336"}, {file = "ciso8601-2.3.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbbe0af7ef440d679ce546f926fc441e31025c6a96c1bb54087df0e5e6c8e021"}, {file = "ciso8601-2.3.2-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69136ef63e7d5178727f358a9cfe4dfda52f132eafcddfa7e6d5933ee1d73b7a"}, {file = "ciso8601-2.3.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff397592a0eadd5e0aec395a285751707c655439abb874ad93e34d04d925ec8d"}, {file = "ciso8601-2.3.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7eb6c8756806f4b8320fe57e3b048dafc54e99af7586160ff9318f35fc521268"}, {file = "ciso8601-2.3.2.tar.gz", hash = "sha256:ec1616969aa46c51310b196022e5d3926f8d3fa52b80ec17f6b4133623bd5434"}, ] [[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 = "freezegun" version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, ] [package.dependencies] python-dateutil = ">=2.7" [[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 = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] [[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.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [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 = "propcache" version = "0.2.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" files = [ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] [[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.10.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] [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 = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.25.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" files = [ {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-freezegun" version = "0.4.2" description = "Wrap tests with fixtures in freeze_time" optional = false python-versions = "*" files = [ {file = "pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949"}, {file = "pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7"}, ] [package.dependencies] freezegun = ">0.3" pytest = ">=3.0.0" [[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 = "python-engineio" version = "4.11.2" description = "Engine.IO server and client for Python" optional = false python-versions = ">=3.6" files = [ {file = "python_engineio-4.11.2-py3-none-any.whl", hash = "sha256:f0971ac4c65accc489154fe12efd88f53ca8caf04754c46a66e85f5102ef22ad"}, {file = "python_engineio-4.11.2.tar.gz", hash = "sha256:145bb0daceb904b4bb2d3eb2d93f7dbb7bb87a6a0c4f20a94cc8654dec977129"}, ] [package.dependencies] simple-websocket = ">=0.10.0" [package.extras] asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] docs = ["sphinx"] [[package]] name = "python-socketio" version = "5.12.1" description = "Socket.IO server and client for Python" optional = false python-versions = ">=3.8" files = [ {file = "python_socketio-5.12.1-py3-none-any.whl", hash = "sha256:24a0ea7cfff0e021eb28c68edbf7914ee4111bdf030b95e4d250c4dc9af7a386"}, {file = "python_socketio-5.12.1.tar.gz", hash = "sha256:0299ff1f470b676c09c1bfab1dead25405077d227b2c13cf217a34dadc68ba9c"}, ] [package.dependencies] aiohttp = {version = ">=3.4", optional = true, markers = "extra == \"asyncio-client\""} bidict = ">=0.21.0" python-engineio = ">=4.11.0" [package.extras] asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] docs = ["sphinx"] [[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 = "setuptools" version = "74.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "simple-websocket" version = "1.0.0" description = "Simple WebSocket server and client for Python" optional = false python-versions = ">=3.6" files = [ {file = "simple-websocket-1.0.0.tar.gz", hash = "sha256:17d2c72f4a2bd85174a97e3e4c88b01c40c3f81b7b648b0cc3ce1305968928c8"}, {file = "simple_websocket-1.0.0-py3-none-any.whl", hash = "sha256:1d5bf585e415eaa2083e2bcf02a3ecf91f9712e7b3e6b9fa0b461ad04e0837bc"}, ] [package.dependencies] wsproto = "*" [package.extras] docs = ["sphinx"] [[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.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, ] [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 = "3.0.2" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=3.8" files = [ {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, ] [package.dependencies] docutils = ">0.18,<0.22" sphinx = ">=6,<9" sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "transifex-client", "twine", "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 = "wsproto" version = "1.2.0" description = "WebSockets state-machine based protocol implementation" optional = false python-versions = ">=3.7.0" files = [ {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, ] [package.dependencies] h11 = ">=0.9.0,<1" [[package]] name = "yarl" version = "1.13.1" description = "Yet another URL library" optional = false python-versions = ">=3.8" files = [ {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad"}, {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4"}, {file = "yarl-1.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c"}, {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05"}, {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7"}, {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae"}, {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a"}, {file = "yarl-1.13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735"}, {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac"}, {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e"}, {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2"}, {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d"}, {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606"}, {file = "yarl-1.13.1-cp310-cp310-win32.whl", hash = "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154"}, {file = "yarl-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88"}, {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51"}, {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e"}, {file = "yarl-1.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc"}, {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495"}, {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2"}, {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38"}, {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74"}, {file = "yarl-1.13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9"}, {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2"}, {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f"}, {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10"}, {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da"}, {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246"}, {file = "yarl-1.13.1-cp311-cp311-win32.whl", hash = "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a"}, {file = "yarl-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2"}, {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9"}, {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe"}, {file = "yarl-1.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419"}, {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57"}, {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b"}, {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c"}, {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220"}, {file = "yarl-1.13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8"}, {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43"}, {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4"}, {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f"}, {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc"}, {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485"}, {file = "yarl-1.13.1-cp312-cp312-win32.whl", hash = "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320"}, {file = "yarl-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799"}, {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550"}, {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c"}, {file = "yarl-1.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71"}, {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1"}, {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813"}, {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da"}, {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851"}, {file = "yarl-1.13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8"}, {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206"}, {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c"}, {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c"}, {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734"}, {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26"}, {file = "yarl-1.13.1-cp313-cp313-win32.whl", hash = "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d"}, {file = "yarl-1.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8"}, {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2"}, {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b"}, {file = "yarl-1.13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23"}, {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d"}, {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8"}, {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e"}, {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99"}, {file = "yarl-1.13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5"}, {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6"}, {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177"}, {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224"}, {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09"}, {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644"}, {file = "yarl-1.13.1-cp38-cp38-win32.whl", hash = "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e"}, {file = "yarl-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d"}, {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c"}, {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85"}, {file = "yarl-1.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049"}, {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5"}, {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465"}, {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf"}, {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b"}, {file = "yarl-1.13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc"}, {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7"}, {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3"}, {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424"}, {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3"}, {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d"}, {file = "yarl-1.13.1-cp39-cp39-win32.whl", hash = "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323"}, {file = "yarl-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093"}, {file = "yarl-1.13.1-py3-none-any.whl", hash = "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0"}, {file = "yarl-1.13.1.tar.gz", hash = "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0"}, ] [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 = "29f3b1946880fa903039d70777552955f89289d99afb9df554499bc23841ce1d" yalexs-8.11.0/pylintrc000066400000000000000000000023371474351555200147150ustar00rootroot00000000000000[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.11.0/pyproject.toml000066400000000000000000000265251474351555200160470ustar00rootroot00000000000000[tool.poetry] name = "yalexs" version = "8.11.0" 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.10.5" aiofiles = ">=23" freenub = ">=0.1.0" typing-extensions = ">=4.5.0" python-socketio = {version = ">=5.11.3", extras = ["asyncio-client"]} propcache = ">=0.0.0" [tool.poetry.group.dev.dependencies] pytest = ">=8" pytest-cov = ">=3,<7" aioresponses = "^0.7.6" requests-mock = "^1.12.1" aiounittest = "^1.4.2" pytest-asyncio = ">=0.24.0" freezegun = "^1.5.1" pytest-freezegun = "^0.4.2" setuptools = "^74.1.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" [tool.ruff.lint] select = [ "A001", # Variable {name} is shadowing a Python builtin "ASYNC210", # Async functions should not call blocking HTTP methods "ASYNC220", # Async functions should not create subprocesses with blocking methods "ASYNC221", # Async functions should not run processes with blocking methods "ASYNC222", # Async functions should not wait on processes with blocking methods "ASYNC230", # Async functions should not open files with blocking methods like open "ASYNC251", # Async functions should not call time.sleep "B002", # Python does not support the unary prefix increment "B005", # Using .strip() with multi-character strings is misleading "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. "B017", # pytest.raises(BaseException) should be considered evil "B018", # Found useless attribute access. Either assign it to a variable or remove it. "B023", # Function definition does not bind loop variable {name} "B026", # Star-arg unpacking after a keyword argument is strongly discouraged "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? "B904", # Use raise from to specify exception cause "B905", # zip() without an explicit strict= parameter "BLE", "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake "F541", # f-string without any placeholders "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format "I", # isort "INP", # flake8-no-pep420 "ISC", # flake8-implicit-str-concat "ICN001", # import concentions; {name} should be imported as {asname} "LOG", # flake8-logging "N804", # First argument of a class method should be named cls "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase "PERF", # Perflint "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style "PTH", # flake8-pathlib "PYI", # flake8-pyi "RET", # flake8-return "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task "RUF007", # Prefer itertools.pairwise() over zip() when iterating over successive pairs "RUF008", # Do not use mutable default values for dataclass attributes "RUF010", # Use explicit conversion flag "RUF013", # PEP 484 prohibits implicit Optional "RUF016", # Slice in indexed access to type {value_type} uses type {index_type} instead of an integer "RUF017", # Avoid quadratic list summation "RUF018", # Avoid assignment expressions in assert statements "RUF019", # Unnecessary key check before dictionary access "RUF020", # {never_like} | T is equivalent to T "RUF021", # Parenthesize a and b expressions when chaining and and or together, to make the precedence clear "RUF022", # Sort __all__ "RUF023", # Sort __slots__ "RUF024", # Do not pass mutable objects as values to dict.fromkeys "RUF026", # default_factory is a positional-only argument to defaultdict "RUF030", # print() call in assert statement is likely unintentional "RUF032", # Decimal() called with float literal argument "RUF033", # __post_init__ method with argument defaults "RUF034", # Useless if-else condition "RUF100", # Unused `noqa` directive "RUF101", # noqa directives that use redirected rule codes "RUF200", # Failed to parse pyproject.toml: {message} "S102", # Use of exec detected "S103", # bad-file-permissions "S108", # hardcoded-temp-file "S306", # suspicious-mktemp-usage "S307", # suspicious-eval-usage "S313", # suspicious-xmlc-element-tree-usage "S314", # suspicious-xml-element-tree-usage "S315", # suspicious-xml-expat-reader-usage "S316", # suspicious-xml-expat-builder-usage "S317", # suspicious-xml-sax-usage "S318", # suspicious-xml-mini-dom-usage "S319", # suspicious-xml-pull-dom-usage "S320", # suspicious-xmle-tree-usage "S601", # paramiko-call "S602", # subprocess-popen-with-shell-equals-true "S604", # call-with-shell-equals-true "S608", # hardcoded-sql-expression "S609", # unix-command-wildcard-injection "SIM", # flake8-simplify "SLF", # flake8-self "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print "TC", # flake8-type-checking "TID", # Tidy imports "TRY", # tryceratops "UP", # pyupgrade "UP031", # Use format specifiers instead of percent format "UP032", # Use f-string instead of `format` call "W", # pycodestyle ] ignore = [ "T201", # used in examples "T203", # used in examples "D102", # Too many to fix now "D103", # Too many to fix now "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring "D205", # Too many to fix now "D213", # Multi-line docstring summary should start at the second line "D400", # Too many to fix now "D401", # Too many to fix right now "D406", # Section name should end with a newline "D407", # Section name underlining "D415", # Too many to fix now "E501", # line too long "PTH118", # Too many to fix now "PTH120", # Too many to fix now "PTH123", # Too many to fix now "PGH003", # Too many to fix now "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception "PT018", # Assertion should be broken down into multiple parts "RUF001", # String contains ambiguous unicode character. "RUF002", # Docstring contains ambiguous unicode character. "RUF003", # Comment contains ambiguous unicode character. "RUF015", # Prefer next(...) over single element slice "SIM102", # Use a single if statement instead of nested if statements "SIM103", # Return the condition {condition} directly "SIM108", # Use ternary operator {contents} instead of if-else-block "SIM115", # Use context handler for opening files # Moving imports into type-checking blocks can mess with pytest.patch() "TC001", # Move application import {} into a type-checking block "TC002", # Move third-party import {} into a type-checking block "TC003", # Move standard library import {} into a type-checking block "TRY003", # Avoid specifying long messages outside the exception class "TRY400", # Use `logging.exception` instead of `logging.error` # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", "E114", "E117", "D206", "D300", "Q", "COM812", "COM819", # Disabled because ruff does not understand type of __all__ generated by a function "PLE0605", "PLC0206", # Too many to fix "PT009", # Too many to fix "PT012", # Too many to fix "PT017", # Too many to fix "PT027", # Too many to fix "TRY004", # Would be a breaking change "D100", # Too many to fix "D101", # Too many to fix "D104", # Too many to fix "D105", # Too many to fix "D107", # Too many to fix "TID252", # Too many to fix "PTH110", # Too many to fix "PTH108", # Too many to fix "SLF001", # Too many to fix ] yalexs-8.11.0/tests/000077500000000000000000000000001474351555200142635ustar00rootroot00000000000000yalexs-8.11.0/tests/__init__.py000066400000000000000000000000001474351555200163620ustar00rootroot00000000000000yalexs-8.11.0/tests/common.py000066400000000000000000000021331474351555200161240ustar00rootroot00000000000000from __future__ import annotations import asyncio import time from asyncio import AbstractEventLoop, TimerHandle from typing import Union _MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution ScheduledType = Union[TimerHandle, tuple[float, TimerHandle]] def get_scheduled_timer_handles(loop: AbstractEventLoop) -> list[TimerHandle]: """Return a list of scheduled TimerHandles.""" handles: list[ScheduledType] = loop._scheduled # type: ignore[attr-defined] return [ handle if isinstance(handle, TimerHandle) else handle[1] for handle in handles ] def fire_time_changed() -> None: timestamp = time.time() loop = asyncio.get_running_loop() for task in list(get_scheduled_timer_handles(loop)): if not isinstance(task, asyncio.TimerHandle): continue if task.cancelled(): continue mock_seconds_into_future = timestamp - time.time() future_seconds = task.when() - (loop.time() + _MONOTONIC_RESOLUTION) if mock_seconds_into_future >= future_seconds: task._run() task.cancel() yalexs-8.11.0/tests/conftest.py000066400000000000000000000000001474351555200164500ustar00rootroot00000000000000yalexs-8.11.0/tests/fixtures/000077500000000000000000000000001474351555200161345ustar00rootroot00000000000000yalexs-8.11.0/tests/fixtures/auto_lock_activity.json000066400000000000000000000004761474351555200227320ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/auto_relock_activity.json000066400000000000000000000014651474351555200232600ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/auto_unlock_activity.json000066400000000000000000000014461474351555200232730ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/bluetooth_lock_activity.json000066400000000000000000000021431474351555200237600ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/door_closed_activity.json000066400000000000000000000013301474351555200232340ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/door_closed_activity_wrong_deviceid.json000066400000000000000000000013331474351555200263070ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/door_closed_activity_wrong_houseid.json000066400000000000000000000013361474351555200261760ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/door_open_activity.json000066400000000000000000000013261474351555200227310ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/doorbell_motion_activity.json000066400000000000000000000024461474351555200241400ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/doorbell_motion_activity_no_image.json000066400000000000000000000014631474351555200257740ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/doorbell_motion_activity_old.json000066400000000000000000000025001474351555200247650ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/doorbell_motion_activity_wrong.json000066400000000000000000000024721474351555200253530ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/finger_lock_activity.json000066400000000000000000000007321474351555200232270ustar00rootroot00000000000000{ "id": "e05b1c22-71d1-44b8-8a4c-e729174a8972", "timestamp": 1727126924000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/finger_lock@3x.png", "action": "finger_lock", "deviceID": "3ED27C5DB830439CAA4F381DC8F16444", "deviceType": "lock", "user": { "UserID": "xxxsasdd-230b-4c6a-8a5c-155043492f3b", "FirstName": "Sample", "LastName": "Person" }, "title": "Sample Person locked Front Door with fingerprint" } yalexs-8.11.0/tests/fixtures/finger_unlock_activity.json000066400000000000000000000007401474351555200235710ustar00rootroot00000000000000{ "id": "e05b1c22-71d1-44b8-8a4c-e729174a8972", "timestamp": 1726770544000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/finger_unlock@3x.png", "action": "finger_unlock", "deviceID": "3ED27C5DB830439CAA4F381DC8F16444", "deviceType": "lock", "user": { "UserID": "xxxsasdd-230b-4c6a-8a5c-155043492f3b", "FirstName": "Sample", "LastName": "Person" }, "title": "Sample Person unlocked Front Door with fingerprint" } yalexs-8.11.0/tests/fixtures/get_doorbell.battery_full.json000066400000000000000000000054321474351555200241670ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_doorbell.battery_low.json000066400000000000000000000054201474351555200240230ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_doorbell.battery_medium.json000066400000000000000000000054311474351555200245040ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_doorbell.json000066400000000000000000000046531474351555200215000ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_doorbell.offline.json000066400000000000000000000065251474351555200231210ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_doorbell_missing_image.json000066400000000000000000000035421474351555200243670ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_doorbells.json000066400000000000000000000043031474351555200216530ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_house_activities.json000066400000000000000000000205271474351555200232430ustar00rootroot00000000000000[ { "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.11.0/tests/fixtures/get_lock.doorsense_init.json000066400000000000000000000052401474351555200236420ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_lock.nostatus_with_doorsense.json000066400000000000000000000020701474351555200256100ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_lock.offline.json000066400000000000000000000026731474351555200222470ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_lock.online.json000066400000000000000000000052411474351555200221030ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_lock.online_with_doorsense.json000066400000000000000000000023401474351555200252140ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_lock.online_with_doorsense_disabled.json000066400000000000000000000023451474351555200270500ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_lock_v2.online.json000066400000000000000000000034141474351555200225120ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_locks.json000066400000000000000000000006311474351555200210010ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/get_pins.json000066400000000000000000000012321474351555200206350ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/homekey_unlock_activity_v4.json000066400000000000000000000007131474351555200243710ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/keypad_lock_activity.json000066400000000000000000000013471474351555200232350ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/linked_lock_activity.json000066400000000000000000000004571474351555200232270ustar00rootroot00000000000000{ "id": "0af6bd11-621b-4e32-b0ea-1d13804d31e7", "timestamp": 1725061953081, "icon": "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/linked_lock@3x.png", "action": "linked_lock", "deviceID": "X", "deviceType": "lock", "title": "Front Door locked by linked operation." } yalexs-8.11.0/tests/fixtures/linked_unlock_activity.json000066400000000000000000000004651474351555200235710ustar00rootroot00000000000000{ "id": "74b5a4b6-8c89-4aac-8eb5-38b0350f4164", "timestamp": 1725062446640, "icon": "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/linked_unlock@3x.png", "action": "linked_unlock", "deviceID": "x", "deviceType": "lock", "title": "Front Door unlocked by linked operation." } yalexs-8.11.0/tests/fixtures/lock.json000066400000000000000000000011501474351555200177540ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/lock_accessory_motion_detect.json000066400000000000000000000005401474351555200247460ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/lock_activity.json000066400000000000000000000012611474351555200216730ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/lock_with_doorbell.online.json000066400000000000000000000052411474351555200241610ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/lock_with_unlatch.online.json000066400000000000000000000051471474351555200240220ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/lock_without_doorstate.json000066400000000000000000000011011474351555200236170ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/manual_lock_activity.json000066400000000000000000000005021474351555200232250ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/manual_unlatch_activity.json000066400000000000000000000005041474351555200237350ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/manual_unlock_activity.json000066400000000000000000000005101474351555200235670ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/pin_lock_activity.json000066400000000000000000000007231474351555200225430ustar00rootroot00000000000000{ "id": "e05b1c22-71d1-44b8-8a4c-e729174a8972", "timestamp": 1665378377000, "icon": "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/pin_lock@3x.png", "action": "pin_lock", "deviceID": "3ED27C5DB830439CAA4F381DC8F16444", "deviceType": "lock", "user": { "UserID": "xxxsasdd-230b-4c6a-8a5c-155043492f3b", "FirstName": "Sample", "LastName": "Person" }, "title": "Sample Person locked Front Door with entry code" } yalexs-8.11.0/tests/fixtures/pin_unlock_activity.json000066400000000000000000000007311474351555200231050ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/pin_unlock_activity_missing_image.json000066400000000000000000000006501474351555200260000ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/pin_unlock_activity_with_image.json000066400000000000000000000007601474351555200253040ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/remote_lock_activity.json000066400000000000000000000013171474351555200232500ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/remote_lock_activity_v4.json000066400000000000000000000007021474351555200236560ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/remote_unlatch_activity_v4.json000066400000000000000000000007211474351555200243650ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/remote_unlock_activity_v4.json000066400000000000000000000007101474351555200242200ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/remote_unlock_activity_v4_2.json000066400000000000000000000011001474351555200244330ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/rf_unlock_activity_v4.json000066400000000000000000000007011474351555200233340ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/unlatch.json000066400000000000000000000011561474351555200204700ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/unlatch_without_doorstate.json000066400000000000000000000011561474351555200243370ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/unlock.json000066400000000000000000000011541474351555200203230ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/unlock_activity.json000066400000000000000000000012601474351555200222350ustar00rootroot00000000000000{ "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.11.0/tests/fixtures/unlock_without_doorstate.json000066400000000000000000000011101474351555200241620ustar00rootroot00000000000000{ "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.11.0/tests/manager/000077500000000000000000000000001474351555200156755ustar00rootroot00000000000000yalexs-8.11.0/tests/manager/__init__.py000066400000000000000000000000001474351555200177740ustar00rootroot00000000000000yalexs-8.11.0/tests/manager/test_activity.py000066400000000000000000000161061474351555200211460ustar00rootroot00000000000000from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock import pytest from freezegun.api import FrozenDateTimeFactory from yalexs.api_async import ApiAsync from yalexs.manager.activity import ( ACTIVITY_DEBOUNCE_COOLDOWN, INITIAL_LOCK_RESYNC_TIME, UPDATE_SOON, ActivityStream, ) from yalexs.manager.gateway import Gateway from ..common import fire_time_changed @pytest.mark.asyncio async def test_activity_stream_debounce(freezer: FrozenDateTimeFactory) -> None: """Test activity stream debounce.""" api = MagicMock(auto_spec=ApiAsync) async_get_house_activities = AsyncMock() api.async_get_house_activities = async_get_house_activities august_gateway = MagicMock(auto_spec=Gateway) august_gateway.async_refresh_access_token_if_needed = AsyncMock() august_gateway.async_get_access_token = AsyncMock() push = MagicMock(connected=False) august_gateway.push = push activity = ActivityStream(api, august_gateway, {"myhouseid"}, push) await activity.async_setup() await asyncio.sleep(0) assert async_get_house_activities.call_count == 1 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() assert async_get_house_activities.call_count == 1 freezer.tick(INITIAL_LOCK_RESYNC_TIME) fire_time_changed() assert async_get_house_activities.call_count == 1 async_get_house_activities.reset_mock() assert "myhouseid" not in activity._schedule_updates activity.async_schedule_house_id_refresh("myhouseid") await asyncio.sleep(0) assert async_get_house_activities.call_count == 0 freezer.tick(UPDATE_SOON) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 1 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 2 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 3 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 3 assert "myhouseid" not in activity._schedule_updates freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 3 assert "myhouseid" not in activity._schedule_updates activity.async_schedule_house_id_refresh("myhouseid") await asyncio.sleep(0) assert activity._pending_updates["myhouseid"] == 3 assert async_get_house_activities.call_count == 3 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 4 assert activity._pending_updates["myhouseid"] == 2 # If we get another update request, be sure we reset # but we do not poll right away and only do 2 polls activity.async_schedule_house_id_refresh("myhouseid") await asyncio.sleep(0) assert activity._pending_updates["myhouseid"] == 2 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert activity._pending_updates["myhouseid"] == 1 assert async_get_house_activities.call_count == 5 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert activity._pending_updates["myhouseid"] == 0 assert async_get_house_activities.call_count == 6 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert activity._pending_updates["myhouseid"] == 0 assert async_get_house_activities.call_count == 6 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 6 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) # If we get another update request later, be sure we reset # and poll after 1s with 3 polls activity.async_schedule_house_id_refresh("myhouseid") await asyncio.sleep(0) assert async_get_house_activities.call_count == 6 assert activity._pending_updates["myhouseid"] == 3 freezer.tick(UPDATE_SOON) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 7 assert activity._pending_updates["myhouseid"] == 2 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 8 assert activity._pending_updates["myhouseid"] == 1 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 9 assert activity._pending_updates["myhouseid"] == 0 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 9 assert activity._pending_updates["myhouseid"] == 0 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 9 assert activity._pending_updates["myhouseid"] == 0 @pytest.mark.asyncio async def test_activity_stream_debounce_during_init( freezer: FrozenDateTimeFactory, ) -> None: """Make sure requests during the initial sync get deferred.""" api = MagicMock(auto_spec=ApiAsync) async_get_house_activities = AsyncMock() api.async_get_house_activities = async_get_house_activities august_gateway = MagicMock(auto_spec=Gateway) august_gateway.async_refresh_access_token_if_needed = AsyncMock() august_gateway.async_get_access_token = AsyncMock() push = MagicMock(connected=False) august_gateway.push = push activity = ActivityStream(api, august_gateway, {"myhouseid"}, push) await activity.async_setup() await asyncio.sleep(0) assert async_get_house_activities.call_count == 1 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 1 activity.async_schedule_house_id_refresh("myhouseid") assert activity._pending_updates["myhouseid"] == 1 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 1 activity.async_schedule_house_id_refresh("myhouseid") assert activity._pending_updates["myhouseid"] == 1 freezer.tick(ACTIVITY_DEBOUNCE_COOLDOWN + 1) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 1 freezer.tick(INITIAL_LOCK_RESYNC_TIME) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 2 assert "myhouseid" not in activity._schedule_updates freezer.tick(INITIAL_LOCK_RESYNC_TIME) fire_time_changed() await asyncio.sleep(0) assert async_get_house_activities.call_count == 2 assert "myhouseid" not in activity._schedule_updates yalexs-8.11.0/tests/manager/test_ratelimit.py000066400000000000000000000011001474351555200212700ustar00rootroot00000000000000import time import pytest from yalexs.exceptions import RateLimited from yalexs.manager.ratelimit import RATE_LIMIT_WAKEUP_INTERVAL, _RateLimitChecker @pytest.mark.asyncio async def test_init_rate_limit(): _RateLimitChecker._client_wakeups.clear() await _RateLimitChecker.check_rate_limit("test") await _RateLimitChecker.register_wakeup("test") with pytest.raises(RateLimited) as exc: await _RateLimitChecker.check_rate_limit("test") assert exc.value.next_allowed == pytest.approx( time.monotonic() + RATE_LIMIT_WAKEUP_INTERVAL ) yalexs-8.11.0/tests/test_activity.py000066400000000000000000000636521474351555200175440ustar00rootroot00000000000000import 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_LINKED_LOCK, ACTION_LINKED_UNLOCK, ACTION_LOCK_AUTO_LOCK, ACTION_LOCK_BLE_LOCK, ACTION_LOCK_BLE_UNLATCH, ACTION_LOCK_BLE_UNLOCK, ACTION_LOCK_DOORBELL_BUTTON_PUSHED, ACTION_LOCK_FINGER_LOCK, ACTION_LOCK_FINGER_UNLOCK, 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_LOCK, 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_LOCK, ACTION_LOCK_PIN_UNLATCH, ACTION_LOCK_PIN_UNLOCK, ACTION_LOCK_MANUAL_LOCK, ACTION_LOCK_MANUAL_UNLATCH, ACTION_LOCK_MANUAL_UNLOCK, ACTION_LINKED_LOCK, ACTION_LINKED_UNLOCK, ACTION_LOCK_FINGER_LOCK, ACTION_LOCK_FINGER_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 def test_linked_unlock_activity_v4(self): manual_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("linked_unlock_activity.json")) ) assert manual_lock_activity.operated_by == "Linked Unlock" assert manual_lock_activity.operated_remote is False assert manual_lock_activity.operated_keypad is False assert manual_lock_activity.operated_manual is False assert manual_lock_activity.operated_tag is False assert ( manual_lock_activity.operator_image_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/linked_unlock@3x.png" ) assert ( manual_lock_activity.operator_thumbnail_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/linked_unlock@3x.png" ) def test_linked_lock_activity_v4(self): manual_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("linked_lock_activity.json")) ) assert manual_lock_activity.operated_by == "Linked Lock" assert manual_lock_activity.operated_remote is False assert manual_lock_activity.operated_keypad is False assert manual_lock_activity.operated_manual is False assert manual_lock_activity.operated_tag is False assert ( manual_lock_activity.operator_image_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/linked_lock@3x.png" ) assert ( manual_lock_activity.operator_thumbnail_url == "https://d3osa7xy9vsc0q.cloudfront.net/app/ActivityFeedIcons/linked_lock@3x.png" ) def test_finger_lock_activity(self): keypad_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("finger_lock_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/finger_lock@3x.png" ) assert ( keypad_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/finger_lock@3x.png" ) def test_finger_unlock_activity(self): keypad_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("finger_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/finger_unlock@3x.png" ) assert ( keypad_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/finger_unlock@3x.png" ) def test_pin_lock_activity(self): keypad_lock_activity = LockOperationActivity( SOURCE_LOG, json.loads(load_fixture("pin_lock_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_lock@3x.png" ) assert ( keypad_lock_activity.operator_thumbnail_url == "https://d33mytkkohwnk6.cloudfront.net/app/ActivityFeedIcons/pin_lock@3x.png" ) yalexs-8.11.0/tests/test_api_async.py000066400000000000000000001416741474351555200176570ustar00rootroot00000000000000import 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()) @pytest.fixture def mock_aioresponse(): with aioresponses() as m: yield m 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: # noqa: BLE001 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 @pytest.mark.parametrize( "status_code", [502, 429], ) @pytest.mark.asyncio async def test_retry_502_429(status_code: int, mock_aioresponse: aioresponses) -> None: last_args = {} attempt = 0 def response_callback(url, **kwargs): nonlocal attempt attempt += 1 last_args.update(kwargs) if attempt == 1: return CallbackResult(status=status_code, body="{}") return CallbackResult(status=200, body="{}") for _ in range(2): mock_aioresponse.post( ApiCommon(DEFAULT_BRAND).get_brand_url( API_VALIDATE_VERIFICATION_CODE_URLS["email"] ), callback=response_callback, ) api = ApiAsync(ClientSession()) with ( patch("yalexs.api_async.API_EXCEPTION_RETRY_TIME", 0), patch("yalexs.api_async.API_RETRY_ATTEMPTS", 2), patch("yalexs.api_async.asyncio.sleep"), ): await api.async_validate_verification_code( ACCESS_TOKEN, "email", "emailaddress", 123456 ) assert last_args["json"] == {"code": "123456", "email": "emailaddress"} assert attempt == 2 yalexs-8.11.0/tests/test_authenticator_async.py000066400000000000000000000214241474351555200217460ustar00rootroot00000000000000import 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.authenticator_async import ( AuthenticationState, AuthenticatorAsync, ValidationResult, ) from yalexs.const import DEFAULT_BRAND, HEADER_AUGUST_ACCESS_TOKEN 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()), # noqa: DTZ003 ): 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.11.0/tests/test_exceptions.py000066400000000000000000000031321474351555200200540ustar00rootroot00000000000000from unittest import mock import pytest from aiohttp import ClientResponseError from yalexs.exceptions import ( AugustApiAIOHTTPError, CannotConnect, InvalidAuth, RequireValidation, YaleApiError, YaleXSError, ) def test_exceptions_can_be_empty_for_back_compat(): assert InvalidAuth() assert str(InvalidAuth()) == "InvalidAuth" assert YaleApiError() assert str(YaleApiError()) == "YaleApiError" assert CannotConnect() assert str(CannotConnect()) == "CannotConnect" assert RequireValidation() assert YaleXSError() assert AugustApiAIOHTTPError() def test_august_api_aio_http_error_reraise(): mock_client_response_error = ClientResponseError( mock.MagicMock(), mock.MagicMock(), status=401, ) ex = AugustApiAIOHTTPError("test", mock_client_response_error) assert str(ex) == "test" assert ex.auth_failed is True assert ex.aiohttp_client_error is mock_client_response_error assert ex.args == ("test",) def test_subclassed_august_api_aio_http_error_reraise(): mock_client_response_error = ClientResponseError( mock.MagicMock(), mock.MagicMock(), status=401, ) with pytest.raises(InvalidAuth, match="test"): try: raise YaleApiError("test", mock_client_response_error) except AugustApiAIOHTTPError as ex: assert str(ex) == "test" assert ex.auth_failed is True assert ex.aiohttp_client_error is mock_client_response_error assert ex.args == ("test",) raise InvalidAuth(ex.args[0], ex) from ex yalexs-8.11.0/tests/test_pubnub_activity.py000066400000000000000000000505041474351555200211070ustar00rootroot00000000000000import 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 / 10_000_000), { "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 / 10_000_000), { "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 / 10_000_000), { "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.11.0/tests/test_util.py000066400000000000000000000366111474351555200166600ustar00rootroot00000000000000import 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.11.0/tox.ini000066400000000000000000000024441474351555200144400ustar00rootroot00000000000000[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.11.0/yalexs/000077500000000000000000000000001474351555200144265ustar00rootroot00000000000000yalexs-8.11.0/yalexs/__init__.py000066400000000000000000000001641474351555200165400ustar00rootroot00000000000000"""Init file for yalexs.""" __author__ = """J. Nick Koston""" __email__ = "nick@koston.org" __version__ = "8.11.0" yalexs-8.11.0/yalexs/_compat.py000066400000000000000000000002721474351555200164230ustar00rootroot00000000000000"""Compat for external lib versions.""" try: from propcache.api import cached_property except ImportError: from propcache import cached_property __all__ = ("cached_property",) yalexs-8.11.0/yalexs/activity.py000066400000000000000000000501621474351555200166400ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from enum import Enum from typing import Any, Union from ._compat import cached_property from .backports.enum import StrEnum 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_LOCK = "pin_lock" 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_LINKED_LOCK = "linked_lock" ACTION_LINKED_UNLOCK = "linked_unlock" ACTION_LOCK_FINGER_LOCK = "finger_lock" ACTION_LOCK_FINGER_UNLOCK = "finger_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_LINKED_LOCK, ACTION_LINKED_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_LOCK, ACTION_LOCK_PIN_UNLATCH, ACTION_LOCK_PIN_UNLOCK, ACTION_LOCK_MANUAL_LOCK, ACTION_LOCK_MANUAL_UNLATCH, ACTION_LOCK_MANUAL_UNLOCK, ACTION_LOCK_FINGER_LOCK, ACTION_LOCK_FINGER_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_LINKED_LOCK: ("Linked", "Lock"), ACTION_LINKED_UNLOCK: ("Linked", "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"), ACTION_LOCK_FINGER_LOCK: ("Fingerprint", "Lock"), ACTION_LOCK_FINGER_UNLOCK: ("Fingerprint", "Unlock"), ACTION_LOCK_PIN_LOCK: ("Pin", "Lock"), ACTION_LOCK_PIN_UNLATCH: ("Pin", "Unlatch"), ACTION_LOCK_PIN_UNLOCK: ("Pin", "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_LOCK, ACTION_LOCK_PIN_UNLATCH, ACTION_LOCK_PIN_UNLOCK, ACTION_LOCK_FINGER_LOCK, ACTION_LOCK_FINGER_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_LINKED_LOCK: LockStatus.LOCKED, ACTION_LINKED_UNLOCK: LockStatus.UNLOCKED, 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_LOCK: LockStatus.LOCKED, 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, ACTION_LOCK_FINGER_LOCK: LockStatus.LOCKED, ACTION_LOCK_FINGER_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 operator_image_url = original elif type(original) is dict: # pylint: disable=unidiomatic-typecheck 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 operator_thumbnail_url = thumbnail elif type(thumbnail) is dict: # pylint: disable=unidiomatic-typecheck 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.11.0/yalexs/alarm.py000066400000000000000000000053521474351555200161010ustar00rootroot00000000000000from __future__ import annotations import logging from typing import Any from ._compat import cached_property from .backports.enum import StrEnum from .device import Device, DeviceDetail class ArmState(StrEnum): Away = "FULL_ARM" Home = "PARTIAL_ARM" Disarm = "DISARM" _LOGGER = logging.getLogger(__name__) class Alarm(Device): """Class to hold details about an alarm.""" def __init__(self, device_id: str, data: dict[str, Any]) -> None: _LOGGER.info("Alarm init - %s", data["location"]) super().__init__(device_id, data["location"], data["houseID"]) self._pubsub_channel = data["pubsubChannel"] self._serial_number = data["serialNumber"] self._status = data["status"] self._areaIDs = data["areaIDs"] @cached_property def pubsub_channel(self): return self._pubsub_channel @cached_property def serial_number(self): return self._serial_number @cached_property def status(self): return self._status @cached_property def areaIDs(self): return self._areaIDs def __repr__(self): return f"Alarm(id={self.device_id}, name={self.device_name}, house_id={self.house_id})" class AlarmDevice(DeviceDetail): """Class to hold details about a device attached to the alarm.""" def __init__(self, data: dict[str, Any]) -> None: _LOGGER.info("Alarm init - %s (%s)", data["name"], data["type"]) super().__init__( data["_id"], data["name"], data["alarmID"], data["serialNumber"], data["status"]["firmwareVersion"], data.get("pubsubChannel"), data, ) self._status: str = data["status"] self._model = data["type"] self._battery_level = 100 if self._status.get("lowBattery", False): self._battery_level = 10 @cached_property def status(self) -> str: return self._status @cached_property def model(self) -> str: return self._model @cached_property def is_online(self) -> bool: return self.status.get("online", False) @cached_property def contact_open(self) -> bool: return self.status.get("contactOpen", False) @cached_property def fault(self) -> bool: return self.status.get("fault", False) @cached_property def tamperOpen(self) -> bool: return self.status.get("tamperOpen", False) @cached_property def battery_level(self) -> int | None: """Return an approximation of the battery percentage.""" return self._battery_level def __repr__(self): return f"AlarmDevice(id={self.device_id}, name={self.device_name}, type={self.model}, alarm_id={self.house_id})" yalexs-8.11.0/yalexs/api_async.py000066400000000000000000000453111474351555200167520ustar00rootroot00000000000000"""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 .activity import ActivityTypes from .alarm import Alarm, AlarmDevice, ArmState 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_alarm_devices_json, _process_alarms_json, _process_doorbells_json, _process_locks_json, ) from .const import DEFAULT_BRAND, HEADER_ACCESS_TOKEN, HEADER_AUGUST_ACCESS_TOKEN from .doorbell import Doorbell, DoorbellDetail from .exceptions import InvalidAuth, YaleApiError 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 def _obscure_headers(headers: dict[str, Any]) -> dict[str, Any]: """Obscure the headers for logging.""" if headers is None: return None for obscure_header in ( "x-august-access-token", "x-access-token", "x-august-api-key", "x-api-key", ): if obscure_header in headers: headers = headers.copy() headers[obscure_header] = "****" return headers 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_get_alarms(self, access_token: str) -> list[Alarm]: if not self.brand_supports_alarms: return [] response = await self._async_dict_to_api( self._build_get_alarms_request(access_token) ) return _process_alarms_json(await response.json()) async def async_get_alarm_devices( self, access_token: str, alarm: Alarm ) -> list[AlarmDevice]: if not self.brand_supports_alarms: return [] response = await self._async_dict_to_api( self._build_get_alarm_devices_request( access_token, alarm_id=alarm.device_id ) ) return _process_alarm_devices_json(await response.json()) async def async_arm_alarm( self, access_token: str, alarm: Alarm, arm_state: ArmState ): if not self.brand_supports_alarms: return {} response = await self._async_dict_to_api( self._build_call_alarm_state_request(access_token, alarm, arm_state) ) return await response.json() 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 return ( response_headers.get(HEADER_ACCESS_TOKEN) or response_headers[HEADER_AUGUST_ACCESS_TOKEN] ) async def async_add_websocket_subscription( self, access_token: str ) -> dict[str, Any]: """Add a websocket subscription.""" response = await self._async_dict_to_api( self._build_websocket_subscribe_request(access_token) ) return await response.json() async def async_get_websocket_subscriptions(self, access_token: str) -> str: """Get websocket subscriptions.""" response = await self._async_dict_to_api( self._build_websocket_get_request(access_token) ) return await response.text() 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, _obscure_headers(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 YaleApiError( 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, _obscure_headers(response.headers), await response.read(), ) if response.status in (429, 502): # 429 - rate limited # 502 - bad gateway _LOGGER.debug( "API sent a %s (attempt: %d), sleeping and trying again", response.status, 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 InvalidAuth( f"Authentication failed: Verify brand is correct: {err.message}", err ) from err if err.status == 422: raise YaleApiError( f"The operation failed because the bridge (connect) is offline: {err.message}", err, ) from err if err.status == 423: raise YaleApiError( f"The operation failed because the bridge (connect) is in use: {err.message}", err, ) from err if err.status == 408: raise YaleApiError( f"The operation timed out because the bridge (connect) failed to respond: {err.message}", err, ) from err raise YaleApiError( f"The operation failed with error code {err.status}: {err.message}.", err ) from err yalexs-8.11.0/yalexs/api_common.py000066400000000000000000000337401474351555200171300ustar00rootroot00000000000000"""Api functions common between sync and async.""" from __future__ import annotations import datetime import logging from functools import cache from typing import Any from ._compat import cached_property from .activity import ACTION_TO_CLASS, SOURCE_LOCK_OPERATE, SOURCE_LOG, ActivityTypes from .alarm import Alarm, AlarmDevice, ArmState from .const import BASE_URLS, BRAND_CONFIG, BRANDING, DEFAULT_BRAND, Brand, BrandConfig from .doorbell import Doorbell from .lock import Lock, LockDoorStatus, determine_door_state, door_state_to_string from .time import parse_datetime 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" API_WEBSOCKET_SUBSCRIBERS = "/websocket/subscribers" API_WEBSOCKET_SUBSCRIBERS_WITH_SUBSCRIBER_ID = "/websocket/subscribers/{subscriber_id}" API_GET_ALARMS_URL = "/users/alarms/mine" API_GET_ALARM_DEVICES_URL = "/alarms/{alarm_id}/devices" API_PUT_ALARM_URL = "/alarms/{alarm_id}/state/{arm_state}" _LOGGER = logging.getLogger(__name__) @cache def _get_brand_config(brand: Brand) -> BrandConfig: return BRAND_CONFIG.get(brand, BRAND_CONFIG[DEFAULT_BRAND]) def api_auth_headers( access_token: str | None = None, brand: Brand | None = None ) -> dict[str, str]: brand_config = _get_brand_config(brand) base_headers = { brand_config.api_key_header: brand_config.api_key, brand_config.branding_header: BRANDING.get(brand, HEADER_VALUE_AUGUST_BRANDING), } if access_token: base_headers[brand_config.access_token_header] = access_token return base_headers def _api_headers( access_token: str | None = None, brand: Brand | None = None ) -> dict[str, str]: headers = api_auth_headers(access_token, brand) headers.update( { HEADER_ACCEPT_VERSION: HEADER_VALUE_ACCEPT_VERSION, HEADER_CONTENT_TYPE: HEADER_VALUE_CONTENT_TYPE, HEADER_AUGUST_COUNTRY: HEADER_VALUE_AUGUST_COUNTRY, } ) 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) return [ activity for activity_json in json_dict if (activity := _activity_from_dict(SOURCE_LOG, activity_json, debug)) ] 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()] def _process_alarms_json(json_dict: list[dict[str, Any]]) -> list[Alarm]: return [Alarm(data.get("alarmID"), data) for data in json_dict] def _process_alarm_devices_json(json_dict: list[dict[str, Any]]) -> list[AlarmDevice]: return [AlarmDevice(data) for data in json_dict] 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 @cached_property def brand_supports_alarms(self) -> bool: """Return if the brand supports alarms.""" return self.brand_config.supports_alarms def get_brand_url(self, url_str: str) -> str: """Get url.""" return f"{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 { **self._build_base_request(access_token, "post"), "url": self.get_brand_url(API_SEND_VERIFICATION_CODE_URLS[login_method]), "json": json, } def _build_base_request( self, access_token: str, method: str = "get" ) -> dict[str, Any]: """Build a base request.""" return {"method": method, "access_token": access_token} def _build_validate_verification_code_request( self, access_token, login_method, username, verification_code ): return { **self._build_base_request(access_token, "post"), "url": self.get_brand_url( API_VALIDATE_VERIFICATION_CODE_URLS[login_method] ), "json": {login_method: username, "code": str(verification_code)}, } def _build_get_doorbells_request(self, access_token: str) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_DOORBELLS_URL), } def _build_get_doorbell_detail_request( self, access_token: str, doorbell_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url( API_GET_DOORBELL_URL.format(doorbell_id=doorbell_id) ), } def _build_wakeup_doorbell_request( self, access_token: str, doorbell_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url( API_WAKEUP_DOORBELL_URL.format(doorbell_id=doorbell_id) ), } def _build_get_houses_request(self, access_token: str) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_HOUSES_URL), } def _build_get_house_request(self, access_token, house_id): return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_HOUSE_URL.format(house_id=house_id)), } def _build_get_house_activities_request(self, access_token, house_id, limit=8): return { **self._build_base_request(access_token), "url": self.get_brand_url( API_GET_HOUSE_ACTIVITIES_URL.format(house_id=house_id) ), "version": "4.0.0", "params": {"limit": limit}, } def _build_get_locks_request(self, access_token: str) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_LOCKS_URL), } def _build_get_user_request(self, access_token: str) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_USER_URL), } def _build_get_lock_detail_request( self, access_token: str, lock_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_LOCK_URL.format(lock_id=lock_id)), } def _build_get_lock_status_request( self, access_token: str, lock_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_LOCK_STATUS_URL.format(lock_id=lock_id)), } def _build_get_pins_request( self, access_token: str, lock_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_PINS_URL.format(lock_id=lock_id)), } def _build_refresh_access_token_request(self, access_token: str) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_HOUSES_URL), } def _build_websocket_subscribe_request(self, access_token: str) -> dict[str, Any]: return { **self._build_base_request(access_token, "post"), "url": self.get_brand_url(API_WEBSOCKET_SUBSCRIBERS), "json": { "scopes": ["lock"], }, } def _build_websocket_get_request( self, access_token: str, subscriber_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token, "get"), "url": self.get_brand_url( API_WEBSOCKET_SUBSCRIBERS_WITH_SUBSCRIBER_ID.format( subscriber_id=subscriber_id ) ), } def _build_websocket_delete_request( self, access_token: str, subscriber_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token, "delete"), "url": self.get_brand_url( API_WEBSOCKET_SUBSCRIBERS_WITH_SUBSCRIBER_ID.format( subscriber_id=subscriber_id ) ), } def _build_call_lock_operation_request( self, url_str: str, access_token: str, lock_id: str, timeout ) -> dict[str, Any]: return { **self._build_base_request(access_token, "put"), "url": self.get_brand_url(url_str.format(lock_id=lock_id)), "timeout": timeout, } def _build_get_alarms_request(self, access_token: str) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url(API_GET_ALARMS_URL), } def _build_get_alarm_devices_request( self, access_token: str, alarm_id: str ) -> dict[str, Any]: return { **self._build_base_request(access_token), "url": self.get_brand_url( API_GET_ALARM_DEVICES_URL.format(alarm_id=alarm_id) ), } def _build_call_alarm_state_request( self, access_token: str, alarm: Alarm, arm_state: ArmState ) -> dict[str, Any]: return { **self._build_base_request(access_token=access_token, method="PUT"), "url": self.get_brand_url( API_PUT_ALARM_URL.format(alarm_id=alarm.device_id, arm_state=arm_state) ), "json": {"areaIDs": alarm.areaIDs}, } yalexs-8.11.0/yalexs/authenticator_async.py000066400000000000000000000123201474351555200210450ustar00rootroot00000000000000from __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) 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._api.brand_config.require_oauth: raise RuntimeError(f"OAuth is required for brand {self._api.brand}") 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.11.0/yalexs/authenticator_common.py000066400000000000000000000125331474351555200212260ustar00rootroot00000000000000from __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"]) # noqa: DTZ004 # 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.11.0/yalexs/backports/000077500000000000000000000000001474351555200164165ustar00rootroot00000000000000yalexs-8.11.0/yalexs/backports/__init__.py000066400000000000000000000000001474351555200205150ustar00rootroot00000000000000yalexs-8.11.0/yalexs/backports/enum.py000066400000000000000000000017751474351555200177460ustar00rootroot00000000000000"""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.11.0/yalexs/backports/tasks.py000066400000000000000000000017031474351555200201160ustar00rootroot00000000000000from __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.11.0/yalexs/bridge.py000066400000000000000000000041121474351555200162320ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from typing import Any from ._compat 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.get("updated") self._last_online = data.get("lastOnline") self._last_offline = data.get("lastOffline") @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.11.0/yalexs/const.py000066400000000000000000000107021474351555200161260ustar00rootroot00000000000000"""Constants.""" from __future__ import annotations from dataclasses import dataclass from .backports.enum import StrEnum class Brand(StrEnum): AUGUST = "august" YALE_ACCESS = "yale_access" YALE_HOME = "yale_home" YALE_GLOBAL = "yale_global" DEFAULT_BRAND = Brand.AUGUST @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 supports_alarms: bool require_oauth: bool base_url: str configuration_url: str pubnub_subscribe_token: str | None pubnub_publish_token: str | None 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, supports_alarms=False, require_oauth=False, base_url="https://api-production.august.com", configuration_url="https://account.august.com", pubnub_subscribe_token="sub-c-1030e062-0ebe-11e5-a5c2-0619f8945a4f", # nosec pubnub_publish_token="pub-c-567d7f2d-270a-438a-a785-f0af12ad8312", # nosec ), 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, supports_alarms=False, require_oauth=False, base_url="https://api-production.august.com", configuration_url="https://account.august.com", pubnub_subscribe_token="sub-c-1030e062-0ebe-11e5-a5c2-0619f8945a4f", # nosec pubnub_publish_token="pub-c-567d7f2d-270a-438a-a785-f0af12ad8312", # nosec ), 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="6e2a2093-6118-42c5-8a41-e1fd25dce7a1", # 🤞 supports_doorbells=True, supports_alarms=True, require_oauth=False, base_url="https://api.aaecosystem.com", configuration_url="https://account.aaecosystem.com", pubnub_subscribe_token="sub-c-c9c38d4d-5796-46c9-9262-af20cf6a1d42", # nosec pubnub_publish_token="pub-c-353e8881-cf58-4b26-9baf-96f296de0677", # nosec ), 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=True, supports_alarms=True, # ?? require_oauth=True, base_url="https://api.aaecosystem.com", configuration_url="https://account.aaecosystem.com", # This brand uses WebSockets and has migrated # away from PubNub for this purpose which is great # because its one less credential we have to expose # to the user pubnub_publish_token=None, pubnub_subscribe_token=None, ), } BRANDS = {brand: brand_config.name for brand, brand_config in BRAND_CONFIG.items()} BRANDS_WITHOUT_OAUTH = { brand: brand_config.name for brand, brand_config in BRAND_CONFIG.items() if not brand_config.require_oauth } BRANDING = { brand: brand_config.branding for brand, brand_config in BRAND_CONFIG.items() } BASE_URLS = { brand: brand_config.base_url for brand, brand_config in BRAND_CONFIG.items() } CONFIGURATION_URLS = { brand: brand_config.configuration_url for brand, brand_config in BRAND_CONFIG.items() } yalexs-8.11.0/yalexs/device.py000066400000000000000000000032511474351555200162400ustar00rootroot00000000000000from __future__ import annotations from typing import Any from ._compat 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.11.0/yalexs/doorbell.py000066400000000000000000000140621474351555200166050ustar00rootroot00000000000000from __future__ import annotations import datetime import logging from typing import Any import requests from aiohttp import ClientSession from yalexs.exceptions import ContentTokenExpired from ._compat 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 f"Doorbell(id={self.device_id}, name={self.device_name}, house_id={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.11.0/yalexs/exceptions.py000066400000000000000000000031041474351555200171570ustar00rootroot00000000000000from __future__ import annotations from http import HTTPStatus from aiohttp import ClientError, ClientResponseError class AugustApiAIOHTTPError(Exception): """An yale access api error with a friendly user consumable string.""" def __init__( self, message: str | None = None, aiohttp_client_error: ClientError | None = None, ) -> None: """Initialize the error.""" super().__init__(message or type(self).__name__) self.aiohttp_client_error = aiohttp_client_error self.status = ( isinstance(aiohttp_client_error, ClientResponseError) and aiohttp_client_error.status ) self.auth_failed = self.status in ( HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, ) class YaleApiError(AugustApiAIOHTTPError): """An yale access api error with a friendly user consumable string.""" class RequireValidation(Exception): """Error to indicate we require validation (2fa).""" class CannotConnect(YaleApiError): """Error to indicate we cannot connect.""" class InvalidAuth(YaleApiError): """Error to indicate there is invalid auth.""" class RateLimited(YaleApiError): """Error to indicate we are rate limited.""" def __init__(self, message: str, next_allowed: float) -> None: """Initialize the error.""" super().__init__(message) self.next_allowed = next_allowed class YaleXSError(Exception): """Base error.""" class ContentTokenExpired(Exception): """Token required for accessing this resource is not valid.""" yalexs-8.11.0/yalexs/keypad.py000066400000000000000000000023751474351555200162640ustar00rootroot00000000000000from ._compat import cached_property from .device import DeviceDetail 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.11.0/yalexs/lock.py000066400000000000000000000210521474351555200157300ustar00rootroot00000000000000from __future__ import annotations import datetime from enum import Enum from ._compat 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 = ("lock", "locked", "kAugLockState_Locked", "kAugLockState_SecureMode") LOCKING_STATUS = ("kAugLockState_Locking",) UNLATCHED_STATUS = ("unlatched", "kAugLockState_Unlatched") UNLATCHING_STATUS = ("kAugLockState_Unlatching",) UNLOCKED_STATUS = ("unlock", "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 f"Lock(id={self.device_id}, name={self.device_name}, house_id={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: str) -> LockStatus: 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.11.0/yalexs/manager/000077500000000000000000000000001474351555200160405ustar00rootroot00000000000000yalexs-8.11.0/yalexs/manager/__init__.py000066400000000000000000000000431474351555200201460ustar00rootroot00000000000000from __future__ import annotations yalexs-8.11.0/yalexs/manager/activity.py000066400000000000000000000304771474351555200202610ustar00rootroot00000000000000"""Consume the august activity stream.""" from __future__ import annotations import asyncio import logging from collections import defaultdict from aiohttp import ClientError from ..activity import Activity, ActivityType from ..api_async import ApiAsync from ..backports.tasks import create_eager_task from ..exceptions import AugustApiAIOHTTPError from ..pubnub_async import AugustPubNub from ..util import get_latest_activity from .const import ACTIVITY_UPDATE_INTERVAL from .gateway import Gateway from .socketio import SocketIORunner 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.0 # How long we expect it to take between when we get a WebSocket/PubNub # message and the activity API to be updated. UPDATE_SOON = 3.0 NEVER_TIME = -86400.0 class ActivityStream(SubscriberMixin): """August activity stream handler.""" def __init__( self, api: ApiAsync, august_gateway: Gateway, house_ids: set[str], push: AugustPubNub | SocketIORunner, ) -> None: """Init activity stream object.""" super().__init__(ACTIVITY_UPDATE_INTERVAL) self._schedule_updates: dict[str, asyncio.TimerHandle] = {} 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.push = push self._update_tasks: dict[str, asyncio.Task] = {} self._last_update_time: dict[str, float] = { house_id: NEVER_TIME for house_id in house_ids } self._start_time: float | None = None self._pending_updates: dict[str, int] = {house_id: 1 for house_id in house_ids} 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 = self._loop.time() await self._async_refresh() await self._async_first_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_all_future_updates() def _async_cancel_future_updates(self, house_id: str) -> None: """Cancel future updates.""" if handle := self._schedule_updates.pop(house_id, None): handle.cancel() self._pending_updates[house_id] = 0 def _async_cancel_all_future_updates(self) -> None: """Cancel all future updates.""" for house_id in self._house_ids: self._async_cancel_future_updates(house_id) 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.push.connected async def _async_refresh(self) -> None: """Update the activity stream from August.""" # This is the only place we refresh the api token if self._shutdown: return await self._august_gateway.async_refresh_access_token_if_needed() if not self.push_updates_connected: _LOGGER.debug("Push updates are not connected, data will be stale") async def _async_first_refresh(self) -> None: """Update the activity stream from August for the first time.""" if self.push_updates_connected: _LOGGER.debug("Skipping update because push updates are active") return _LOGGER.debug("Start retrieving device activities") # Await in sequence to avoid hammering the API for house_id in self._house_ids: if not self._update_running(house_id): await self._create_update_task(house_id) def _create_update_task(self, house_id: str) -> asyncio.Task: """Create an update task.""" if self._update_running(house_id): raise RuntimeError("Update already running") self._update_tasks[house_id] = create_eager_task( self._async_execute_schedule_update(house_id), loop=self._loop ) return self._update_tasks[house_id] def _update_running(self, house_id: str) -> bool: """Return if an update is running for the house id.""" return bool( (current_task := self._update_tasks.get(house_id)) and not current_task.done() ) def _updated_recently(self, house_id: str, now: float) -> bool: """Return if the house id was updated recently.""" return self._last_update_time[house_id] + ACTIVITY_DEBOUNCE_COOLDOWN > now def _async_schedule_update_callback(self, house_id: str) -> None: """Schedule an update callback.""" self._schedule_updates.pop(house_id, None) now = self._loop.time() if delay := self._determine_update_delay(house_id, now, from_callback=True): self._async_schedule_update(house_id, now, delay) return self._create_update_task(house_id) def _determine_update_delay( self, house_id: str, now: float, from_callback: bool = False ) -> float: """Return if we should delay the update.""" if self._updated_recently(house_id, now) or self._update_running(house_id): return ACTIVITY_DEBOUNCE_COOLDOWN if not self._initial_resync_complete(now): return INITIAL_LOCK_RESYNC_TIME return 0 if from_callback else UPDATE_SOON def _async_schedule_update(self, house_id: str, now: float, delay: float) -> None: """Update the activity stream now or in the future if its too soon.""" if self._shutdown or self._pending_updates[house_id] <= 0: return _LOGGER.debug( "Scheduling update for house id %s in %s seconds", house_id, delay ) # Do not update right away because the activities API is # likely not updated yet and we will just get the same # activities again. Instead, schedule the update for # the future. if scheduled := self._schedule_updates.pop(house_id, None): scheduled.cancel() self._schedule_updates[house_id] = self._loop.call_at( now + delay, self._async_schedule_update_callback, house_id ) async def _async_execute_schedule_update(self, house_id: str) -> None: """Execute a scheduled update.""" self._pending_updates[house_id] -= 1 self._last_update_time[house_id] = self._loop.time() await self._async_update_house_id(house_id) if (pending_count := self._pending_updates[house_id]) > 0: _LOGGER.debug( "There are %s pending updates for house id %s", pending_count, house_id ) now = self._loop.time() delay = self._determine_update_delay(house_id, now) self._async_schedule_update(house_id, now, delay) def _initial_resync_complete(self, now: float) -> bool: """Return if the initial resync is complete.""" return self._start_time and now - self._start_time > INITIAL_LOCK_RESYNC_TIME def _set_update_count(self, house_id: str, now: float) -> None: """Set the update count.""" # Schedule one update soon and 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._initial_resync_complete(now): # No resync yet, above spamming the API update_count = 1 elif self._updated_recently(house_id, now) or self._update_running(house_id): # Update running or already updated recently # no point in doing 3 updates as we will # delay anyways update_count = 2 else: # Not updated recently, be sure we do 3 updates # so we do not miss any activity update_count = 3 self._pending_updates[house_id] = update_count 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(house_id) now = self._loop.time() self._set_update_count(house_id, now) delay = self._determine_update_delay(house_id, now) self._async_schedule_update(house_id, now, delay) def _activity_limit(self) -> bool: """Return if the activity limit has been reached.""" if self._did_first_update: return ACTIVITY_STREAM_FETCH_LIMIT return ACTIVITY_CATCH_UP_FETCH_LIMIT async def _async_update_house_id(self, house_id: str) -> None: """Update device activities for a house. Must only be called from _async_execute_schedule_update """ if self._shutdown: return _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=self._activity_limit(), ) except (AugustApiAIOHTTPError, 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.11.0/yalexs/manager/const.py000066400000000000000000000015221474351555200175400ustar00rootroot00000000000000"""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.11.0/yalexs/manager/data.py000066400000000000000000000455261474351555200173370ustar00rootroot00000000000000"""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 .._compat import cached_property 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 from .activity import ActivityStream from .const import MIN_TIME_BETWEEN_DETAIL_UPDATES from .exceptions import CannotConnect, YaleXSError from .gateway import Gateway from .ratelimit import _RateLimitChecker from .socketio import SocketIORunner 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._push_unsub: Callable[[], Coroutine[Any, Any, None]] | None = None self._initial_sync_task: asyncio.Task | None = None self._error_exception_class = error_exception_class self._shutdown: bool = False @cached_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() await _RateLimitChecker.check_rate_limit(token) await _RateLimitChecker.register_wakeup(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 and self.brand is not Brand.YALE_GLOBAL: # 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) push: AugustPubNub | SocketIORunner if self.brand is Brand.YALE_GLOBAL: push = SocketIORunner(self._gateway) else: push = AugustPubNub() for device in self._device_detail_by_id.values(): push.register_device(device) self.activity_stream = ActivityStream( self._api, self._gateway, self._house_ids, push ) await self.activity_stream.async_setup() push.subscribe(self.async_push_message) self._push_unsub = await push.run(user_data["UserID"], 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_push_message( self, device_id: str, date_time: datetime, message: dict[str, Any] ) -> None: """Process a push 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.""" self._shutdown = True 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 if self._push_unsub: await self._push_unsub() @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) -> None: """Refresh data.""" if self._shutdown: return 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: # noqa: PERF203 _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 self._shutdown: return 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.""" token = await self._gateway.async_get_access_token() await _RateLimitChecker.check_rate_limit(token) result = await self._async_status_async(device_id, hyper_bridge) await _RateLimitChecker.register_wakeup(token) return result 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 not in (Brand.YALE_HOME, Brand.YALE_GLOBAL): 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.11.0/yalexs/manager/exceptions.py000066400000000000000000000003131474351555200205700ustar00rootroot00000000000000"""Exceptions for errors.""" from __future__ import annotations from ..exceptions import ( # noqa: F401 CannotConnect, InvalidAuth, RequireValidation, YaleApiError, YaleXSError, ) yalexs-8.11.0/yalexs/manager/gateway.py000066400000000000000000000143131474351555200200550ustar00rootroot00000000000000"""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, RateLimited 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 from .ratelimit import _RateLimitChecker _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: # noqa: C901 """Authenticate with the details provided to setup.""" try: self.authentication = await self.authenticator.async_authenticate() token = await self.async_get_access_token() await _RateLimitChecker.check_rate_limit(token) 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 RateLimited: raise except AugustApiAIOHTTPError as ex: if ex.auth_failed: raise InvalidAuth(ex.args[0], ex.aiohttp_client_error) from ex raise CannotConnect(ex.args[0], ex.aiohttp_client_error) from ex except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: raise InvalidAuth(ex.args[0], ex) from ex raise CannotConnect(ex.args[0], ex) from ex except ClientError as ex: _LOGGER.error("Unable to connect to August service: %s", str(ex)) raise CannotConnect(ex.args[0], ex) 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.11.0/yalexs/manager/ratelimit.py000066400000000000000000000026541474351555200204130ustar00rootroot00000000000000"""Support for August devices.""" from __future__ import annotations import time from collections import defaultdict from ..exceptions import RateLimited RATE_LIMIT_WAKEUP_INTERVAL = 60 * 26 _NEVER_TIME = -RATE_LIMIT_WAKEUP_INTERVAL class RateLimitCheck: """The rate limit is checked locally here to avoid getting blocked. This is a basic rate limit checker that will check the rate limit locally to avoid getting blocked. If rate limiting is not checked locally there is a risk that the client will get permanently blocked by the server. """ def __init__(self) -> None: """Initialize the rate limit checker.""" self._client_wakeups: defaultdict[str, float] = defaultdict(lambda: _NEVER_TIME) async def check_rate_limit(self, token: str) -> None: """Check if the client is rate limited.""" now = time.monotonic() last_time = self._client_wakeups[token] next_allowed = last_time + RATE_LIMIT_WAKEUP_INTERVAL if next_allowed > now: min_until_next_allowed = int((next_allowed - now) / 60) raise RateLimited( f"Rate limited, try again in {min_until_next_allowed} minutes", next_allowed, ) async def register_wakeup(self, token: str) -> None: """Register a wakeup for the client.""" self._client_wakeups[token] = time.monotonic() _RateLimitChecker = RateLimitCheck() yalexs-8.11.0/yalexs/manager/socketio.py000066400000000000000000000066501474351555200202410ustar00rootroot00000000000000import asyncio import logging import sys from collections.abc import Coroutine from contextlib import suppress from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Callable import socketio from ..api_common import api_auth_headers from ..backports.tasks import create_eager_task from ..const import Brand if sys.version_info < (3, 11): UTC = timezone.utc else: from datetime import UTC _LOGGER = logging.getLogger(__name__) if TYPE_CHECKING: from .gateway import Gateway UpdateCallbackType = Callable[[str, datetime, dict[str, Any]], None] class SocketIORunner: """Run the socketio client.""" def __init__(self, gateway: "Gateway") -> None: """Initialize the socketio client.""" self.gateway = gateway self._listeners: set[UpdateCallbackType] = set() self._access_token = None self.connected = False self._subscriber_id: str | None = None self._refresh_task: asyncio.Task | None = None def subscribe(self, callback: UpdateCallbackType) -> Callable[[], None]: """Add a listener.""" self._listeners.add(callback) def _remove_listener(): self._listeners.remove(callback) return _remove_listener def headers(self) -> dict[str, str]: """Get the headers.""" return api_auth_headers(self._access_token, brand=Brand.YALE_GLOBAL) async def _refresh_access_token(self) -> None: """Refresh the access token.""" self._access_token = await self.gateway.async_get_access_token() async def _run(self) -> None: """Run the socketio client.""" sio = socketio.AsyncClient() @sio.event def connect() -> None: _LOGGER.debug("websocket connection established") self.connected = True @sio.event def data(data: dict[str, Any]) -> None: _LOGGER.debug("message received with %s", data) now = datetime.now(UTC) device_id = data.get("lockID") for listener in self._listeners: listener(device_id, now, data) @sio.event def disconnect() -> None: _LOGGER.debug("disconnected from server") self._refresh_task = create_eager_task(self._refresh_access_token()) self.connected = False await sio.connect( f"https://websocket.aaecosystem.com/?subscriberID={self._subscriber_id}", retry=True, transports=["websocket"], headers=self.headers, ) await sio.wait() async def run( self, user_uuid: str, brand: Brand = Brand.YALE_GLOBAL ) -> Callable[[], Coroutine[Any, Any, None]]: """Create a socketio session.""" self._access_token = await self.gateway.async_get_access_token() api = self.gateway.api sub_info = await api.async_add_websocket_subscription(self._access_token) _LOGGER.debug("sub_info: %s", sub_info) self._subscriber_id = sub_info["subscriberID"] _LOGGER.debug("subscriberID: %s", self._subscriber_id) socketio_task = create_eager_task(self._run()) async def _async_unsub(): _LOGGER.debug("Shutting down socketio") socketio_task.cancel() self._listeners.clear() with suppress(asyncio.CancelledError): await socketio_task _LOGGER.debug("socketio stopped") return _async_unsub yalexs-8.11.0/yalexs/manager/subscriber.py000066400000000000000000000060251474351555200205600ustar00rootroot00000000000000"""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.11.0/yalexs/pin.py000066400000000000000000000045451474351555200155760ustar00rootroot00000000000000from __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 f"Pin(id={self.pin_id} firstName={self.first_name}, lastName={self.last_name})" yalexs-8.11.0/yalexs/pubnub_activity.py000066400000000000000000000114421474351555200202110ustar00rootroot00000000000000import 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( # noqa: C901 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 message.get("result") != "failed" ): _LOGGER.debug("Not creating lock activity from status push") 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("lockAction", 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.11.0/yalexs/pubnub_async.py000066400000000000000000000116451474351555200174770ustar00rootroot00000000000000"""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 BRAND_CONFIG, 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.now(datetime.UTC) # 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() async def run( self, user_uuid: str, brand: Brand = Brand.AUGUST ) -> Callable[[], Coroutine[Any, Any, None]]: """Run the pubnub loop.""" brand_config = BRAND_CONFIG[brand] pnconfig = PNConfiguration() pnconfig.subscribe_key = brand_config.pubnub_subscribe_token pnconfig.publish_key = brand_config.pubnub_publish_token pnconfig.uuid = f"pn-{str(user_uuid).upper()}" pnconfig.reconnect_policy = PNReconnectionPolicy.EXPONENTIAL pubnub = PubNubAsyncio(pnconfig) pubnub.add_listener(self) pubnub.subscribe().channels(self.channels).execute() async def _async_unsub(): _LOGGER.debug("Removing listeners PubNub") pubnub.remove_listener(self) _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.11.0/yalexs/time.py000066400000000000000000000010571474351555200157410ustar00rootroot00000000000000from __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 | 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.11.0/yalexs/users.py000066400000000000000000000022131474351555200161370ustar00rootroot00000000000000from __future__ import annotations from typing import Any from ._compat 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.11.0/yalexs/util.py000066400000000000000000000106541474351555200157630ustar00rootroot00000000000000import datetime import random import ssl from functools import cache from typing import TYPE_CHECKING, Optional, Union from .activity import ( ACTION_BRIDGE_OFFLINE, ACTION_BRIDGE_ONLINE, ACTIVITY_ACTION_STATES, ACTIVITY_MOVING_STATES, 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