pax_global_header00006660000000000000000000000064144127710130014512gustar00rootroot0000000000000052 comment=c616ce2f34f7cb074d987cc7d7c752e485a96452 benleb-surepy-1daca1a/000077500000000000000000000000001441277101300150235ustar00rootroot00000000000000benleb-surepy-1daca1a/.github/000077500000000000000000000000001441277101300163635ustar00rootroot00000000000000benleb-surepy-1daca1a/.github/FUNDING.yml000066400000000000000000000000171441277101300201760ustar00rootroot00000000000000github: benleb benleb-surepy-1daca1a/.github/dependabot.yml000066400000000000000000000006511441277101300212150ustar00rootroot00000000000000# 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://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: {interval: "daily"} benleb-surepy-1daca1a/.github/release-drafter.yml000066400000000000000000000000541441277101300221520ustar00rootroot00000000000000template: | ## ๐Ÿ“‹ Changelog $CHANGES benleb-surepy-1daca1a/.github/workflows/000077500000000000000000000000001441277101300204205ustar00rootroot00000000000000benleb-surepy-1daca1a/.github/workflows/codeql-analysis.yml000066400000000000000000000050321441277101300242330ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. name: "CodeQL" on: push: branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: '0 22 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['python'] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 benleb-surepy-1daca1a/.github/workflows/publish-to-pypi.yml_disabled000066400000000000000000000022551441277101300260430ustar00rootroot00000000000000name: Publish Python ๐Ÿ distributions ๐Ÿ“ฆ to PyPI on: push jobs: build-n-publish: name: Build & publish Python ๐Ÿ dist ๐Ÿ“ฆ to PyPI runs-on: ubuntu-18.04 steps: - name: ๐Ÿš› Checkout uses: actions/checkout@v2 - name: ๐Ÿš› Unshallow repo and fetch tags run: git fetch --prune --unshallow origin +refs/tags/*:refs/tags/* - name: ๐Ÿ—๏ธ Set up Python 3.8 uses: actions/setup-python@v1 with: {python-version: 3.8} - name: ๐Ÿ—๏ธ Install pep517 run: python -m pip install pep517 --user - name: ๐ŸŽ Build a binary wheel and a source tarball run: python -m pep517.build --source --binary --out-dir dist/ . - name: Publish distribution ๐Ÿ“ฆ to PyPI if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.pypi_password }} - name: Publish package ๐Ÿ“ฆ to TestPyPI uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.test_pypi_password }} repository_url: https://test.pypi.org/legacy/ benleb-surepy-1daca1a/.github/workflows/release-drafter.yml000066400000000000000000000004711441277101300242120ustar00rootroot00000000000000name: Release Drafter on: push: {branches: [master, main, dev]} pull_request: {branches: [master, main, dev]} workflow_dispatch: jobs: update_release_draft: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v5 env: {GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} benleb-surepy-1daca1a/.gitignore000066400000000000000000000022761441277101300170220ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # 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/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # 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 # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site mkdocs.yml # mypy .mypy_cache/ benleb-surepy-1daca1a/LICENSE000066400000000000000000000020541441277101300160310ustar00rootroot00000000000000MIT License Copyright (c) 2019 Ben Lebherz 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. benleb-surepy-1daca1a/README.md000066400000000000000000000153471441277101300163140ustar00rootroot00000000000000# [![surepy](https://socialify.git.ci/benleb/surepy/image?description=1&descriptionEditable=Library%20%26%20CLI%20to%20interact%20with%20the%20Sure%20Petcare%20API%20to%20monitor%20and%20control%20the%20Sure%20Petcare%20Pet%20Door%2FCat%20Flap%20Connect%20%F0%9F%9A%AA%20and%20the%20Pet%20Feeder%20Connect%20%F0%9F%8D%BD&font=KoHo&forks=1&language=1&logo=https%3A%2F%2Femojipedia-us.s3.dualstack.us-west-1.amazonaws.com%2Fthumbs%2F240%2Fapple%2F237%2Fpaw-prints_1f43e.png&pulls=1&stargazers=1)](https://github.com/benleb/surepy) Library & CLI to interact with the Sure Petcare API. [**surepy**](https://github.com/benleb/surepy) lets you monitor and control the Pet Door/Cat Flap Connect ๐Ÿšช and the Pet Feeder Connect ๐Ÿฝ by [Sure Petcare](https://www.surepetcare.com). --- [**surepy**](https://github.com/benleb/surepy) features: ๐Ÿ”‘ **get an api token** with your account credentials ๐Ÿšช **lock/unlock** a door or flap ๐Ÿพ get the **location** of **pets** & **devices** ๐Ÿˆ get the **state** and more attributes of **pets** & **devices** ๐Ÿ•ฐ๏ธ get **historic** data & events of pets & devices ๐Ÿ“ฌ get a list of (past) **notifications** ## Getting Started [**surepy**](https://github.com/benleb/surepy) is available via [pypi.org](https://pypi.org) ```bash python3 -m pip install --upgrade surepy # or pip install --upgrade surepy ``` there is also a small cli available ```bash $ surepy --help Usage: surepy [OPTIONS] COMMAND [ARGS]... surepy cli ๐Ÿพ https://github.com/benleb/surepy Options: --version show surepy version -j, --json enable json api response output -t, --token TEXT api token --help Show this message and exit. Commands: devices get devices locking lock control notification get notifications pets get pets position set pet position report get pet/household report token get a token ``` >*the cli **is mainly intended for developing & debugging purposes** and probably has bugs - be careful* ๐Ÿพ ## Library example ```python import asyncio from os import environ from pprint import pprint from typing import Dict, List from surepy import Surepy from surepy.entities import SurepyEntity from surepy.entities.devices import SurepyDevice from surepy.entities.pet import Pet async def main(): # # user/password authentication (gets a token in background) # surepy = Surepy(email=user, password=password) # token authentication (token supplied via SUREPY_TOKEN env var) token = environ.get("SUREPY_TOKEN") surepy = Surepy(auth_token=token) # list with all pets pets: List[Pet] = await surepy.get_pets() for pet in pets: print(f"\n\n{pet.name}: {pet.state} | {pet.location}\n") pprint(pet.raw_data()) print(f"\n\n - - - - - - - - - - - - - - - - - - - -\n\n") # all entities as id-indexed dict entities: Dict[int, SurepyEntity] = await surepy.get_entities() # list with alldevices devices: List[SurepyDevice] = await surepy.get_devices() for device in devices: print(f"{device.name = } | {device.serial = } | {device.battery_level = }") print(f"{device.type = } | {device.unique_id = } | {device.id = }") print(f"{entities[device.parent_id].full_name = } | {entities[device.parent_id] = }\n") asyncio.run(main()) ``` --- ## Naming confusion for *surepetcarebeta* users ๐Ÿพ ๐Ÿคช ๐Ÿคฆ Sorry for the bad naming and resulting confusion and chaos ๐Ÿ™„ To "fix" this, I **renamed *surepetcarebeta* to *sureha***. | Name | Repo | Type | Description | Need Help? |---|---|---|---|---| | **[surepy](https://github.com/benleb/surepy) ๐Ÿพ** | [github.com/benleb/surepy](https://github.com/benleb/surepy) | Python Library | Library to interact with the API of Sure Petcare. Also provides Classes for the various Sure Petcare Devicess. Use this if you write an own python tool/app and want to interact with the Sure Petcare API | [Issues](https://github.com/benleb/surepy/issues) | | **[surepetcare](https://www.home-assistant.io/integrations/surepetcare)** ![HA Favicon](https://www.home-assistant.io/images/favicon.ico) | [github.com/home-assistant/core](https://github.com/home-assistant/core) | [Home Assistant](https://github.com/home-assistant/core) Integration | **Official Home Assistant Integration** for the Sure Petcare Devices like Doors, Flaps, Feeders, ... | [Issues](https://github.com/home-assistant/core/issues), [HA Forum](https://community.home-assistant.io) | | | | | | | | **[sureha](https://github.com/benleb/sureha)** ~~surepetcarebeta~~ ~~[benleb/surepetcare](https://github.com/benleb/sureha)~~ | [github.com/benleb/sureha](https://github.com/benleb/sureha) | [Home Assistant](https://github.com/home-assistant/core) Integration | Home Assistant Integration developed in my own repo without reviews from the HA Team. This can be installed via [HACS](https://hacs.xyz/) and is something like a preview integration **for advanced users**. Usually this provides more (experimental) features and faster fixes but lacks the code quality (reviews) and such from HA | [Issues](https://github.com/benleb/sureha/issues) | | | | | | | | **[pethublocal](https://github.com/plambrechtsen/pethublocal)** | [github.com/plambrechtsen/pethublocal](https://github.com/plambrechtsen/pethublocal) | [Home Assistant](https://github.com/home-assistant/core) Integration | Home Assistant Integration developed by [@plambrechtsen](https://github.com/plambrechtsen) which works **completely independent from Sure Petcare**. Check outs his repo for more information! | [Issues](https://github.com/plambrechtsen/pethublocal/issues), [HA Forum](https://community.home-assistant.io) | --- ## Used by * [Sure Petcare](https://www.home-assistant.io/integrations/surepetcare/) integration in [Home Assistant](https://www.home-assistant.io/) Feel free to add you project! ## Acknowledgments * Thanks to all the people who provided information about devices I do not own myself, thanks! * Thanks to [@rcastberg](https://github.com/rcastberg) for hist previous work on the [Sure Petcare](https://www.surepetcare.com) API ([github.com/rcastberg/sure_petcare](https://github.com/rcastberg/sure_petcare)) * Thanks to [@wei](https://github.com/wei) for the header image generator ([github.com/wei/socialify](https://github.com/wei/socialify)) ## Meta **Ben Lebherz**: *cat lover ๐Ÿพ developer & maintainer* - [@benleb](https://github.com/benleb) | [@ben_leb](https://twitter.com/ben_leb) This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details benleb-surepy-1daca1a/poetry.lock000066400000000000000000002571521441277101300172330ustar00rootroot00000000000000[[package]] name = "aiodns" version = "3.0.0" description = "Simple DNS resolver for asyncio" category = "main" optional = false python-versions = "*" [package.dependencies] pycares = ">=4.0.0" [[package]] name = "aiohttp" version = "3.7.4.post0" description = "Async http client/server framework (asyncio)" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] aiodns = {version = "*", optional = true, markers = "extra == \"speedups\""} async-timeout = ">=3.0.1,<5.0" attrs = ">=17.3.0" brotlipy = {version = "*", optional = true, markers = "extra == \"speedups\""} cchardet = {version = "*", optional = true, markers = "extra == \"speedups\""} chardet = ">=2.0,<5.0" multidict = ">=4.5,<7.0" typing-extensions = ">=3.6.5" yarl = ">=1.0,<2.0" [package.extras] speedups = ["aiodns", "brotlipy", "cchardet"] [[package]] name = "astroid" version = "2.7.3" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" wrapt = ">=1.11,<1.13" [[package]] name = "async-timeout" version = "3.0.1" description = "Timeout context manager for asyncio programs" category = "main" optional = false python-versions = ">=3.5.3" [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "bandit" version = "1.7.0" description = "Security oriented static analyser for python code." category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=1.0.1" PyYAML = ">=5.3.1" six = ">=1.10.0" stevedore = ">=1.20.0" [[package]] name = "black" version = "21.9b0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] click = ">=7.1.2" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0,<1" platformdirs = ">=2" regex = ">=2020.1.8" tomli = ">=0.2.6,<2.0.0" typing-extensions = [ {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, ] [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] python2 = ["typed-ast (>=1.4.2)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "brotlipy" version = "0.7.0" description = "Python binding to the Brotli library" category = "main" optional = false python-versions = "*" [package.dependencies] cffi = ">=1.0.0" [[package]] name = "cchardet" version = "2.1.7" description = "cChardet is high speed universal character encoding detector." category = "main" optional = false python-versions = "*" [[package]] name = "certifi" version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = "*" [[package]] name = "cffi" version = "1.14.6" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false python-versions = "*" [package.dependencies] pycparser = "*" [[package]] name = "chardet" version = "4.0.0" description = "Universal encoding detector for Python 2 and 3" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" version = "2.0.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false python-versions = ">=3.5.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "click" version = "8.0.1" description = "Composable command line interface toolkit" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" category = "main" optional = false python-versions = "*" [package.extras] test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" version = "5.5" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.dependencies] toml = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["toml"] [[package]] name = "gitdb" version = "4.0.7" description = "Git Object Database" category = "dev" optional = false python-versions = ">=3.4" [package.dependencies] smmap = ">=3.0.1,<5" [[package]] name = "gitpython" version = "3.1.20" description = "Python Git Library" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] gitdb = ">=4.0.1,<5" typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} [[package]] name = "idna" version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = "*" [[package]] name = "isort" version = "5.9.3" description = "A Python utility / library to sort Python imports." category = "dev" optional = false python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] [[package]] name = "lazy-object-proxy" version = "1.6.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = "*" [[package]] name = "multidict" version = "5.1.0" description = "multidict implementation" category = "main" optional = false python-versions = ">=3.6" [[package]] name = "mypy" version = "0.910" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" toml = "*" typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "dev" optional = false python-versions = "*" [[package]] name = "packaging" version = "21.0" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" [[package]] name = "pathspec" version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pbr" version = "5.6.0" description = "Python Build Reasonableness" category = "dev" optional = false python-versions = ">=2.6" [[package]] name = "platformdirs" version = "2.3.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.6" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycares" version = "4.0.0" description = "Python interface for c-ares" category = "main" optional = false python-versions = "*" [package.dependencies] cffi = ">=1.5.0" [package.extras] idna = ["idna (>=2.1)"] [[package]] name = "pycodestyle" version = "2.7.0" description = "Python style guide checker" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycparser" version = "2.20" description = "C parser in Python" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false python-versions = ">=3.5" [[package]] name = "pylint" version = "2.10.2" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] astroid = ">=2.7.2,<2.8" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" platformdirs = ">=2.2.0" toml = ">=0.7.1" [[package]] name = "pyparsing" version = "2.4.7" description = "Python parsing module" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-cov" version = "2.12.1" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] coverage = ">=5.2.1" pytest = ">=4.6" toml = "*" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] [[package]] name = "pyyaml" version = "5.4.1" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" version = "2021.8.28" description = "Alternative regular expression module, to replace re." category = "dev" optional = false python-versions = "*" [[package]] name = "requests" version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rich" version = "10.9.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false python-versions = ">=3.6,<4.0" [package.dependencies] colorama = ">=0.4.0,<0.5.0" commonmark = ">=0.9.0,<0.10.0" pygments = ">=2.6.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "smmap" version = "4.0.0" description = "A pure Python implementation of a sliding window memory map manager" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "stevedore" version = "3.4.0" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" version = "1.2.1" description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "typing-extensions" version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false python-versions = "*" [[package]] name = "urllib3" version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "wrapt" version = "1.12.1" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false python-versions = "*" [[package]] name = "yarl" version = "1.6.3" description = "Yet another URL library" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] idna = ">=2.0" multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "==3.*,>=3.8.0" content-hash = "80176bb5deeb2f2d234b4d26146348b9f5a4b02f0fefcf8b28e9608a32a7eebc" [metadata.files] aiodns = [ {file = "aiodns-3.0.0-py3-none-any.whl", hash = "sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2"}, {file = "aiodns-3.0.0.tar.gz", hash = "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6"}, ] aiohttp = [ {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, ] astroid = [ {file = "astroid-2.7.3-py3-none-any.whl", hash = "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"}, {file = "astroid-2.7.3.tar.gz", hash = "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c"}, ] async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] bandit = [ {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, ] black = [ {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, ] brotlipy = [ {file = "brotlipy-0.7.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:af65d2699cb9f13b26ec3ba09e75e80d31ff422c03675fcb36ee4dabe588fdc2"}, {file = "brotlipy-0.7.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:50ca336374131cfad20612f26cc43c637ac0bfd2be3361495e99270883b52962"}, {file = "brotlipy-0.7.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:fd1d1c64214af5d90014d82cee5d8141b13d44c92ada7a0c0ec0679c6f15a471"}, {file = "brotlipy-0.7.0-cp27-cp27m-win32.whl", hash = "sha256:5de6f7d010b7558f72f4b061a07395c5c3fd57f0285c5af7f126a677b976a868"}, {file = "brotlipy-0.7.0-cp27-cp27m-win_amd64.whl", hash = "sha256:637847560d671657f993313ecc6c6c6666a936b7a925779fd044065c7bc035b9"}, {file = "brotlipy-0.7.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b4c98b0d2c9c7020a524ca5bbff42027db1004c6571f8bc7b747f2b843128e7a"}, {file = "brotlipy-0.7.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8b39abc3256c978f575df5cd7893153277216474f303e26f0e43ba3d3969ef96"}, {file = "brotlipy-0.7.0-cp33-cp33m-macosx_10_6_intel.whl", hash = "sha256:96bc59ff9b5b5552843dc67999486a220e07a0522dddd3935da05dc194fa485c"}, {file = "brotlipy-0.7.0-cp33-cp33m-manylinux1_i686.whl", hash = "sha256:091b299bf36dd6ef7a06570dbc98c0f80a504a56c5b797f31934d2ad01ae7d17"}, {file = "brotlipy-0.7.0-cp33-cp33m-manylinux1_x86_64.whl", hash = "sha256:0be698678a114addcf87a4b9496c552c68a2c99bf93cf8e08f5738b392e82057"}, {file = "brotlipy-0.7.0-cp33-cp33m-win32.whl", hash = "sha256:d2c1c724c4ac375feb2110f1af98ecdc0e5a8ea79d068efb5891f621a5b235cb"}, {file = "brotlipy-0.7.0-cp33-cp33m-win_amd64.whl", hash = "sha256:3a3e56ced8b15fbbd363380344f70f3b438e0fd1fcf27b7526b6172ea950e867"}, {file = "brotlipy-0.7.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:653faef61241bf8bf99d73ca7ec4baa63401ba7b2a2aa88958394869379d67c7"}, {file = "brotlipy-0.7.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:0fa6088a9a87645d43d7e21e32b4a6bf8f7c3939015a50158c10972aa7f425b7"}, {file = "brotlipy-0.7.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:79aaf217072840f3e9a3b641cccc51f7fc23037496bd71e26211856b93f4b4cb"}, {file = "brotlipy-0.7.0-cp34-cp34m-win32.whl", hash = "sha256:a07647886e24e2fb2d68ca8bf3ada398eb56fd8eac46c733d4d95c64d17f743b"}, {file = "brotlipy-0.7.0-cp34-cp34m-win_amd64.whl", hash = "sha256:c6cc0036b1304dd0073eec416cb2f6b9e37ac8296afd9e481cac3b1f07f9db25"}, {file = "brotlipy-0.7.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:382971a641125323e90486244d6266ffb0e1f4dd920fbdcf508d2a19acc7c3b3"}, {file = "brotlipy-0.7.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:82f61506d001e626ec3a1ac8a69df11eb3555a4878599befcb672c8178befac8"}, {file = "brotlipy-0.7.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:7ff18e42f51ebc9d9d77a0db33f99ad95f01dd431e4491f0eca519b90e9415a9"}, {file = "brotlipy-0.7.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:8ef230ca9e168ce2b7dc173a48a0cc3d78bcdf0bd0ea7743472a317041a4768e"}, {file = "brotlipy-0.7.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:b7cf5bb69e767a59acc3da0d199d4b5d0c9fed7bef3ffa3efa80c6f39095686b"}, {file = "brotlipy-0.7.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:e5c549ae5928dda952463196180445c24d6fad2d73cb13bd118293aced31b771"}, {file = "brotlipy-0.7.0-cp35-abi3-win32.whl", hash = "sha256:79ab3bca8dd12c17e092273484f2ac48b906de2b4828dcdf6a7d520f99646ab3"}, {file = "brotlipy-0.7.0-cp35-abi3-win_amd64.whl", hash = "sha256:ac1d66c9774ee62e762750e399a0c95e93b180e96179b645f28b162b55ae8adc"}, {file = "brotlipy-0.7.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:07194f4768eb62a4f4ea76b6d0df6ade185e24ebd85877c351daa0a069f1111a"}, {file = "brotlipy-0.7.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7e31f7adcc5851ca06134705fcf3478210da45d35ad75ec181e1ce9ce345bb38"}, {file = "brotlipy-0.7.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9448227b0df082e574c45c983fa5cd4bda7bfb11ea6b59def0940c1647be0c3c"}, {file = "brotlipy-0.7.0-cp35-cp35m-win32.whl", hash = "sha256:dc6c5ee0df9732a44d08edab32f8a616b769cc5a4155a12d2d010d248eb3fb07"}, {file = "brotlipy-0.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3c1d5e2cf945a46975bdb11a19257fa057b67591eb232f393d260e7246d9e571"}, {file = "brotlipy-0.7.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:2a80319ae13ea8dd60ecdc4f5ccf6da3ae64787765923256b62c598c5bba4121"}, {file = "brotlipy-0.7.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2699945a0a992c04fc7dc7fa2f1d0575a2c8b4b769f2874a08e8eae46bef36ae"}, {file = "brotlipy-0.7.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1ea4e578241504b58f2456a6c69952c88866c794648bdc74baee74839da61d44"}, {file = "brotlipy-0.7.0-cp36-cp36m-win32.whl", hash = "sha256:2e5c64522364a9ebcdf47c5744a5ddeb3f934742d31e61ebfbbc095460b47162"}, {file = "brotlipy-0.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:09ec3e125d16749b31c74f021aba809541b3564e5359f8c265cbae442810b41a"}, {file = "brotlipy-0.7.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:4e4638b49835d567d447a2cfacec109f9a777f219f071312268b351b6839436d"}, {file = "brotlipy-0.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5664fe14f3a613431db622172bad923096a303d3adce55536f4409c8e2eafba4"}, {file = "brotlipy-0.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1379347337dc3d20b2d61456d44ccce13e0625db2611c368023b4194d5e2477f"}, {file = "brotlipy-0.7.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:22a53ccebcce2425e19f99682c12be510bf27bd75c9b77a1720db63047a77554"}, {file = "brotlipy-0.7.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4bac11c1ffba9eaa2894ec958a44e7f17778b3303c2ee9f99c39fcc511c26668"}, {file = "brotlipy-0.7.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:08a16ebe2ffc52f645c076f96b138f185e74e5d59b4a65e84af17d5997d82890"}, {file = "brotlipy-0.7.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7b21341eab7c939214e457e24b265594067a6ad268305289148ebaf2dacef325"}, {file = "brotlipy-0.7.0-pp226-pp226u-macosx_10_10_x86_64.whl", hash = "sha256:786afc8c9bd67de8d31f46e408a3386331e126829114e4db034f91eacb05396d"}, {file = "brotlipy-0.7.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:890b973039ba26c3ad2e86e8908ab527ed64f9b1357f81a676604da8088e4bf9"}, {file = "brotlipy-0.7.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:4864ac52c116ea3e3a844248a9c9fbebb8797891cbca55484ecb6eed3ebeba24"}, {file = "brotlipy-0.7.0.tar.gz", hash = "sha256:36def0b859beaf21910157b4c33eb3b06d8ce459c942102f16988cca6ea164df"}, ] cchardet = [ {file = "cchardet-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6f70139aaf47ffb94d89db603af849b82efdf756f187cdd3e566e30976c519f"}, {file = "cchardet-2.1.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a25f9577e9bebe1a085eec2d6fdd72b7a9dd680811bba652ea6090fb2ff472f"}, {file = "cchardet-2.1.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6b6397d8a32b976a333bdae060febd39ad5479817fabf489e5596a588ad05133"}, {file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:228d2533987c450f39acf7548f474dd6814c446e9d6bd228e8f1d9a2d210f10b"}, {file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:54341e7e1ba9dc0add4c9d23b48d3a94e2733065c13920e85895f944596f6150"}, {file = "cchardet-2.1.7-cp36-cp36m-win32.whl", hash = "sha256:eee4f5403dc3a37a1ca9ab87db32b48dc7e190ef84601068f45397144427cc5e"}, {file = "cchardet-2.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f86e0566cb61dc4397297696a4a1b30f6391b50bc52b4f073507a48466b6255a"}, {file = "cchardet-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:302aa443ae2526755d412c9631136bdcd1374acd08e34f527447f06f3c2ddb98"}, {file = "cchardet-2.1.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:70eeae8aaf61192e9b247cf28969faef00578becd2602526ecd8ae7600d25e0e"}, {file = "cchardet-2.1.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a39526c1c526843965cec589a6f6b7c2ab07e3e56dc09a7f77a2be6a6afa4636"}, {file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b154effa12886e9c18555dfc41a110f601f08d69a71809c8d908be4b1ab7314f"}, {file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ec3eb5a9c475208cf52423524dcaf713c394393e18902e861f983c38eeb77f18"}, {file = "cchardet-2.1.7-cp37-cp37m-win32.whl", hash = "sha256:50ad671e8d6c886496db62c3bd68b8d55060688c655873aa4ce25ca6105409a1"}, {file = "cchardet-2.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:54d0b26fd0cd4099f08fb9c167600f3e83619abefeaa68ad823cc8ac1f7bcc0c"}, {file = "cchardet-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b59ddc615883835e03c26f81d5fc3671fab2d32035c87f50862de0da7d7db535"}, {file = "cchardet-2.1.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:27a9ba87c9f99e0618e1d3081189b1217a7d110e5c5597b0b7b7c3fedd1c340a"}, {file = "cchardet-2.1.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:90086e5645f8a1801350f4cc6cb5d5bf12d3fa943811bb08667744ec1ecc9ccd"}, {file = "cchardet-2.1.7-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:45456c59ec349b29628a3c6bfb86d818ec3a6fbb7eb72de4ff3bd4713681c0e3"}, {file = "cchardet-2.1.7-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f16517f3697569822c6d09671217fdeab61dfebc7acb5068634d6b0728b86c0b"}, {file = "cchardet-2.1.7-cp38-cp38-win32.whl", hash = "sha256:0b859069bbb9d27c78a2c9eb997e6f4b738db2d7039a03f8792b4058d61d1109"}, {file = "cchardet-2.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:273699c4e5cd75377776501b72a7b291a988c6eec259c29505094553ee505597"}, {file = "cchardet-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:48ba829badef61441e08805cfa474ccd2774be2ff44b34898f5854168c596d4d"}, {file = "cchardet-2.1.7-cp39-cp39-manylinux1_i686.whl", hash = "sha256:bd7f262f41fd9caf5a5f09207a55861a67af6ad5c66612043ed0f81c58cdf376"}, {file = "cchardet-2.1.7-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fdac1e4366d0579fff056d1280b8dc6348be964fda8ebb627c0269e097ab37fa"}, {file = "cchardet-2.1.7-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:80e6faae75ecb9be04a7b258dc4750d459529debb6b8dee024745b7b5a949a34"}, {file = "cchardet-2.1.7-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c96aee9ebd1147400e608a3eff97c44f49811f8904e5a43069d55603ac4d8c97"}, {file = "cchardet-2.1.7-cp39-cp39-win32.whl", hash = "sha256:2309ff8fc652b0fc3c0cff5dbb172530c7abb92fe9ba2417c9c0bcf688463c1c"}, {file = "cchardet-2.1.7-cp39-cp39-win_amd64.whl", hash = "sha256:24974b3e40fee9e7557bb352be625c39ec6f50bc2053f44a3d1191db70b51675"}, {file = "cchardet-2.1.7.tar.gz", hash = "sha256:c428b6336545053c2589f6caf24ea32276c6664cb86db817e03a94c60afa0eaf"}, ] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] cffi = [ {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, ] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] coverage = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] gitdb = [ {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] gitpython = [ {file = "GitPython-3.1.20-py3-none-any.whl", hash = "sha256:b1e1c269deab1b08ce65403cf14e10d2ef1f6c89e33ea7c5e5bb0222ea593b8a"}, {file = "GitPython-3.1.20.tar.gz", hash = "sha256:df0e072a200703a65387b0cfdf0466e3bab729c0458cf6b7349d0e9877636519"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] multidict = [ {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"}, {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"}, {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"}, {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"}, {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"}, {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"}, {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"}, {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"}, {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"}, {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"}, {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"}, {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"}, {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"}, {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"}, {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"}, {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"}, {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"}, {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"}, {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"}, {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"}, {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"}, {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"}, {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"}, {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"}, {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"}, {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"}, {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"}, {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"}, {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"}, {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"}, {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, ] mypy = [ {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pbr = [ {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, ] platformdirs = [ {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pycares = [ {file = "pycares-4.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db5a533111a3cfd481e7e4fb2bf8bef69f4fa100339803e0504dd5aecafb96a5"}, {file = "pycares-4.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fdff88393c25016f417770d82678423fc7a56995abb2df3d2a1e55725db6977d"}, {file = "pycares-4.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0aa97f900a7ffb259be77d640006585e2a907b0cd4edeee0e85cf16605995d5a"}, {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a34b0e3e693dceb60b8a1169668d606c75cb100ceba0a2df53c234a0eb067fbc"}, {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7661d6bbd51a337e7373cb356efa8be9b4655fda484e068f9455e939aec8d54e"}, {file = "pycares-4.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:57315b8eb8fdbc56b3ad4932bc4b17132bb7c7fd2bd590f7fb84b6b522098aa9"}, {file = "pycares-4.0.0-cp36-cp36m-win32.whl", hash = "sha256:dca9dc58845a9d083f302732a3130c68ded845ad5d463865d464e53c75a3dd45"}, {file = "pycares-4.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c95c964d5dd307e104b44b193095c67bb6b10c9eda1ffe7d44ab7a9e84c476d9"}, {file = "pycares-4.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26e67e4f81c80a5955dcf6193f3d9bee3c491fc0056299b383b84d792252fba4"}, {file = "pycares-4.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd3011ffd5e1ad55880f7256791dbab9c43ebeda260474a968f19cd0319e1aef"}, {file = "pycares-4.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1b959dd5921d207d759d421eece1b60416df33a7f862465739d5f2c363c2f523"}, {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6f258c1b74c048a9501a25f732f11b401564005e5e3c18f1ca6cad0c3dc0fb19"}, {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b17ef48729786e62b574c6431f675f4cb02b27691b49e7428a605a50cd59c072"}, {file = "pycares-4.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:82b3259cb590ddd107a6d2dc52da2a2e9a986bf242e893d58c786af2f8191047"}, {file = "pycares-4.0.0-cp37-cp37m-win32.whl", hash = "sha256:4876fc790ae32832ae270c4a010a1a77e12ddf8d8e6ad70ad0b0a9d506c985f7"}, {file = "pycares-4.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f60c04c5561b1ddf85ca4e626943cc09d7fb684e1adb22abb632095415a40fd7"}, {file = "pycares-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:615406013cdcd1b445e5d1a551d276c6200b3abe77e534f8a7f7e1551208d14f"}, {file = "pycares-4.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6580aef5d1b29a88c3d72fe73c691eacfd454f86e74d3fdd18f4bad8e8def98b"}, {file = "pycares-4.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ebb3ba0485f66cae8eed7ce3e9ed6f2c0bfd5e7319d5d0fbbb511064f17e1d4"}, {file = "pycares-4.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c5362b7690ca481440f6b98395ac6df06aa50518ccb183c560464d1e5e2ab5d4"}, {file = "pycares-4.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:eb60be66accc9a9ea1018b591a1f5800cba83491d07e9acc8c56bc6e6607ab54"}, {file = "pycares-4.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:44896d6e191a6b5a914dbe3aa7c748481bf6ad19a9df33c1e76f8f2dc33fc8f0"}, {file = "pycares-4.0.0-cp38-cp38-win32.whl", hash = "sha256:09b28fc7bc2cc05f7f69bf1636ddf46086e0a1837b62961e2092fcb40477320d"}, {file = "pycares-4.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4a5081e232c1d181883dcac4675807f3a6cf33911c4173fbea00c0523687ed4"}, {file = "pycares-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:103353577a6266a53e71bfee4cf83825f1401fefa60f0fb8bdec35f13be6a5f2"}, {file = "pycares-4.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ad6caf580ee69806fc6534be93ddbb6e99bf94296d79ab351c37b2992b17abfd"}, {file = "pycares-4.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3d5e50c95849f6905d2a9dbf02ed03f82580173e3c5604a39e2ad054185631f1"}, {file = "pycares-4.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:53bc4f181b19576499b02cea4b45391e8dcbe30abd4cd01492f66bfc15615a13"}, {file = "pycares-4.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d52f9c725d2a826d5ffa37681eb07ffb996bfe21788590ef257664a3898fc0b5"}, {file = "pycares-4.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3c7fb8d34ee11971c39acfaf98d0fac66725385ccef3bfe1b174c92b210e1aa4"}, {file = "pycares-4.0.0-cp39-cp39-win32.whl", hash = "sha256:e9773e07684a55f54657df05237267611a77b294ec3bacb5f851c4ffca38a465"}, {file = "pycares-4.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:38e54037f36c149146ff15f17a4a963fbdd0f9871d4a21cd94ff9f368140f57e"}, {file = "pycares-4.0.0.tar.gz", hash = "sha256:d0154fc5753b088758fbec9bc137e1b24bb84fc0c6a09725c8bac25a342311cd"}, ] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pycparser = [ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] pygments = [ {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pylint = [ {file = "pylint-2.10.2-py3-none-any.whl", hash = "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"}, {file = "pylint-2.10.2.tar.gz", hash = "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] regex = [ {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"}, {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"}, {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"}, {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"}, {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"}, {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] rich = [ {file = "rich-10.9.0-py3-none-any.whl", hash = "sha256:2c84d9b3459c16bf413fe0f9644c7ae1791971e0bb944dfae56e7c7634b187ab"}, {file = "rich-10.9.0.tar.gz", hash = "sha256:ba285f1c519519490034284e6a9d2e6e3f16dc7690f2de3d9140737d81304d22"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] smmap = [ {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, ] stevedore = [ {file = "stevedore-3.4.0-py3-none-any.whl", hash = "sha256:920ce6259f0b2498aaa4545989536a27e4e4607b8318802d7ddc3a533d3d069e"}, {file = "stevedore-3.4.0.tar.gz", hash = "sha256:59b58edb7f57b11897f150475e7bc0c39c5381f0b8e3fa9f5c20ce6c89ec4aa1"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] yarl = [ {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"}, {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"}, {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"}, {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"}, {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"}, {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"}, {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"}, {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"}, {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"}, {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"}, {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"}, {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"}, {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"}, {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"}, {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"}, {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"}, {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"}, {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"}, {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"}, {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"}, {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"}, {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"}, {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"}, {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"}, {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"}, {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"}, {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"}, {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"}, {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"}, {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"}, {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"}, {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"}, {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"}, {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"}, {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, ] benleb-surepy-1daca1a/pyproject.toml000066400000000000000000000056411441277101300177450ustar00rootroot00000000000000[tool.poetry] authors = ["Ben Lebherz "] classifiers = [ "Topic :: Home Automation", "Topic :: Software Development", "Topic :: Software Development :: Libraries :: Python Modules", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Operating System :: OS Independent", "Environment :: Console", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Development Status :: 4 - Beta", "Development Status :: 5 - Production/Stable", ] description = "Library to interact with the flaps & doors from Sure Petcare." documentation = "https://surepy.readthedocs.io" homepage = "https://github.com/benleb/surepy" keywords = ["cat", "surepetcare", "home-assistant"] license = "MIT" name = "surepy" readme = "README.md" repository = "https://github.com/benleb/surepy" version = "0.8.0" [tool.poetry.dependencies] aiohttp = {extras = ["speedups"], version = "^3.7.4"} async-timeout = ">=3.0.1,<5.0" click = ">=7.1.2,<9.0.0" python = "==3.*,>=3.8.0" requests = "^2.24.0" rich = "^10.1.0" [tool.poetry.dev-dependencies] bandit = "^1.6.2" black = "^21.9b0" coverage = {extras = ["toml"], version = "^5.3.1"} isort = "^5.9.3" mypy = "^0.910" pycodestyle = "^2.6.0" pylint = "^2.10.2" pytest = "^6.2.1" pytest-cov = "^2.12.1" [tool.poetry.scripts] surepy = 'surepy.surecli:cli' [tool.pylint.MASTER] ignore = ["tests", "const.py", "surecli"] disable = ["missing-module-docstring", "missing-class-docstring", "missing-function-docstring"] # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs = 3 load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", "pylint_strict_informational", ] [tool.pylint.BASIC] good-names = ["_", "id", "sp", "at"] [tool.pylint.TYPING] py-version = "3.9" runtime-typing = false [tool.pylint.FORMAT] max-line-length = 100 [tool.isort] balanced_wrapping = true combine_as_imports = true combine_star = true force_grid_wrap = 0 include_trailing_comma = true line_length = 100 lines_after_imports = 2 lines_between_types = 1 multi_line_output = 3 order_by_type = true use_parentheses = true [tool.black] exclude = ''' ( /( \.eggs # exclude a few common directories in the | \.git # root of the project | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ) ''' include = '\.pyi?$' line-length = 100 target_version = ["py39"] [tool.pytest.ini_options] addopts = "--cov --setup-show --code-highlight=yes --verbose" minversion = "6.0" testpaths = ["tests"] [tool.coverage.paths] source = ["surepy"] [tool.coverage.run] branch = true source = ["surepy"] [tool.coverage.report] show_missing = true [build-system] build-backend = "poetry.core.masonry.api" requires = ["poetry_core>=1.0.0"] benleb-surepy-1daca1a/surepy/000077500000000000000000000000001441277101300163525ustar00rootroot00000000000000benleb-surepy-1daca1a/surepy/__init__.py000066400000000000000000000335771441277101300205020ustar00rootroot00000000000000""" surepy ==================================== The core module of surepy |license-info| """ from __future__ import annotations import logging from datetime import datetime from importlib.metadata import version from logging import Logger from math import ceil from typing import Any from uuid import uuid1 import aiohttp from rich.console import Console from surepy.client import SureAPIClient, find_token, token_seems_valid from surepy.const import ( API_TIMEOUT, ATTRIBUTES_RESOURCE as ATTR_RESOURCE, BASE_RESOURCE, HOUSEHOLD_TIMELINE_RESOURCE, MESTART_RESOURCE, NOTIFICATION_RESOURCE, TIMELINE_RESOURCE, ) from surepy.entities import SurepyEntity from surepy.entities.devices import Feeder, Felaqua, Flap, Hub, SurepyDevice from surepy.entities.pet import Pet from surepy.enums import EntityType __version__ = version(__name__) # TOKEN_ENV = "SUREPY_TOKEN" # nosec # TOKEN_FILE = Path("~/.surepy.token").expanduser() # get a logger logger: Logger = logging.getLogger(__name__) console = Console(width=120) def natural_time(duration: int) -> str: """Transforms a number of seconds to a more human-friendly string. Args: duration (int): duration in seconds Returns: str: human-friendly duration string """ duration_h, duration_min = divmod(duration, int(60 * 60)) duration_min, duration_sec = divmod(duration_min, int(60)) # append suitable unit if duration >= 60 * 60 * 24: duration_d, duration_h = divmod(duration_h, int(24)) natural = f"{int(duration_d)}d {int(duration_h)}h {int(duration_min)}m" elif duration >= 60 * 60: if duration_min < 2 or duration_min > 58: natural = f"{int(duration_h)}h" else: natural = f"{int(duration_h)}h {int(duration_min)}m" elif duration > 60: natural = f"{int(duration_min)}min" else: natural = f"{int(duration_sec)}sec" return natural class Surepy: """Communication with the Sure Petcare API.""" def __init__( self, email: str | None = None, password: str | None = None, auth_token: str | None = None, api_timeout: int = API_TIMEOUT, session: aiohttp.ClientSession | None = None, ) -> None: """Initialize the connection to the Sure Petcare API.""" # random device id self._device_id: str = str(uuid1()) self._session = session self.sac = SureAPIClient( email=email, password=password, auth_token=auth_token, api_timeout=api_timeout, session=self._session, surepy_version=__version__, ) # api token management self._auth_token: str | None = None if auth_token and token_seems_valid(auth_token): self._auth_token = auth_token else: # if token := find_token(): self._auth_token = find_token() self.entities: dict[int, SurepyEntity] = {} self._pets: dict[int, Any] = {} self._flaps: dict[int, Any] = {} self._feeders: dict[int, Any] = {} self._hubs: dict[int, Any] = {} self._breeds: dict[int, dict[int, Any]] = {} self._species_breeds: dict[int, dict[int, Any]] = {} self._conditions: dict[int, Any] = {} # storage for received api data self._resource: dict[str, Any] = {} # storage for etags self._etags: dict[str, str] = {} logger.debug("initialization completed | vars(): %s", vars()) @property def auth_token(self) -> str | None: """Authentication token for device""" return self._auth_token async def pets_details(self) -> list[dict[str, Any]] | None: """Fetch pet information.""" return await self.sac.get_pets() async def latest_actions(self, household_id: int) -> dict[int, dict[str, Any]] | None: """ Args: household_id (int): ID associated with household pet_id (int): ID associated with pet Returns: Get the latest action using pet_id and household_id from raw data and output as a dictionary """ return await self.get_actions(household_id=household_id) async def all_actions(self, household_id: int) -> dict[int, dict[str, Any]] | None: """Args: - household_id (int): id associated with household - pet_id (int): id associated with pet """ return await self.get_actions(household_id=household_id) async def get_actions(self, household_id: int) -> dict[int, dict[str, Any]] | None: resource = f"{BASE_RESOURCE}/report/household/{household_id}" latest_actions: dict[int, dict[str, Any]] = {} pet_device_pairs: dict[str, Any] = ( await self.sac.call(method="GET", resource=resource) or {} ) if "data" not in pet_device_pairs: return latest_actions data: list[dict[str, Any]] = pet_device_pairs["data"] for pair in data: pet_id = int(pair["pet_id"]) device_id = int(pair["device_id"]) device: SurepyDevice = self.entities[device_id] # type: ignore latest_actions[pet_id] = {} latest_actions[pet_id] = self.entities[device_id]._data # movement if ( device.type in [EntityType.CAT_FLAP, EntityType.PET_FLAP] and pair["movement"]["datapoints"] ): latest_datapoint = pair["movement"]["datapoints"].pop() # latest_actions[pet_id]["move"] = latest_datapoint latest_actions[pet_id] = self.entities[device_id]._data["move"] = latest_datapoint # feeding elif ( device.type in [EntityType.FEEDER, EntityType.FEEDER_LITE] and pair["feeding"]["datapoints"] ): latest_datapoint = pair["feeding"]["datapoints"].pop() # latest_actions[pet_id]["lunch"] = latest_datapoint latest_actions[pet_id] = self.entities[device_id]._data["lunch"] = latest_datapoint # drinking elif device.type == EntityType.FELAQUA and pair["drinking"]["datapoints"]: latest_datapoint = pair["drinking"]["datapoints"].pop() # latest_actions[pet_id]["drink"] = latest_datapoint latest_actions[pet_id] = self.entities[device_id]._data["drink"] = latest_datapoint return latest_actions async def get_latest_anonymous_drinks(self, household_id: int) -> dict[str, Any] | None: latest_drink: dict[str, float | str | datetime] = {} household_timeline = await self.get_household_timeline(household_id, entries=50) felaqua_related_entries: list[dict[str, Any]] = list( filter( lambda x: x["type"] in [29, 30, 34], # type: ignore household_timeline, # type: ignore ) ) if felaqua_related_entries: try: device_id = felaqua_related_entries[0]["weights"][0]["device_id"] latest_entry_frame = felaqua_related_entries[0]["weights"][0]["frames"][0] remaining = latest_entry_frame["current_weight"] change = latest_entry_frame["change"] updated_at = latest_entry_frame["updated_at"] latest_drink = {"remaining": remaining, "change": change, "date": updated_at} self.entities[device_id]._data["latest_drink"] = latest_drink except (KeyError, TypeError, IndexError): logger.warning( "no water remaining/change events found in household timeline " "(checked last %s entries)", len(household_timeline) or 0, ) return latest_drink async def get_household_timeline( self, household_id: int | None = None, entries: int = 25 ) -> list[dict[str, Any]]: """Fetch Felaqua water level information.""" # pagination as the api gives us at most 25 results per page max_entries_per_page = 25 pages_to_fetch = ceil(entries / max_entries_per_page) current_page = 1 household_timeline = [] while current_page <= pages_to_fetch: resource = HOUSEHOLD_TIMELINE_RESOURCE.format( BASE_RESOURCE=BASE_RESOURCE, household_id=household_id, page=current_page, page_size=max_entries_per_page, ) if timeline := await self.sac.call(method="GET", resource=resource): household_timeline += timeline.get("data", []) current_page += 1 return household_timeline async def get_timeline(self) -> dict[str, Any]: """Retrieve the flap data/state.""" return await self.sac.call(method="GET", resource=TIMELINE_RESOURCE) or {} async def get_notification(self) -> dict[str, Any] | None: """Retrieve the flap data/state.""" return await self.sac.call( method="GET", resource=NOTIFICATION_RESOURCE, timeout=API_TIMEOUT * 2 ) async def get_report(self, household_id: int, pet_id: int | None = None) -> dict[str, Any]: """Retrieve the pet/household report.""" return ( await self.sac.call( method="GET", resource=f"{BASE_RESOURCE}/report/household/{household_id}/pet/{pet_id}", ) if pet_id else await self.sac.call( method="GET", resource=f"{BASE_RESOURCE}/report/household/{household_id}" ) ) or {} async def get_pet(self, pet_id: int) -> Pet | None: if pet_id not in self.entities: await self.get_entities() if self.entities[pet_id].type == EntityType.PET: return self.entities[pet_id] # type: ignore else: return None async def get_pets(self) -> list[Pet]: return [pet for pet in (await self.get_entities()).values() if isinstance(pet, Pet)] async def get_device(self, device_id: int) -> SurepyDevice | None: if device_id not in self.entities: await self.get_entities() if self.entities[device_id].type != EntityType.PET: return self.entities[device_id] # type: ignore else: return None async def get_devices(self) -> list[SurepyDevice]: return [ device for device in (await self.get_entities()).values() if isinstance(device, SurepyDevice) ] async def get_attributes(self) -> dict[str, Any] | None: # fetch additional data from sure petcare attributes: dict[str, Any] | None = None if (raw_data := (await self.sac.call(method="GET", resource=ATTR_RESOURCE))) and ( attributes := raw_data.get("data") ): for breed in attributes.get("breed", {}): self._breeds[breed["id"]] = breed["name"] if breed["species_id"] not in self._breeds: self._species_breeds[breed["species_id"]] = {} self._species_breeds[breed["species_id"]][breed["id"]] = breed["name"] for condition in attributes.get("condition", {}): self._conditions[condition["id"]] = condition["name"] return attributes async def get_entities(self, refresh: bool = False) -> dict[int, SurepyEntity]: """Get all Entities (Pets/Devices)""" household_ids: set[int] = set() felaqua_household_ids: set[int] = set() surepy_entities: dict[int, SurepyEntity] = {} raw_data: dict[str, list[dict[str, Any]]] = {} # get data like species, breed, conditions # await self.get_attributes() if MESTART_RESOURCE not in self.sac.resources or refresh: if response := await self.sac.call(method="GET", resource=MESTART_RESOURCE): raw_data = response.get("data", {}) else: raw_data = self.sac.resources[MESTART_RESOURCE].get("data", {}) if not raw_data: logger.error("could not fetch data ยฏ\\_(ใƒ„)_/ยฏ") return surepy_entities all_entities = raw_data.get("devices", []) + raw_data.get("pets", []) for entity in all_entities: # key used by sure petcare in api response entity_type = EntityType(int(entity.get("product_id", 0))) entity_id = entity["id"] if entity_type in [EntityType.CAT_FLAP, EntityType.PET_FLAP]: surepy_entities[entity_id] = Flap(data=entity) elif entity_type in [EntityType.FEEDER, EntityType.FEEDER_LITE]: surepy_entities[entity_id] = Feeder(data=entity) elif entity_type == EntityType.FELAQUA: surepy_entities[entity_id] = Felaqua(data=entity) felaqua_household_ids.add(int(surepy_entities[entity_id].household_id)) elif entity_type == EntityType.HUB: surepy_entities[entity_id] = Hub(data=entity) elif entity_type == EntityType.PET: surepy_entities[entity_id] = Pet(data=entity) else: logger.warning( "unknown type: %s (%s): %s", entity.get("name", "-"), entity_type, entity ) household_ids.add(surepy_entities[entity_id].household_id) self.entities[entity_id] = surepy_entities[entity_id] # fetch additional data about movement, feeding & drinking for household_id in household_ids: await self.get_actions(household_id=household_id) for household_id in felaqua_household_ids: await self.get_latest_anonymous_drinks(household_id=household_id) # stupid idea, fix this _ = [ feeder.add_bowls() # type: ignore for feeder in surepy_entities.values() if feeder.type == EntityType.FEEDER ] return self.entities benleb-surepy-1daca1a/surepy/client.py000066400000000000000000000350351441277101300202100ustar00rootroot00000000000000""" surepy ==================================== The core module of surepy |license-info| """ from __future__ import annotations import asyncio import logging from datetime import datetime, time from http import HTTPStatus from http.client import HTTPException from logging import Logger from os import environ from pathlib import Path from typing import Any from uuid import uuid1 import aiohttp import async_timeout from .const import ( ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, API_TIMEOUT, AUTH_RESOURCE, AUTHORIZATION, BASE_RESOURCE, CONNECTION, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, CONTROL_RESOURCE, ETAG, HOST, HTTP_HEADER_X_REQUESTED_WITH, ORIGIN, PET_RESOURCE, POSITION_RESOURCE, REFERER, SUREPY_USER_AGENT, USER_AGENT, DEVICE_TAG_RESOURCE, ) from .enums import Location, LockState from .exceptions import SurePetcareAuthenticationError, SurePetcareConnectionError, SurePetcareError TOKEN_ENV = "SUREPY_TOKEN" # nosec TOKEN_FILE = Path("~/.surepy.token").expanduser() # get a logger logger: Logger = logging.getLogger(__name__) def token_seems_valid(token: str) -> bool: """check validity of an api token based on its characters and length Args: token (str): sure petcare api token Returns: bool: True if ``token`` seems valid """ return ( (token is not None) and token.isascii() and token.isprintable() and (320 < len(token) < 448) ) def find_token() -> str | None: token: str | None = None # check env token if (env_token := environ.get(TOKEN_ENV, None)) and token_seems_valid(token=env_token): token = env_token # check file token elif ( TOKEN_FILE.exists() and (file_token := TOKEN_FILE.read_text(encoding="utf-8")) and token_seems_valid(token=file_token) ): token = file_token return token class SureAPIClient: """Communication with the Sure Petcare API.""" def __init__( self, email: str | None = None, password: str | None = None, # loop: Optional[asyncio.AbstractEventLoop] = None, auth_token: str | None = None, api_timeout: int = API_TIMEOUT, session: aiohttp.ClientSession | None = None, surepy_version: str | None = None, ) -> None: """Initialize the connection to the Sure Petcare API.""" self._session = session # sure petcare credentials self.email = email self.password = password # random device id self._device_id: str = str(uuid1()) # connection settings self._api_timeout: int = api_timeout self._surepy_version: str | None = surepy_version # api token management self._auth_token: str | None = None if auth_token and token_seems_valid(auth_token): self._auth_token = auth_token elif token := find_token(): self._auth_token = token else: # no valid credentials/token SurePetcareAuthenticationError("sorry ๐Ÿพ no valid credentials/token found ยฏ\\_(ใƒ„)_/ยฏ") # storage for received api data self.resources: dict[str, Any] = {} # storage for etags self._etags: dict[str, str] = {} logger.debug("initialization completed | vars(): %s", vars()) def _generate_headers(self) -> dict[str, str]: """Build a HTTP header accepted by the API""" user_agent = ( SUREPY_USER_AGENT.format(version=self._surepy_version) if self._surepy_version else None ) return { HOST: "app.api.surehub.io", CONNECTION: "keep-alive", ACCEPT: f"{CONTENT_TYPE_JSON}, {CONTENT_TYPE_TEXT_PLAIN}, */*", ORIGIN: "https://surepetcare.io", USER_AGENT: user_agent if user_agent else SUREPY_USER_AGENT, REFERER: "https://surepetcare.io", ACCEPT_ENCODING: "gzip, deflate", ACCEPT_LANGUAGE: "en-US,en-GB;q=0.9", HTTP_HEADER_X_REQUESTED_WITH: "com.sureflap.surepetcare", AUTHORIZATION: f"Bearer {self._auth_token}", "X-Device-Id": self._device_id, } async def get_token(self) -> str | None: """Get or refresh the authentication token.""" authentication_data: dict[str, str | None] = dict( email_address=self.email, password=self.password, device_id=self._device_id ) token: str | None = None session = self._session if self._session else aiohttp.ClientSession() try: raw_response: aiohttp.ClientResponse = await session.post( url=AUTH_RESOURCE, data=authentication_data, headers=self._generate_headers() ) if raw_response.status == HTTPStatus.OK: response: dict[str, Any] = await raw_response.json() if "data" in response and "token" in response["data"]: token = self._auth_token = response["data"]["token"] elif raw_response.status == HTTPStatus.NOT_MODIFIED: # Etag header matched, no new data available pass elif raw_response.status == HTTPStatus.UNAUTHORIZED: self._auth_token = None raise SurePetcareAuthenticationError() else: logger.debug("Response from %s: %s", AUTH_RESOURCE, raw_response) raise SurePetcareError() return token except asyncio.TimeoutError as error: logger.debug("Timeout while calling %s: %s", AUTH_RESOURCE, error) raise SurePetcareConnectionError() from error except (aiohttp.ClientError, AttributeError) as error: logger.debug("Failed to fetch %s: %s", AUTH_RESOURCE, error) raise SurePetcareError() from error finally: if not self._session: await session.close() async def call( self, method: str, resource: str, data: dict[str, Any] | None = None, json: dict[str, Any] | None = None, second_try: bool = False, **_: Any, ) -> dict[str, Any] | None: """Retrieve the flap data/state.""" # logger.debug("") # logger.debug("๐Ÿพ %s call to: %s", method, resource) # if data: # logger.debug("๐Ÿพ with data: %s", data) if not self._auth_token: self._auth_token = await self.get_token() if method not in ["GET", "PUT", "POST", "DELETE"]: raise HTTPException(f"unknown http method: {method}") response_data = None session = self._session if self._session else aiohttp.ClientSession() try: with async_timeout.timeout(self._api_timeout): headers = self._generate_headers() # use etag if available if resource in self._etags: headers[ETAG] = str(self._etags.get(resource)) # logger.debug("๐Ÿพ \x1b[38;2;255;26;102mยท\x1b[0m etag: %s", headers[ETAG]) await session.options(resource, headers=headers) response: aiohttp.ClientResponse = await session.request( method, resource, headers=headers, data=data, json=json ) if response.status == HTTPStatus.OK or response.status == HTTPStatus.CREATED: self.resources[resource] = response_data = await response.json() if ETAG in response.headers: self._etags[resource] = response.headers[ETAG].strip('"') elif response.status == HTTPStatus.NOT_MODIFIED: # Etag header matched, no new data available logger.debug( "๐Ÿพ \x1b[38;2;0;255;0mยท\x1b[0m %d: etag matched - no new data available", response.status, ) elif response.status == HTTPStatus.UNAUTHORIZED: logger.error( "๐Ÿพ \x1b[38;2;255;26;102mยท\x1b[0m %s %s: %d | %s", method, resource.replace("https://", ""), response.status, response, ) self._auth_token = None if not second_try: token_refreshed = await self.get_token() if token_refreshed: await self.call(method="GET", resource=resource, second_try=True) raise SurePetcareAuthenticationError() else: logger.info( "๐Ÿพ \x1b[38;2;255;0;255mยท\x1b[0m %s %s: %d | %s", method, resource.replace("https://", ""), response.status, response, ) if response_data: responselen = len(response_data.get("data", 0)) else: responselen = 0 logger.debug( "๐Ÿพ \x1b[38;2;0;255;0mยท\x1b[0m %s %s | %d", method, resource.replace("https://", ""), responselen, ) if method == "DELETE" and response.status == HTTPStatus.NO_CONTENT: # TODO: this does not return any data, is there a better way? return "DELETE 204 No Content" return response_data except (asyncio.TimeoutError, aiohttp.ClientError) as error: logger.error("Can not load data from %s", resource) raise SurePetcareConnectionError() from error finally: if not self._session: await session.close() async def get_pets(self) -> list[dict[str, Any]] | None: """Retrieve the pet data/state.""" resource = PET_RESOURCE response_data: list[dict[str, Any]] | None = [] response: dict[str, Any] | None = await self.call(method="GET", resource=resource) if response: response_data = response.get("data") return response_data async def set_pet_location(self, pet_id: int, location: Location) -> dict[str, Any] | None: """Retrieve the flap data/state.""" resource = POSITION_RESOURCE.format(BASE_RESOURCE=BASE_RESOURCE, pet_id=pet_id) if location not in [Location.INSIDE, Location.OUTSIDE]: raise ValueError(f"Unknown location: {location.name.title()}") data = { "where": int(location.value), "since": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), } if (response := await self.call(method="POST", resource=resource, data=data)) and ( response_data := response.get("data") ): desired_state = data.get("where") state = response_data.get("where") # check if the state is correctly updated if state == desired_state: return response raise SurePetcareError(f"Setting position of {pet_id} failed!") async def lock(self, device_id: int) -> dict[str, Any] | None: """Retrieve the flap data/state.""" return await self._set_lock_state(device_id, LockState.LOCKED_ALL) async def lock_in(self, device_id: int) -> dict[str, Any] | None: """Retrieve the flap data/state.""" return await self._set_lock_state(device_id, LockState.LOCKED_IN) async def lock_out(self, device_id: int) -> dict[str, Any] | None: """Retrieve the flap data/state.""" return await self._set_lock_state(device_id, LockState.LOCKED_OUT) async def unlock(self, device_id: int) -> dict[str, Any] | None: """Retrieve the flap data/state.""" return await self._set_lock_state(device_id, LockState.UNLOCKED) async def _set_lock_state(self, device_id: int, mode: LockState) -> dict[str, Any] | None: """Retrieve the flap data/state.""" resource = CONTROL_RESOURCE.format(BASE_RESOURCE=BASE_RESOURCE, device_id=device_id) data = {"locking": int(mode.value)} if ( response := await self.call( method="PUT", resource=resource, device_id=device_id, data=data ) ) and (response_data := response.get("data")): desired_state = data.get("locking") state = response_data.get("locking") # check if the state is correctly updated if state == desired_state: return response # return None raise SurePetcareError("ERROR (UN)LOCKING DEVICE - PLEASE CHECK IMMEDIATELY!") async def set_curfew( self, device_id: int, lock_time: time, unlock_time: time ) -> dict[str, Any] | None: """Set the flap curfew times, using the household's timezone""" resource = CONTROL_RESOURCE.format(BASE_RESOURCE=BASE_RESOURCE, device_id=device_id) data = { "curfew": [ { "lock_time": lock_time.strftime("%H:%M"), "unlock_time": unlock_time.strftime("%H:%M"), "enabled": True, } ] } if ( response := await self.call( method="PUT", resource=resource, device_id=device_id, json=data ) ) and (response_data := response.get("data")): desired_state = data.get("curfew") state = response_data.get("curfew") # check if the state is correctly updated if state == desired_state: return response # return None raise SurePetcareError("ERROR SETTING CURFEW - PLEASE CHECK IMMEDIATELY!") async def _add_tag_to_device(self, device_id: int, tag_id: int) -> dict[str, Any] | None: """Add the specified tag ID to the specified device ID""" resource = DEVICE_TAG_RESOURCE.format(BASE_RESOURCE=BASE_RESOURCE, device_id=device_id, tag_id=tag_id) if( response := await self.call( method="PUT", resource=resource ) ): return response async def _remove_tag_from_device(self, device_id: int, tag_id: int) -> dict[str, Any] | None: """Removes the specified tag ID from the specified device ID""" resource = DEVICE_TAG_RESOURCE.format(BASE_RESOURCE=BASE_RESOURCE, device_id=device_id, tag_id=tag_id) if( response := await self.call( method="DELETE", resource=resource ) ): return response benleb-surepy-1daca1a/surepy/const.py000066400000000000000000000031271441277101300200550ustar00rootroot00000000000000# battery voltages SURE_BATT_VOLTAGE_FULL = 1.6 SURE_BATT_VOLTAGE_LOW = 1.2 SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW # HTTP user agent SUREPY_USER_AGENT = "surepy {version} - https://github.com/benleb/surepy" # Sure Petcare API endpoints BASE_RESOURCE: str = "https://app.api.surehub.io/api" AUTH_RESOURCE: str = f"{BASE_RESOURCE}/auth/login" MESTART_RESOURCE: str = f"{BASE_RESOURCE}/me/start" TIMELINE_RESOURCE: str = f"{BASE_RESOURCE}/timeline" HOUSEHOLD_TIMELINE_RESOURCE: str = "{BASE_RESOURCE}/timeline/household/{household_id}?page={page}" NOTIFICATION_RESOURCE: str = f"{BASE_RESOURCE}/notification" PET_RESOURCE: str = f"{BASE_RESOURCE}/pet?with%5B%5D=photo&with%5B%5D=breed&with%5B%5D=conditions&with%5B%5D=tag&with%5B%5D=food_type&with%5B%5D=species&with%5B%5D=position&with%5B%5D=status" DEVICE_RESOURCE: str = f"{BASE_RESOURCE}/device?with%5B%5D=children&with%5B%5D=tags&with%5B%5D=control&with%5B%5D=status" CONTROL_RESOURCE: str = "{BASE_RESOURCE}/device/{device_id}/control" POSITION_RESOURCE: str = "{BASE_RESOURCE}/pet/{pet_id}/position" ATTRIBUTES_RESOURCE: str = f"{BASE_RESOURCE}/start" DEVICE_TAG_RESOURCE: str = "{BASE_RESOURCE}/device/{device_id}/tag/{tag_id}" API_TIMEOUT = 45 # HTTP constants ACCEPT = "Accept" ACCEPT_ENCODING = "Accept-Encoding" ACCEPT_LANGUAGE = "Accept-Language" AUTHORIZATION = "Authorization" CONNECTION = "Connection" CONTENT_TYPE_JSON = "application/json" CONTENT_TYPE_TEXT_PLAIN = "text/plain" ETAG = "Etag" HOST = "Host" HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" ORIGIN = "Origin" REFERER = "Referer" USER_AGENT = "User-Agent" benleb-surepy-1daca1a/surepy/entities/000077500000000000000000000000001441277101300201765ustar00rootroot00000000000000benleb-surepy-1daca1a/surepy/entities/__init__.py000066400000000000000000000034031441277101300223070ustar00rootroot00000000000000from __future__ import annotations from abc import ABC from dataclasses import dataclass from datetime import datetime from pprint import pformat from typing import Any from surepy.enums import EntityType, Location class SurepyEntity(ABC): def __init__(self, data: dict[str, Any]): # sure petcare id self._id: int = int(data.get("id", data.get("_id"))) # self._sac: SureAPIClient = sac self._data = data self._type = EntityType(int(data.get("product_id", 0))) self._name: str = self._data.get("name", "Unknown") def __str__(self) -> str: return self.__repr__() def __repr__(self) -> str: return f"{self._type.name.capitalize()}(data={pformat(self._data)})" @property def id(self) -> int: return self._id @property def unique_id(self) -> str: return f"{self.household_id}-{self.id}" @property def name(self) -> str: return self._name @property def full_name(self) -> str: return f"{self._type.name}_{self.name}" @property def type(self) -> EntityType: return self._type @property def household_id(self) -> int: """ID of the household the entity belongs to.""" return int(self._data["household_id"]) def raw_data(self) -> dict[str, Any]: return self._data @dataclass class StateFeeding: change: list[float] at: datetime | None @dataclass class StateDrinking: change: list[float] at: datetime | None @dataclass class PetLocationData: where: Location since: datetime | None def __str__(self) -> str: return self.where.name.title() @dataclass class PetActivity(PetLocationData): pass @dataclass class PetLocation(PetLocationData): pass benleb-surepy-1daca1a/surepy/entities/devices.py000066400000000000000000000161531441277101300222000ustar00rootroot00000000000000""" surepy.devices ==================================== ABC representing a Sure Petcare Device. |license-info| """ from __future__ import annotations import logging from abc import ABC from typing import Any from urllib.parse import urlparse from surepy.const import SURE_BATT_VOLTAGE_FULL, SURE_BATT_VOLTAGE_LOW from surepy.entities import SurepyEntity from surepy.enums import BowlPosition, FoodType, LockState # get a logger logger: logging.Logger = logging.getLogger(__name__) class Hub(SurepyEntity): """Sure Petcare Hub.""" @property def online(self) -> bool: return bool(self._data.get("status", {}).get("online")) @property def parent_id(self) -> int | None: return self._data.get("parent_device_id", None) @property def serial(self) -> str | None: """ID of the household the pet belongs to.""" return str(serial) if (serial := self._data.get("serial_number")) else None @property def icon(self) -> str | None: """Picture of the Pet.""" return urlparse("https://surehub.io/assets/images/hub-icon.svg").geturl() class SurepyDevice(SurepyEntity, ABC): """Abstract Surepy base device""" @property def parent_id(self) -> int | None: return self._data.get("parent_device_id", None) @property def serial(self) -> str | None: """ID of the household the pet belongs to.""" return str(serial) if (serial := self._data.get("serial_number")) else None @property def battery_level(self) -> int | None: """Return battery level in percent.""" return self.calculate_battery_level() def calculate_battery_level( self, voltage_full: float = SURE_BATT_VOLTAGE_FULL, voltage_low: float = SURE_BATT_VOLTAGE_LOW, num_batteries: int = 4, ) -> int | None: """Return battery voltage.""" try: voltage_diff = voltage_full - voltage_low battery_voltage = float(self._data["status"]["battery"]) voltage_per_battery = battery_voltage / num_batteries voltage_per_battery_diff = voltage_per_battery - voltage_low # return batterie level between 0 and 100 return max(min(int(voltage_per_battery_diff / voltage_diff * 100), 100), 0) except (KeyError, TypeError, ValueError) as error: logger.debug("error while calculating battery level: %s", error) return None class FeederBowl: """Sure Petcare Felaqua.""" def __init__(self, data: dict[str, int | float | str], feeder: Feeder): """Initialize a Sure Petcare sensor.""" self._data: dict[str, int | float | str] = data self._name = f"{feeder.name} Bowl {self._data['index']}" @property def name(self) -> str: return self._name @property def weight(self) -> float: return float(self._data["weight"]) @property def change(self) -> float: return float(self._data["change"]) @property def target(self) -> int | None: return int(self._data["target"]) if "target" in self._data else None @property def index(self) -> int | None: return int(self._data["index"]) if "index" in self._data else None @property def food_type_id(self) -> int | None: return int(self._data["food_type_id"]) if "food_type_id" in self._data else None @property def food_type(self) -> str | None: return FoodType(self.food_type_id).name.capitalize() if self.food_type_id else None @property def position(self) -> str | None: return BowlPosition(self.index).name.capitalize() if self.index else None def raw_data(self) -> dict[str, int | float | str]: return self._data class Tag: """Tags assigned to a device.""" def __init__(self, data: dict[str, int | float | str], feeder: Feeder): """Initialize a Sure Petcare sensor.""" self._data: dict[str, int | float | str] = data @property def id(self) -> int: return int(self._data["id"]) def index(self) -> int: return int(self._data["index"]) def profile(self) -> int: return int(self._data["profile"]) def version(self) -> str: return self._data["version"] def created_at(self) -> str: return str(self._data["created_at"]) def updated_at(self) -> str: return self._data["updated_at"] def raw_data(self) -> dict[str, int | float | str]: return self._data class Feeder(SurepyDevice): """Sure Petcare Cat- or Pet-Flap.""" def __init__(self, data: dict[str, Any]): """Initialize a Sure Petcare sensor.""" super().__init__(data) self.bowls: dict[int, FeederBowl] = {} self.add_bowls() self.tags: dict[int, Tag] = {} self.add_tags() @property def bowl_count(self) -> int: return len(self.bowls) @property def total_weight(self) -> float: return sum([bowl.weight or 0.0 for bowl in self.bowls.values() if bowl.weight > 0.0]) def add_bowls(self) -> None: if lunch := self._data.get("lunch"): for bowl in lunch.get("weights", []): self.bowls[bowl["index"]] = FeederBowl(data=bowl, feeder=self) @property def icon(self) -> str | None: """Icon of the Felaqua.""" return urlparse("https://surehub.io/assets/images/feeder-left-menu.png").geturl() def add_tags(self) -> None: if tags := self._data.get("tags"): for tag in tags: self.tags[tag["index"]] = Tag(data=tag, feeder=self) class Felaqua(SurepyDevice): """Sure Petcare Cat- or Pet-Flap.""" @property def water_remaining(self) -> float | None: remaining = None try: remaining = float(self._data["latest_drink"]["remaining"]) except (KeyError, TypeError): pass return remaining @property def water_change(self) -> float | None: change = None try: change = float(self._data["latest_drink"]["change"]) except (KeyError, TypeError): pass return change @property def icon(self) -> str | None: """Icon of the Felaqua.""" return urlparse("https://surehub.io/assets/images/poseidon-left-menu.png").geturl() class Flap(SurepyDevice): """Sure Petcare Cat- or Pet-Flap.""" @property def state(self) -> LockState: return LockState(self._data["status"]["locking"]["mode"]) @property def unlocked(self) -> bool: return self.state in [LockState.UNLOCKED, LockState.CURFEW_UNLOCKED] @property def icon(self) -> str | None: """Icon of the Pet/Cap Flap.""" icon_url = "https://surehub.io/assets/images/petdoor-left-menu.png" if self.state == LockState.LOCKED_ALL: icon_url = "https://surehub.io/assets/images/both-ways-icon.svg" elif self.state == LockState.LOCKED_IN: icon_url = "https://surehub.io/assets/images/inside-icon.svg" elif self.state == LockState.LOCKED_OUT: icon_url = "https://surehub.io/assets/images/outside-icon.svg" return urlparse(icon_url).geturl() benleb-surepy-1daca1a/surepy/entities/pet.py000066400000000000000000000100371441277101300213410ustar00rootroot00000000000000""" surepy.pet ==================================== The `Pet` class of surepy |license-info| """ from __future__ import annotations from datetime import datetime from typing import Any from urllib.parse import urlparse from surepy.entities import PetActivity, PetLocation, StateDrinking, StateFeeding, SurepyEntity from surepy.entities.states import PetState from surepy.enums import EntityType, FoodType, Location class Pet(SurepyEntity): """ Represents pet. Contains attributes of the pet. Attributes obtained through Sure PetCare API and include - pet ID (int) provided by Sure PetCare - pet type default always "pet" - pet data: raw output from Sure PetCare API - pet name (string) name of pet - pet state (integer in API, string in script), one of "outside" or "home" """ def __init__(self, data: dict[str, Any]): super().__init__(data=data) self.pet_id: int = int(data["id"]) self._type: EntityType = EntityType.PET self._data: dict[str, Any] = data self._name = str(name) if (name := self._data.get("name")) else "Unnamed" self.state = PetState(data["status"]) if "status" in data else "Unknown" @property def id(self) -> int: """ID of the household the pet belongs to.""" return self.pet_id @property def tag_id(self) -> int | None: """ID of the household the pet belongs to.""" return int(tag_id) if (tag_id := self._data.get("tag_id")) else None @property def food_type(self) -> str | None: """Type of food.""" # pylint: disable=used-before-assignment return str(FoodType(type_id)) if (type_id := self._data.get("food_type_id")) else None @property def updated_at(self) -> datetime | None: """Type of food.""" return ( datetime.fromisoformat(updated_at) if (updated_at := self._data.get("updated_at")) else None ) @property def photo_url(self) -> str | None: """Picture of the Pet.""" picture_url = ( photo_url if (photo_url := self._data.get("photo", {}).get("location")) else "https://surehub.io/assets/images/no-pet-pic-dark.svg" ) return urlparse(picture_url).geturl() @property def at_home(self) -> bool: """Location of the Pet.""" return bool(self.location.where == Location.INSIDE) @property def location(self) -> PetLocation: """Location of the Pet.""" position = self._data.get("position", {}) # pylint: disable=no-member return PetLocation( where=Location(position.get("where", Location.UNKNOWN.value)), since=position.get("since", None), ) @property def activity(self) -> PetActivity: """Last Activity of the Pet.""" activity = self._data.get("status", {}).get("activity", {}) # pylint: disable=no-member return PetActivity( where=Location(activity.get("where", Location.UNKNOWN.value)), since=activity.get("since", None), ) @property def feeding(self) -> StateFeeding | None: """Last Activity of the Pet.""" if activity := self._data.get("status", {}).get("feeding", {}): return StateFeeding( change=activity.get("change", [0.0, 0.0]), at=datetime.fromisoformat(activity.get("at", None)), ) return None @property def drinking(self) -> StateDrinking | None: """Last Activity of the Pet.""" if activity := self._data.get("status", {}).get("drinking", {}): return StateDrinking( change=activity.get("change", [0.0]), at=datetime.fromisoformat(activity.get("at", None)), ) return None @property def last_lunch(self) -> datetime | None: return self.feeding.at if self.feeding else None @property def last_drink(self) -> datetime | None: return self.drinking.at if self.drinking else None benleb-surepy-1daca1a/surepy/entities/states.py000066400000000000000000000037421441277101300220610ustar00rootroot00000000000000""" surepy.entities.states ==================================== Classes representing pet states. |license-info| """ from __future__ import annotations from abc import ABC from datetime import datetime from typing import Any from surepy.enums import Location class PetState(ABC): """abstract surepy state.""" def __init__(self, state: dict[str, dict[str, Any]]): self.activity: ActivityState | None = ( ActivityState(state=state["activity"]) if "activity" in state else None ) self.drinking: DrinkingState | None = ( DrinkingState(state=state["drinking"]) if "drinking" in state else None ) self.feeding: FeedingState | None = ( FeedingState(state=state["feeding"]) if "feeding" in state else None ) class ActivityState: """surepy activity state.""" def __init__(self, state: dict[str, Any]): self.device_id = state.get("device_id") self.tag_id = state.get("tag_id") self.since: datetime = ( datetime.fromisoformat(state["at"]) if isinstance(state.get("at", None), str) else None ) self.where: Location = Location(state["where"]) class DrinkingState: """surepy drinking state.""" def __init__(self, state: dict[str, Any]): self.device_id = state.get("device_id") self.tag_id = state.get("tag_id") self.at: datetime = datetime.fromisoformat(state.get("at")) self.change: float = state["change"] if "change" in state else None class FeedingState: """surepy feeding state.""" def __init__(self, state: dict[str, Any]): self.device_id = state.get("device_id") self.tag_id = state.get("tag_id") self.at: datetime = datetime.fromisoformat(state.get("at")) self.changes: list[float] = state["change"] if "change" in state else None self.change_bowl_one = self.changes[0] if self.changes else None self.change_bowl_two = self.changes[1] if self.changes else None benleb-surepy-1daca1a/surepy/enums.py000066400000000000000000000025661441277101300200640ustar00rootroot00000000000000from enum import IntEnum class SureEnum(IntEnum): """Sure base enum.""" def __str__(self) -> str: return self.name.title() # pylint: disable=no-member class EntityType(SureEnum): """Sure Entity Types.""" PET = 0 # artificial ID, not used by the Sure Petcare API HUB = 1 # Hub REPEATER = 2 # Repeater PET_FLAP = 3 # Pet Door Connect FEEDER = 4 # Microchip Pet Feeder Connect PROGRAMMER = 5 # Programmer CAT_FLAP = 6 # Cat Flap Connect FEEDER_LITE = 7 # Feeder Lite FELAQUA = 8 # Felaqua Connect class LockState(SureEnum): """Sure Petcare API State IDs.""" UNLOCKED = 0 LOCKED_IN = 1 LOCKED_OUT = 2 LOCKED_ALL = 3 CURFEW = 4 CURFEW_LOCKED = -1 CURFEW_UNLOCKED = -2 CURFEW_UNKNOWN = -3 class Location(SureEnum): """Sure Locations.""" INSIDE = 1 OUTSIDE = 2 UNKNOWN = -1 class FoodType(SureEnum): """Sure Food Types.""" WET = 1 DRY = 2 BOTH = 3 UNKNOWN = -1 class BowlPosition(SureEnum): """Sure Feeder Bowl side.""" LEFT = 0 RIGHT = 1 class Species(SureEnum): """Species of the pet.""" CAT = 1 DOG = 2 class TimelineEvent(SureEnum): """Sure timeline event types.""" BATTERY_LOW = 1 FOOD_FILLED = 21 EAT = 22 FEEDER_TARE = 24 DRINK = 29 REMINDER_FRESH_WATER = 32 ANONYMOUS_DRINK = 34 benleb-surepy-1daca1a/surepy/exceptions.py000066400000000000000000000004641441277101300211110ustar00rootroot00000000000000class SurePetcareError(Exception): """General Sure Petcare Error exception occurred.""" class SurePetcareConnectionError(SurePetcareError): """When a connection error is encountered.""" class SurePetcareAuthenticationError(SurePetcareError): """When a authentication error is encountered.""" benleb-surepy-1daca1a/surepy/py.typed000066400000000000000000000000001441277101300200370ustar00rootroot00000000000000benleb-surepy-1daca1a/surepy/surecli/000077500000000000000000000000001441277101300200205ustar00rootroot00000000000000benleb-surepy-1daca1a/surepy/surecli/__init__.py000066400000000000000000000447731441277101300221500ustar00rootroot00000000000000""" surepy.cli ==================================== The cli module of surepy |license-info| """ from __future__ import annotations import asyncio import json from datetime import datetime, time from functools import wraps from pathlib import Path from shutil import copyfile from sys import exit from typing import Any, cast import click from aiohttp import ClientSession, TCPConnector from rich import box from rich.table import Table from surepy import Surepy, __name__ as sp_name, __version__ as sp_version, console, natural_time from surepy.entities.devices import Flap, SurepyDevice, Feeder from surepy.entities.pet import Pet from surepy.enums import Location, LockState TOKEN_ENV = "SUREPY_TOKEN" def coro(f: Any) -> Any: @wraps(f) def wrapper(*args: Any, **kwargs: Any) -> Any: return asyncio.run(f(*args, **kwargs)) return wrapper token_file = Path("~/.surepy.token").expanduser() old_token_file = token_file.with_suffix(".old_token") CONTEXT_SETTINGS: dict[str, Any] = dict(help_option_names=["--help"]) version_message = ( f" [#ffffff]{sp_name}[/] ๐Ÿพ [#666666]v[#aaaaaa]{sp_version.replace('.', '[#ff1d5e].[/]')}" ) def print_header() -> None: """print header to terminal""" print() console.print(version_message, justify="left") print() def token_available(ctx: click.Context) -> str | None: if token := ctx.obj.get("token"): return str(token) console.print("\n [red bold]no token found![/]\n checked in:\n") console.print(" ยท [bold]--token[/]") console.print(f" ยท [bold]{TOKEN_ENV}[/] env var") console.print(f" ยท [white bold]{token_file}[/]") console.print("\n\n sorry ๐Ÿพ [bold]ยฏ\\_(ใƒ„)_/ยฏ[/]\n\n") return None # async def json_response( # data: dict[str, Any], ctx: click.Context, sp: Surepy | None = None # ) -> None: # if ctx.obj.get("json", False): # if sp: # await sp.sac.close_session() # # console.print(data) # # exit(0) # @click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True) @click.pass_context @click.option("--version", default=False, is_flag=True, help=f"show {sp_name} version") # @click.option("-v", "--verbose", default=False, is_flag=True, help="enable additional output") # @click.option("-d", "--debug", default=False, is_flag=True, help="enable debug output") @click.option("-j", "--json", default=False, is_flag=True, help="enable json api response output") @click.option( "-t", "--token", "user_token", default=None, type=str, help="api token", hide_input=True ) def cli(ctx: click.Context, json: bool, user_token: str, version: bool) -> None: """surepy cli ๐Ÿพ https://github.com/benleb/surepy """ ctx.ensure_object(dict) # ctx.obj["verbose"] = verbose # ctx.obj["debug"] = debug ctx.obj["json"] = json ctx.obj["token"] = user_token # if not json: # print_header() if not ctx.invoked_subcommand: if version: click.echo(version_message) exit(0) click.echo(ctx.get_help()) @cli.command() @click.pass_context @click.option( "-u", "--user", required=True, type=str, help="sure petcare api account username (email)" ) @click.option( "-p", "--password", required=True, type=str, help="sure petcare api account password", hide_input=True, ) @coro async def token(ctx: click.Context, user: str, password: str) -> None: """get a token""" surepy_token: str | None = None async with ClientSession(connector=TCPConnector(ssl=False)) as session: sp = Surepy(email=user, password=password, session=session) if surepy_token := await sp.sac.get_token(): if token_file.exists() and surepy_token != token_file.read_text(encoding="utf-8"): copyfile(token_file, old_token_file) token_file.write_text(surepy_token, encoding="utf-8") # await sp.sac.close_session() console.rule(f"[bold]{user}[/] [#ff1d5e]ยท[/] [bold]Token[/]", style="#ff1d5e") console.print(f"[bold]{token}[/]", soft_wrap=True) console.rule(style="#ff1d5e") print() @cli.command() @click.pass_context @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @coro async def pets(ctx: click.Context, token: str | None) -> None: """get pets""" token = token if token else ctx.obj.get("token", None) async with ClientSession(connector=TCPConnector(ssl=False)) as session: sp = Surepy(auth_token=token, session=session) pets: list[Pet] = await sp.get_pets() # json output if ctx.obj.get("json", False): for pet in pets: json_str = json.dumps(pet.raw_data(), indent=4) print(json_str) return # pretty print table = Table(box=box.MINIMAL) table.add_column("Name", style="bold") table.add_column("Where", justify="right") table.add_column("Feeding A", justify="right", style="bold") table.add_column("Feeding B", justify="right", style="bold") table.add_column("Lunch Time", justify="right", style="bold") table.add_column("Drinking", justify="right", style="bold") table.add_column("Drink Time", justify="right", style="bold") table.add_column("ID ๐Ÿ‘ค ", justify="right") table.add_column("Household ๐Ÿก", justify="right") for pet in pets: feeding_a = feeding_b = lunch_time = None drinking_change = drink_time = None if pet.feeding: feeding_a = f"{pet.feeding.change[0]}g" feeding_b = f"{pet.feeding.change[1]}g" lunch_time = pet.feeding.at.time() if pet.feeding.at else None if pet.drinking: drinking_change = f"{pet.drinking.change[0]}ml" drink_time = pet.drinking.at.time() if pet.drinking.at else None table.add_row( str(pet.name), str(pet.location), f"{feeding_a}", f"{feeding_b}", str(lunch_time), f"{drinking_change}", str(drink_time), str(pet.pet_id), str(pet.household_id), ) console.print(table, "", sep="\n") @cli.command() @click.pass_context @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @coro async def devices(ctx: click.Context, token: str | None) -> None: """get devices""" token = token if token else ctx.obj.get("token", None) async with ClientSession(connector=TCPConnector(ssl=False)) as session: sp = Surepy(auth_token=token, session=session) devices: list[SurepyDevice] = await sp.get_devices() # await json_response(devices, ctx) # table = Table(title="[bold][#ff1d5e]ยท[/] Devices [#ff1d5e]ยท[/]", box=box.MINIMAL) table = Table(box=box.MINIMAL) table.add_column("ID", justify="right", style="") table.add_column("Household", justify="right", style="") table.add_column("Name", style="bold") table.add_column("Type", style="") table.add_column("Serial", style="") # sorted_devices = sorted(devices, key=lambda x: int(devices[x]["household_id"])) # devices = await sp.sac.get_devices() # devices = await sp.get_entities() for device in devices: table.add_row( str(device.id), str(device.household_id), str(device.name), str(device.type.name.replace("_", " ").title()), str(device.serial) or "-", ) console.print(table, "", sep="\n") @cli.command() @click.pass_context @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @click.option("-p", "--pet", "pet_id", required=False, type=int, help="id of the pet") @click.option( "-h", "--household", "household_id", required=True, type=int, help="id of the household" ) @coro async def report( ctx: click.Context, household_id: int, pet_id: int | None = None, token: str | None = None ) -> None: """get pet/household report""" token = token if token else ctx.obj.get("token", None) async with ClientSession(connector=TCPConnector(ssl=False)) as session: sp = Surepy(auth_token=token, session=session) entities = await sp.get_entities() json_data = await sp.get_report(pet_id=pet_id, household_id=household_id) if data := json_data.get("data"): table = Table(box=box.MINIMAL) all_keys: list[str] = ["pet", "from", "to", "duration", "entry_device", "exit_device"] for key in all_keys: table.add_column(str(key)) for pet in data: datapoints_drinking: list[dict[str, Any]] = pet.get("drinking", {}).get( "datapoints", [] ) datapoints_feeding: list[dict[str, Any]] = pet.get("feeding", {}).get( "datapoints", [] ) datapoints_movement: list[dict[str, Any]] = pet.get("movement", {}).get( "datapoints", [] ) datapoints = datapoints_drinking + datapoints_feeding + datapoints_movement if datapoints: for datapoint in datapoints: from_time = datetime.fromisoformat(datapoint["from"]) to_time = ( datetime.fromisoformat(datapoint["to"]) if "active" not in datapoint else None ) if "active" in datapoint: datapoint["duration"] = ( datetime.now(tz=from_time.tzinfo) - from_time ).total_seconds() entry_device = entities.get(datapoint.get("entry_device_id", 0), None) exit_device = entities.pop(datapoint.get("exit_device_id", 0), None) table.add_row( str(entities[pet["pet_id"]].name), str(from_time.strftime("%d/%m %H:%M")), str(to_time.strftime("%d/%m %H:%M") if to_time else "-"), str(natural_time(datapoint["duration"])), str(entry_device.name if entry_device else "-"), str(exit_device.name if exit_device else "-"), ) console.print(table, "", sep="\n") @cli.command() @click.pass_context @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @coro async def notification(ctx: click.Context, token: str | None = None) -> None: """get notifications""" token = token if token else ctx.obj.get("token", None) async with ClientSession(connector=TCPConnector(ssl=False)) as session: sp = Surepy(auth_token=token, session=session) json_data = await sp.get_notification() or None if json_data and (data := json_data.get("data")): table = Table(box=box.MINIMAL) all_keys: set[str] = set() all_keys.update(*[entry.keys() for entry in data]) for key in all_keys: table.add_column(str(key)) for entry in data: table.add_row(*([str(e) for e in entry.values()])) console.print(table, "", sep="\n") @cli.command() @click.pass_context @click.option( "-d", "--device", "device_id", required=True, type=int, help="id of the sure petcare device" ) @click.option( "-m", "--mode", required=True, type=click.Choice(["lock", "in", "out", "unlock"]), help="locking mode", ) @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @coro async def locking(ctx: click.Context, device_id: int, mode: str, token: str | None = None) -> None: """lock control""" token = token if token else ctx.obj.get("token", None) sp = Surepy(auth_token=str(token)) if (flap := await sp.get_device(device_id=device_id)) and (type(flap) == Flap): flap = cast(Flap, flap) lock_state: LockState if mode == "lock": lock_state = LockState.LOCKED_ALL state = "locked" elif mode == "in": lock_state = LockState.LOCKED_IN state = "locked in" elif mode == "out": lock_state = LockState.LOCKED_OUT state = "locked out" elif mode == "unlock": lock_state = LockState.UNLOCKED state = "unlocked" else: return console.print(f"setting {flap.name} to '{state}'...") if await sp.sac._set_lock_state(device_id=device_id, mode=lock_state) and ( device := await sp.get_device(device_id=device_id) ): console.print(f"โœ… {device.name} set to '{state}' ๐Ÿพ") else: console.print(f"โŒ setting to '{state}' may have worked but something is fishy..!") # await sp.sac.close_session() @cli.command() @click.pass_context @click.option( "-d", "--device", "device_id", required=True, type=int, help="id of the sure petcare device" ) @click.option( "--lock-time", required=True, type=time.fromisoformat, help="Curfew lock time (in household's timezone)", ) @click.option( "--unlock-time", required=True, type=time.fromisoformat, help="Curfew unlock time (in household's timezone)", ) @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @coro async def curfew( ctx: click.Context, device_id: int, lock_time: time, unlock_time: time, token: str | None = None ) -> None: """curfew control""" token = token if token else ctx.obj.get("token", None) sp = Surepy(auth_token=token) if (flap := await sp.get_device(device_id=device_id)) and (type(flap) == Flap): flap = cast(Flap, flap) console.print( f"setting {flap.name} curfew lock_time={str(lock_time)} unlock_time={str(unlock_time)}" ) if await sp.sac.set_curfew( device_id=device_id, lock_time=lock_time, unlock_time=unlock_time ) and (device := await sp.get_device(device_id=device_id)): console.print( f"โœ… {device.name} curfew lock_time={str(lock_time)} unlock_time={str(unlock_time)} ๐Ÿพ" ) else: console.print( f"โŒ setting curfew lock_time={str(lock_time)} unlock_time={str(unlock_time)} may have worked but " f"something is fishy..!" ) @cli.command() @click.pass_context @click.option("--pet", "pet_id", required=True, type=int, help="id of the pet") @click.option( "--position", required=True, type=click.Choice(["in", "out"]), help="position", ) @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @coro async def position( ctx: click.Context, pet_id: int, position: str, token: str | None = None ) -> None: """set pet position""" token = token if token else ctx.obj.get("token", None) sp = Surepy(auth_token=str(token)) pet: Pet | None location: Location | None if (pet := await sp.get_pet(pet_id=pet_id)) and (type(pet) == Pet): if position == "in": location = Location.INSIDE elif position == "out": location = Location.OUTSIDE else: return if location: if await sp.sac.set_pet_location(pet.id, location): console.print(f"{pet.name} set to '{location.name}' ๐Ÿพ") else: console.print( f"setting to '{location.name}' probably worked but something else is fishy...!" ) # await sp.sac.close_session() @cli.command() @click.pass_context @click.option( "-d", "--device", "device_id", required=True, type=int, help="id of the sure petcare device" ) @click.option("-p", "--pet", "pet_id", required=False, type=int, help="id of the pet") @click.option( "-m", "--mode", required=True, type=click.Choice(["add", "remove", "list"]), help="assignment action", ) @click.option( "-t", "--token", required=False, type=str, help="sure petcare api token", hide_input=True ) @coro async def feederassign(ctx: click.Context, device_id: int, mode: str, pet_id: int | None = None, token: str | None = None) -> None: """feeder pet assignment""" token = token if token else ctx.obj.get("token", None) sp = Surepy(auth_token=str(token)) if (feeder := await sp.get_device(device_id=device_id)) and (type(feeder) == Feeder): pets: list[Pet] = await sp.get_pets() if mode == "list": table = Table(box=box.MINIMAL) table.add_column("ID", style="bold") table.add_column("Name", style="") table.add_column("Created At", style="") for tag in feeder.tags.values(): for pet in pets: if tag.id == pet.tag_id: table.add_row( str(pet.id), str(pet.name), str(datetime.fromisoformat(tag.created_at())), ) console.print(table, "", sep="\n") if mode == "add": for pet in pets: if pet.id == pet_id: for tag in feeder.tags.values(): if tag.id == pet.tag_id: console.print(f"Pet is already assigned to this feeder.") return if await sp.sac._add_tag_to_device(device_id=device_id, tag_id=pet.tag_id): console.print(f"โœ… {pet.name} added to '{feeder.name}' ๐Ÿพ") if mode == "remove": for pet in pets: if pet.id == pet_id: for tag in feeder.tags.values(): if tag.id == pet.tag_id: if await sp.sac._remove_tag_from_device(device_id=device_id, tag_id=pet.tag_id): console.print(f"โœ… {pet.name} removed from '{feeder.name}' ๐Ÿพ") return console.print("Pet is not assigned to this feeder.") else: return # await sp.sac.close_session() if __name__ == "__main__": cli(obj={})