├── .bumpversion.cfg ├── .github └── workflows │ ├── lint.yaml │ ├── publish.yaml │ ├── test.yaml │ └── type-check.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── flask_firebase_admin ├── __init__.py ├── __version__.py ├── flask_firebase_admin.py └── status_codes.py ├── poetry.lock ├── pyproject.toml ├── pyrightconfig.json └── tests ├── conftest.py └── flask_firebase_admin_test.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.4 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:pyproject.toml] 8 | search = version = "{current_version}" 9 | replace = version = "{new_version}" 10 | 11 | [bumpversion:file:flask_firebase_admin/__version__.py] 12 | search = __version__ = "{current_version}" 13 | replace = __version__ = "{new_version}" 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: "Lint Code Base" 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 3.8 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.8 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install poetry==1.1.0 19 | BLACK_VERSION=$(poetry show black | grep version | rev | cut -d " " -f 1 | rev) 20 | FLAKE8_VERSION=$(poetry show flake8 | grep version | rev | cut -d " " -f 1 | rev) 21 | pip install "black==${BLACK_VERSION}" "flake8==${FLAKE8_VERSION}" 22 | - name: Lint with flake8 23 | run: | 24 | # stop the build if there are Python syntax errors or undefined names 25 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 26 | flake8 . --count --max-complexity=10 --max-line-length=88 --statistics 27 | - name: Check with black 28 | run: | 29 | black . --check --diff 30 | - name: Run GitHub super-linter 31 | uses: docker://github/super-linter:v3 32 | env: 33 | VALIDATE_ALL_CODEBASE: true 34 | VALIDATE_YAML: true 35 | VALIDATE_JSON: true 36 | VALIDATE_MD: true 37 | VALIDATE_BASH: true 38 | VALIDATE_DOCKER: true 39 | VALIDATE_GO: true 40 | VALIDATE_ENV: true 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | wheels: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3.x 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: "3.x" 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install poetry==1.1.0 21 | - name: Build and Publish 22 | run: | 23 | poetry config pypi-token.pypi "$PYPI_TOKEN" 24 | poetry publish --build 25 | env: 26 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: "Unit Tests for python ${{ matrix.python }} on ${{ matrix.os }}" 8 | 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest, macOS-latest] 13 | python: ["3.7", "3.8", "3.9"] 14 | 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python ${{ matrix.python }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install poetry 28 | poetry config virtualenvs.create false 29 | poetry install --no-interaction 30 | 31 | - name: Run Unit Tests 32 | run: | 33 | pytest --doctest-modules --cov=flask_firebase_admin --cov-report=xml --cov-report=term tests/ flask_firebase_admin/ 34 | 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v1 37 | with: 38 | file: ./coverage.xml 39 | fail_ci_if_error: true 40 | -------------------------------------------------------------------------------- /.github/workflows/type-check.yaml: -------------------------------------------------------------------------------- 1 | name: Type Check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: "Type Check Code Base" 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: "Set up Python 3.8" 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.8 15 | - name: "Install Dependencies" 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install poetry 19 | poetry config virtualenvs.create false 20 | poetry install --no-interaction --no-dev 21 | - name: "Set up Node.js" 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: "12.x" 25 | - name: "Install Pyright" 26 | run: | 27 | npm install -g pyright 28 | - name: "Type Check with Pyright" 29 | run: | 30 | pyright flask_firebase_admin/ 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | service_accounts 3 | 4 | 5 | # Created by https://www.toptal.com/developers/gitignore/api/python 6 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 7 | 8 | ### Python ### 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | pytestdebug.log 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | doc/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | pythonenv* 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # profiling data 146 | .prof 147 | 148 | # End of https://www.toptal.com/developers/gitignore/api/python 149 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.8 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.0.1 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-case-conflict 10 | - id: check-docstring-first 11 | - id: check-json 12 | - id: check-toml 13 | - id: check-vcs-permalinks 14 | - id: check-yaml 15 | - id: debug-statements 16 | - id: name-tests-test 17 | - id: requirements-txt-fixer 18 | - repo: https://github.com/psf/black 19 | rev: 21.5b2 20 | hooks: 21 | - id: black 22 | - repo: https://github.com/PyCQA/flake8 23 | rev: 3.9.2 24 | hooks: 25 | - id: flake8 26 | additional_dependencies: ["flake8-typing-imports==1.10.1"] 27 | args: 28 | - "--max-complexity=10" 29 | - "--max-line-length=88" 30 | - repo: https://github.com/asottile/reorder_python_imports 31 | rev: v2.5.0 32 | hooks: 33 | - id: reorder-python-imports 34 | args: [--py3-plus] 35 | - repo: https://github.com/asottile/pyupgrade 36 | rev: v2.19.0 37 | hooks: 38 | - id: pyupgrade 39 | args: [--py36-plus] 40 | - repo: https://github.com/asottile/add-trailing-comma 41 | rev: v2.1.0 42 | hooks: 43 | - id: add-trailing-comma 44 | args: [--py36-plus] 45 | - repo: https://github.com/asottile/setup-cfg-fmt 46 | rev: v1.17.0 47 | hooks: 48 | - id: setup-cfg-fmt 49 | - repo: https://github.com/pre-commit/pygrep-hooks 50 | rev: v1.8.0 51 | hooks: 52 | - id: python-check-blanket-noqa 53 | - id: python-use-type-annotations 54 | # - repo: https://github.com/pre-commit/mirrors-mypy 55 | # rev: v0.812 56 | # hooks: 57 | # - id: mypy 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew Ross 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Firebase Admin 2 | 3 | Add Firebase (a Firebase Admin app) to a Flask application. 4 | 5 | [![PyPI Version](https://img.shields.io/pypi/v/flask-firebase-admin.svg)](https://pypi.org/project/flask-firebase-admin/) 6 | [![Tests](https://github.com/andrewrosss/flask-firebase-admin/actions/workflows/test.yaml/badge.svg)](https://github.com/andrewrosss/flask-firebase-admin/actions/workflows/test.yaml) 7 | [![codecov](https://codecov.io/gh/andrewrosss/flask-firebase-admin/branch/master/graph/badge.svg?token=JM7PL13H59)](https://codecov.io/gh/andrewrosss/flask-firebase-admin) 8 | [![Type Check](https://github.com/andrewrosss/flask-firebase-admin/actions/workflows/type-check.yaml/badge.svg)](https://github.com/andrewrosss/flask-firebase-admin/actions/workflows/type-check.yaml) 9 | [![Code Style](https://github.com/andrewrosss/flask-firebase-admin/actions/workflows/lint.yaml/badge.svg)](https://github.com/andrewrosss/flask-firebase-admin/actions/workflows/lint.yaml) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | pip install flask-firebase-admin 15 | ``` 16 | 17 | ## Quickstart 18 | 19 | In the simplest case, let's protect a route, specifically, we'll require a user to provide a firebase jwt to one of our routes: 20 | 21 | ```python 22 | from flask import Flask, request 23 | from flask_firebase_admin import FirebaseAdmin 24 | 25 | app = Flask(__name__) 26 | firebase = FirebaseAdmin(app) # uses GOOGLE_APPLICATION_CREDENTIALS 27 | 28 | @app.route("/unprotected") 29 | def unprotected(): 30 | return {"message": "Hello anonymous user!"} 31 | 32 | @app.route("/protected") 33 | @firebase.jwt_required # This route now requires authorization via firebase jwt 34 | def protected(): 35 | # By default JWT payload is stored under request.jwt_payload 36 | return {"message": f"Hello {request.jwt_payload['email']}!"} 37 | 38 | if __name__ == "__main__": 39 | app.run(debug=True) 40 | ``` 41 | 42 | Assuming the code above is located in a module named `app.py`, start the Flask application: 43 | 44 | ```bash 45 | GOOGLE_APPLICATION_CREDENTIALS="/path/to/service_account.json" python app.py 46 | ``` 47 | 48 | And in a separate terminal window, ping the unprotected route: 49 | 50 | ```bash 51 | $ curl http://127.0.0.1:5000/unprotected 52 | { 53 | "message": "Hello anonymous user!" 54 | } 55 | ``` 56 | 57 | Looks good. Now the protected route: 58 | 59 | ```bash 60 | $ curl http://127.0.0.1:5000/protected 61 | { 62 | "error": { 63 | "message": "No credentials provided" 64 | } 65 | } 66 | ``` 67 | 68 | OK, makes sense. Now with some credentials: 69 | 70 | ```bash 71 | $ TOKEN="your-firebase-token ..." 72 | $ curl -H "Authorization: Bearer ${TOKEN}" http://127.0.0.1:5000/protected 73 | { 74 | "message": "Hello !" 75 | } 76 | ``` 77 | 78 | Excellent. We now have a application with routes (one route) which require the user to provide their Firebase JWT to access! 79 | 80 | Internally the `jwt_required` method provided by the `FirebaseAdmin` object calls the `firebase_admin.auth.verify_id_token` function which returns a dictionary of key-value pairs parsed from the decoded JWT. This dictionary is accessible via the `request` object provided by flask, specifically, this information is attached to the `request.jwt_payload` attribute by default. 81 | 82 | ## Configuration 83 | 84 | > **Note:** The following groups of configuration parameters are mutually exclusive: 85 | > 86 | > - `FIREBASE_ADMIN_APP` (This config parameter takes precendence and if specified then configuration from the other group is ignored) 87 | > 88 | > *** 89 | > 90 | > - `FIREBASE_ADMIN_CREDENTIAL` 91 | > 92 | > `FIREBASE_ADMIN_OPTIONS` 93 | > 94 | > `FIREBASE_ADMIN_NAME` 95 | > 96 | > `FIREBASE_ADMIN_RAISE_IF_APP_EXISTS` 97 | 98 | The `FirebaseAdmin` object can be configured in the following ways: 99 | 100 | - `FIREBASE_ADMIN_CREDENTIAL` 101 | 102 | **Defaults to `None`**. This is the credential passed to the call to `firebase_admin.initialize_app`. When this parameter is None the Firebase Admin application tries to initialize using `GOOGLE_APPLICATION_CREDENTIALS`. If initializing the Firebase Admin app with the `GOOGLE_APPLICATION_CREDENTIALS` environment variable is undesirebale, credentials can be created manually, for example: 103 | 104 | ```python 105 | app = Flask(__name__) 106 | app.config["FIREBASE_ADMIN_CREDENTIAL"] = credentials.Certificate("/path/to/key.json") 107 | firebase = FirebaseAdmin(app) # no longer uses GOOGLE_APPLICATION_CREDENTIALS 108 | ``` 109 | 110 | Or perhaps something like: 111 | 112 | ```python 113 | app = Flask(__name__) 114 | cert = { 115 | "type": "service_account", 116 | "project_id": os.getenv("PROJECT_ID"), 117 | "private_key_id": os.getenv("PRIVATE_KEY_ID"), 118 | "private_key": os.getenv("PRIVATE_KEY"), 119 | "client_email": os.getenv("CLIENT_EMAIL"), 120 | "client_id": os.getenv("CLIENT_ID"), 121 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 122 | "token_uri": "https://oauth2.googleapis.com/token", 123 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 124 | "client_x509_cert_url": os.getenv("CLIENT_X509_CERT_URL"), 125 | } 126 | app.config["FIREBASE_ADMIN_CREDENTIAL"] = credentials.Certificate(cert) 127 | firebase = FirebaseAdmin(app) # no longer uses GOOGLE_APPLICATION_CREDENTIALS 128 | ``` 129 | 130 | - `FIREBASE_ADMIN_OPTIONS` 131 | 132 | **Defaults to `None`**. This config is passed directly to `admin.initialize_app()` as the second `options` argument. From the Firebase Admin SDK docs: _A dictionary of configuration options (optional). Supported options include **databaseURL**, **storageBucket**, **projectId**, **databaseAuthVariableOverride**, **serviceAccountId** and **httpTimeout**. If httpTimeout is not set, the SDK uses a default timeout of 120 seconds._ 133 | 134 | - `FIREBASE_ADMIN_NAME` 135 | 136 | **Defaults to `'[DEFAULT]'`**. This config is passed directly to `admin.initialize_app()` as the third `name` argument. 137 | 138 | - `FIREBASE_ADMIN_AUTHORIZATION_SCHEME` 139 | 140 | **Defaults to `'Bearer'`**. This is the authorization scheme expected by the `FirebaseAdmin` object. Changing this parameter changes the format of the auth header that is required by the client. For example, if we set this to `'JWT'` we would then need to include an authorization header of the form: `Authorization: JWT ` when making requests to protected routes. 141 | 142 | - `FIREBASE_ADMIN_CHECK_REVOKED` 143 | 144 | **Defaults to `True`**. This parameter is passed as the `check_revoked` argument in the call to `firebase_admin.auth.verify_id_token()`. 145 | 146 | - `FIREBASE_ADMIN_PAYLOAD_ATTR` 147 | 148 | **Defaults to `'jwt_payload'`**. This is attribute on the flask `request` object from which we can access the JWT payload data. If we were to change this to, say, `'jwt'` we would then access the JWT payload using `request.jwt` 149 | 150 | - `FIREBASE_ADMIN_RAISE_IF_APP_EXISTS` 151 | 152 | **Defaults to `True`**. Internally, `flask-firebase-admin` calls `admin.initialize_app()`, if the app with the configured name already exists the Firebase Admin SDK raises a `ValueError` exception. When this config variable is set to `False`, `flask-firebase-admin` will catch this error, get, and subsequently use the existing admin app by the given name. 153 | 154 | - `FIREBASE_ADMIN_APP` 155 | 156 | **Defaults to `None`**. This is a way to explicity provided the `FirebaseAdmin` extension with a particular firebase admin app to use. For example: 157 | 158 | ```python 159 | import firebase_admin 160 | from flask import Flask 161 | from flask_firebase_admin import FirebaseAdmin 162 | 163 | # elsewhere ... 164 | default_admin_app = firebase_admin.initialize_app() 165 | other_admin_app = firebase_admin.initialize_app(other_creds, other_options, other_name) 166 | 167 | # then ... 168 | app = Flask(__name__) 169 | app.config["FIREBASE_ADMIN_APP"] = other_admin_app 170 | 171 | # now firebase.jwt_required will use other_admin_app for authentication 172 | firebase = FirebaseAdmin(app) 173 | ``` 174 | 175 | An example using more of the available configuration: 176 | 177 | ```python 178 | from flask import Flask, request 179 | from firebase_admin import credentials 180 | from flask_firebase_admin import FirebaseAdmin 181 | 182 | app = Flask(__name__) 183 | app.config["FIREBASE_ADMIN_CREDENTIAL"] = credentials.Certificate("/path/to/key.json") 184 | app.config["FIREBASE_ADMIN_AUTHORIZATION_SCHEME"] = "JWT" 185 | app.config["FIREBASE_ADMIN_CHECK_REVOKED"] = False # don't check for revoked tokens 186 | app.config["FIREBASE_ADMIN_PAYLOAD_ATTR"] = "firebase_jwt" 187 | 188 | # initialized with credentials defined above, not GOOGLE_APPLICATION_CREDENTIALS 189 | firebase = FirebaseAdmin(app) 190 | 191 | @app.route("/unprotected") 192 | def unprotected(): 193 | return {"message": "Hello anonymous user!"} 194 | 195 | @app.route("/protected") 196 | @firebase.jwt_required # This route now requires authorization via firebase jwt 197 | def protected(): 198 | # we now access the JWT payload using request.firebase_jwt 199 | return {"message": f"Hello {request.firebase_jwt['email']}!"} 200 | 201 | if __name__ == "__main__": 202 | app.run(debug=True) 203 | ``` 204 | 205 | To call the `/protected` route we have to update our auth header that we sent originally: 206 | 207 | ```bash 208 | $ TOKEN="your-firebase-token ..." 209 | $ curl -H "Authorization: JWT ${TOKEN}" http://127.0.0.1:5000/protected 210 | { 211 | "message": "Hello !" 212 | } 213 | ``` 214 | 215 | ## Extras 216 | 217 | For convenience, the modules in the `firebase_admin` package are aliased as class-level attributes on the `FirebaseAdmin` object. For example: 218 | 219 | ```python 220 | from flask import Flask 221 | from flask_firebase_admin import FirebaseAdmin 222 | 223 | app = Flask(__name__) 224 | firebase = FirebaseAdmin(app) 225 | db = firebase.firestore.client() # <-- connect firestore client 226 | 227 | @app.route("/unprotected") 228 | def unprotected(): 229 | return {"message": "Hello anonymous user!"} 230 | 231 | @app.route("/protected") 232 | @firebase.jwt_required 233 | def protected(): 234 | # do stuff in firestore using the db object defined above. 235 | ... 236 | 237 | if __name__ == "__main__": 238 | app.run(debug=True) 239 | ``` 240 | 241 | ## Contributing 242 | 243 | 1. Have or install a recent version of `poetry` (version >= 1.1) 244 | 1. Fork the repo 245 | 1. Setup a virtual environment (however you prefer) 246 | 1. Run `poetry install` 247 | 1. Run `pre-commit install` 248 | 1. Add your changes (adding/updating tests is always nice too) 249 | 1. Commit your changes + push to your fork 250 | 1. Open a PR 251 | -------------------------------------------------------------------------------- /flask_firebase_admin/__init__.py: -------------------------------------------------------------------------------- 1 | from . import flask_firebase_admin 2 | from . import status_codes 3 | from .__version__ import __version__ 4 | from .flask_firebase_admin import FirebaseAdmin 5 | 6 | __all__ = ( 7 | "flask_firebase_admin", 8 | "status_codes", 9 | "__version__", 10 | "FirebaseAdmin", 11 | ) 12 | -------------------------------------------------------------------------------- /flask_firebase_admin/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.4" 2 | -------------------------------------------------------------------------------- /flask_firebase_admin/flask_firebase_admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import wraps 4 | from types import ModuleType 5 | from typing import Any 6 | from typing import Callable 7 | 8 | import firebase_admin 9 | from firebase_admin import auth 10 | from firebase_admin import credentials # noqa: F401 11 | from firebase_admin import db # noqa: F401 12 | from firebase_admin import exceptions # noqa: F401 13 | from firebase_admin import firestore # noqa: F401 14 | from firebase_admin import instance_id # noqa: F401 15 | from firebase_admin import messaging # noqa: F401 16 | from firebase_admin import ml # noqa: F401 17 | from firebase_admin import project_management # noqa: F401 18 | from firebase_admin import storage # noqa: F401 19 | from firebase_admin import tenant_mgt # noqa: F401 20 | from flask import current_app 21 | from flask import Flask 22 | from flask import make_response 23 | from flask import Request 24 | from flask import request 25 | from flask import Response 26 | 27 | from .status_codes import HTTP_401_UNAUTHORIZED 28 | 29 | 30 | FIREBASE_ADMIN_AUTHORIZATION_SCHEME = "Bearer" 31 | FIREBASE_ADMIN_CHECK_REVOKED = True 32 | FIREBASE_ADMIN_PAYLOAD_ATTR = "jwt_payload" 33 | FIREBASE_ADMIN_NAME = firebase_admin._DEFAULT_APP_NAME 34 | FIREBASE_ADMIN_RAISE_IF_APP_EXISTS = True 35 | 36 | 37 | class FirebaseAdmin: 38 | realm = "API" 39 | 40 | auth: ModuleType = auth 41 | credentials: ModuleType = credentials # noqa: F811 42 | db: ModuleType = db # noqa: F811 43 | exceptions: ModuleType = exceptions # noqa: F811 44 | firestore: ModuleType = firestore # noqa: F811 45 | instance_id: ModuleType = instance_id # noqa: F811 46 | messaging: ModuleType = messaging # noqa: F811 47 | ml: ModuleType = ml # noqa: F811 48 | project_management: ModuleType = project_management # noqa: F811 49 | storage: ModuleType = storage # noqa: F811 50 | tenant_mgt: ModuleType = tenant_mgt # noqa: F811 51 | 52 | def __init__(self, app: Flask = None) -> None: 53 | self.app = app 54 | if app is not None: 55 | self.init_app(app) 56 | 57 | def init_app(self, app: Flask) -> None: 58 | app.config.setdefault("FIREBASE_ADMIN_APP") 59 | app.config.setdefault( 60 | "FIREBASE_ADMIN_AUTHORIZATION_SCHEME", 61 | FIREBASE_ADMIN_AUTHORIZATION_SCHEME, 62 | ) 63 | app.config.setdefault( 64 | "FIREBASE_ADMIN_CHECK_REVOKED", 65 | FIREBASE_ADMIN_CHECK_REVOKED, 66 | ) 67 | app.config.setdefault( 68 | "FIREBASE_ADMIN_PAYLOAD_ATTR", 69 | FIREBASE_ADMIN_PAYLOAD_ATTR, 70 | ) 71 | 72 | if app.config["FIREBASE_ADMIN_APP"]: 73 | # we've been given a firebase admin app, store it, and return early 74 | self._admin = app.config["FIREBASE_ADMIN_APP"] 75 | return 76 | 77 | app.config.setdefault("FIREBASE_ADMIN_CREDENTIAL") 78 | app.config.setdefault("FIREBASE_ADMIN_OPTIONS") 79 | app.config.setdefault("FIREBASE_ADMIN_NAME", FIREBASE_ADMIN_NAME) 80 | app.config.setdefault( 81 | "FIREBASE_ADMIN_RAISE_IF_APP_EXISTS", 82 | FIREBASE_ADMIN_RAISE_IF_APP_EXISTS, 83 | ) 84 | 85 | cred = app.config["FIREBASE_ADMIN_CREDENTIAL"] 86 | options = app.config["FIREBASE_ADMIN_OPTIONS"] 87 | name = app.config["FIREBASE_ADMIN_NAME"] 88 | raise_if_app_exists = app.config["FIREBASE_ADMIN_RAISE_IF_APP_EXISTS"] 89 | 90 | self._admin = try_initialize_app(cred, options, name, raise_if_app_exists) 91 | 92 | @property 93 | def admin(self) -> firebase_admin.App: 94 | return self._admin 95 | 96 | def jwt_required(self, f: Callable) -> Callable: 97 | @wraps(f) 98 | def wrap(*args, **kwargs): 99 | header = get_authorization_header(request) 100 | if header is None: 101 | return self.make_401("No credentials provided") 102 | 103 | expected_prefix = current_app.config["FIREBASE_ADMIN_AUTHORIZATION_SCHEME"] 104 | header_prefix, token = parse_header_credentials(header) 105 | if header_prefix is None or token is None: 106 | return self.make_401( 107 | "Invalid authorization header format. Expected: " 108 | f"{expected_prefix} ", 109 | ) 110 | 111 | if header_prefix != expected_prefix: 112 | return self.make_401( 113 | f"Invalid authorization scheme. Expected: {expected_prefix}", 114 | ) 115 | 116 | try: 117 | payload_attr = current_app.config["FIREBASE_ADMIN_PAYLOAD_ATTR"] 118 | jwt_payload = self.decode_token(token) 119 | setattr(request, payload_attr, jwt_payload) 120 | except ( 121 | auth.InvalidIdTokenError, 122 | auth.ExpiredIdTokenError, 123 | auth.RevokedIdTokenError, 124 | auth.CertificateFetchError, 125 | ): 126 | return self.make_401("Token validation Failed") 127 | 128 | return f(*args, **kwargs) 129 | 130 | return wrap 131 | 132 | def decode_token(self, token: str) -> dict[str, Any]: 133 | check_revoked = current_app.config["FIREBASE_ADMIN_CHECK_REVOKED"] 134 | return auth.verify_id_token(token, self.admin, check_revoked) 135 | 136 | def make_401(self, message: str) -> Response: 137 | body = {"error": {"message": message}} 138 | status = HTTP_401_UNAUTHORIZED 139 | auth_scheme = current_app.config["FIREBASE_ADMIN_AUTHORIZATION_SCHEME"] 140 | headers = { 141 | "WWW-Authenticate": f'{auth_scheme} realm="{self.realm}", charset="UTF-8"', 142 | } 143 | return make_response((body, status, headers)) 144 | 145 | 146 | def get_authorization_header(request: Request) -> str | None: 147 | return request.headers.get("Authorization") 148 | 149 | 150 | def parse_header_credentials(header: str) -> tuple[None, None] | tuple[str, str]: 151 | values = header.split(" ") 152 | if len(values) != 2: 153 | return None, None 154 | type_, credential = values 155 | return type_, credential 156 | 157 | 158 | def try_initialize_app( 159 | cred, 160 | options, 161 | name, 162 | raise_if_app_exists: bool = True, 163 | ) -> firebase_admin.App: 164 | try: 165 | return firebase_admin.initialize_app(cred, options=options, name=name) 166 | except ValueError: 167 | if raise_if_app_exists: 168 | msg = ( 169 | f"The firebase admin app [{name!r}] already exists. If this is " 170 | "expected set app.config['FIREBASE_ADMIN_RAISE_IF_APP_EXISTS'] = False " 171 | "to have flask-firebase-admin to use the existing firebase admin app." 172 | ) 173 | raise RuntimeError(msg) 174 | return firebase_admin.get_app(name) 175 | -------------------------------------------------------------------------------- /flask_firebase_admin/status_codes.py: -------------------------------------------------------------------------------- 1 | HTTP_100_CONTINUE = 100 2 | HTTP_101_SWITCHING_PROTOCOLS = 101 3 | 4 | HTTP_200_OK = 200 5 | HTTP_201_CREATED = 201 6 | HTTP_202_ACCEPTED = 202 7 | HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 8 | HTTP_204_NO_CONTENT = 204 9 | HTTP_205_RESET_CONTENT = 205 10 | HTTP_206_PARTIAL_CONTENT = 206 11 | 12 | HTTP_300_MULTIPLE_CHOICES = 300 13 | HTTP_301_MOVED_PERMANENTLY = 301 14 | HTTP_302_FOUND = 302 15 | HTTP_303_SEE_OTHER = 303 16 | HTTP_304_NOT_MODIFIED = 304 17 | HTTP_305_USE_PROXY = 305 18 | HTTP_306_RESERVED = 306 19 | HTTP_307_TEMPORARY_REDIRECT = 307 20 | 21 | HTTP_400_BAD_REQUEST = 400 22 | HTTP_401_UNAUTHORIZED = 401 23 | HTTP_402_PAYMENT_REQUIRED = 402 24 | HTTP_403_FORBIDDEN = 403 25 | HTTP_404_NOT_FOUND = 404 26 | HTTP_405_METHOD_NOT_ALLOWED = 405 27 | HTTP_406_NOT_ACCEPTABLE = 406 28 | HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 29 | HTTP_408_REQUEST_TIMEOUT = 408 30 | HTTP_409_CONFLICT = 409 31 | HTTP_410_GONE = 410 32 | HTTP_411_LENGTH_REQUIRED = 411 33 | HTTP_412_PRECONDITION_FAILED = 412 34 | HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 35 | HTTP_414_REQUEST_URI_TOO_LONG = 414 36 | HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 37 | HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 38 | HTTP_417_EXPECTATION_FAILED = 417 39 | HTTP_428_PRECONDITION_REQUIRED = 428 40 | HTTP_429_TOO_MANY_REQUESTS = 429 41 | HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 42 | 43 | HTTP_500_INTERNAL_SERVER_ERROR = 500 44 | HTTP_501_NOT_IMPLEMENTED = 501 45 | HTTP_502_BAD_GATEWAY = 502 46 | HTTP_503_SERVICE_UNAVAILABLE = 503 47 | HTTP_504_GATEWAY_TIMEOUT = 504 48 | HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 49 | HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 50 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "aspy.refactor-imports" 11 | version = "2.2.0" 12 | description = "Utilities for refactoring imports in python-like syntax." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.6.1" 16 | 17 | [package.dependencies] 18 | cached-property = "*" 19 | 20 | [[package]] 21 | name = "astroid" 22 | version = "2.7.3" 23 | description = "An abstract syntax tree for Python with inference support." 24 | category = "dev" 25 | optional = false 26 | python-versions = "~=3.6" 27 | 28 | [package.dependencies] 29 | lazy-object-proxy = ">=1.4.0" 30 | typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 31 | typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} 32 | wrapt = ">=1.11,<1.13" 33 | 34 | [[package]] 35 | name = "atomicwrites" 36 | version = "1.4.0" 37 | description = "Atomic file writes." 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 41 | 42 | [[package]] 43 | name = "attrs" 44 | version = "21.2.0" 45 | description = "Classes Without Boilerplate" 46 | category = "dev" 47 | optional = false 48 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 49 | 50 | [package.extras] 51 | 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"] 52 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 53 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 54 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 55 | 56 | [[package]] 57 | name = "backports.entry-points-selectable" 58 | version = "1.1.0" 59 | description = "Compatibility shim providing selectable entry points for older implementations" 60 | category = "dev" 61 | optional = false 62 | python-versions = ">=2.7" 63 | 64 | [package.dependencies] 65 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 66 | 67 | [package.extras] 68 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 69 | testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 70 | 71 | [[package]] 72 | name = "black" 73 | version = "20.8b1" 74 | description = "The uncompromising code formatter." 75 | category = "dev" 76 | optional = false 77 | python-versions = ">=3.6" 78 | 79 | [package.dependencies] 80 | appdirs = "*" 81 | click = ">=7.1.2" 82 | mypy-extensions = ">=0.4.3" 83 | pathspec = ">=0.6,<1" 84 | regex = ">=2020.1.8" 85 | toml = ">=0.10.1" 86 | typed-ast = ">=1.4.0" 87 | typing-extensions = ">=3.7.4" 88 | 89 | [package.extras] 90 | colorama = ["colorama (>=0.4.3)"] 91 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 92 | 93 | [[package]] 94 | name = "bump2version" 95 | version = "1.0.1" 96 | description = "Version-bump your software with a single command!" 97 | category = "dev" 98 | optional = false 99 | python-versions = ">=3.5" 100 | 101 | [[package]] 102 | name = "cachecontrol" 103 | version = "0.12.6" 104 | description = "httplib2 caching for requests" 105 | category = "main" 106 | optional = false 107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 108 | 109 | [package.dependencies] 110 | msgpack = ">=0.5.2" 111 | requests = "*" 112 | 113 | [package.extras] 114 | filecache = ["lockfile (>=0.9)"] 115 | redis = ["redis (>=2.10.5)"] 116 | 117 | [[package]] 118 | name = "cached-property" 119 | version = "1.5.2" 120 | description = "A decorator for caching properties in classes." 121 | category = "dev" 122 | optional = false 123 | python-versions = "*" 124 | 125 | [[package]] 126 | name = "cachetools" 127 | version = "4.2.2" 128 | description = "Extensible memoizing collections and decorators" 129 | category = "main" 130 | optional = false 131 | python-versions = "~=3.5" 132 | 133 | [[package]] 134 | name = "certifi" 135 | version = "2021.5.30" 136 | description = "Python package for providing Mozilla's CA Bundle." 137 | category = "main" 138 | optional = false 139 | python-versions = "*" 140 | 141 | [[package]] 142 | name = "cffi" 143 | version = "1.14.6" 144 | description = "Foreign Function Interface for Python calling C code." 145 | category = "main" 146 | optional = false 147 | python-versions = "*" 148 | 149 | [package.dependencies] 150 | pycparser = "*" 151 | 152 | [[package]] 153 | name = "cfgv" 154 | version = "3.3.1" 155 | description = "Validate configuration and produce human readable error messages." 156 | category = "dev" 157 | optional = false 158 | python-versions = ">=3.6.1" 159 | 160 | [[package]] 161 | name = "charset-normalizer" 162 | version = "2.0.4" 163 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 164 | category = "main" 165 | optional = false 166 | python-versions = ">=3.5.0" 167 | 168 | [package.extras] 169 | unicode_backport = ["unicodedata2"] 170 | 171 | [[package]] 172 | name = "click" 173 | version = "8.0.1" 174 | description = "Composable command line interface toolkit" 175 | category = "main" 176 | optional = false 177 | python-versions = ">=3.6" 178 | 179 | [package.dependencies] 180 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 181 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 182 | 183 | [[package]] 184 | name = "colorama" 185 | version = "0.4.4" 186 | description = "Cross-platform colored terminal text." 187 | category = "main" 188 | optional = false 189 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 190 | 191 | [[package]] 192 | name = "coverage" 193 | version = "5.5" 194 | description = "Code coverage measurement for Python" 195 | category = "dev" 196 | optional = false 197 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 198 | 199 | [package.extras] 200 | toml = ["toml"] 201 | 202 | [[package]] 203 | name = "distlib" 204 | version = "0.3.2" 205 | description = "Distribution utilities" 206 | category = "dev" 207 | optional = false 208 | python-versions = "*" 209 | 210 | [[package]] 211 | name = "filelock" 212 | version = "3.0.12" 213 | description = "A platform independent file lock." 214 | category = "dev" 215 | optional = false 216 | python-versions = "*" 217 | 218 | [[package]] 219 | name = "firebase-admin" 220 | version = "4.5.3" 221 | description = "Firebase Admin Python SDK" 222 | category = "main" 223 | optional = false 224 | python-versions = ">=3.5" 225 | 226 | [package.dependencies] 227 | cachecontrol = ">=0.12.6" 228 | google-api-core = {version = ">=1.14.0,<2.0.0dev", extras = ["grpc"], markers = "platform_python_implementation != \"PyPy\""} 229 | google-api-python-client = ">=1.7.8" 230 | google-cloud-firestore = {version = ">=1.4.0", markers = "platform_python_implementation != \"PyPy\""} 231 | google-cloud-storage = ">=1.18.0" 232 | 233 | [[package]] 234 | name = "flake8" 235 | version = "3.9.2" 236 | description = "the modular source code checker: pep8 pyflakes and co" 237 | category = "dev" 238 | optional = false 239 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 240 | 241 | [package.dependencies] 242 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 243 | mccabe = ">=0.6.0,<0.7.0" 244 | pycodestyle = ">=2.7.0,<2.8.0" 245 | pyflakes = ">=2.3.0,<2.4.0" 246 | 247 | [[package]] 248 | name = "flask" 249 | version = "2.0.1" 250 | description = "A simple framework for building complex web applications." 251 | category = "main" 252 | optional = false 253 | python-versions = ">=3.6" 254 | 255 | [package.dependencies] 256 | click = ">=7.1.2" 257 | itsdangerous = ">=2.0" 258 | Jinja2 = ">=3.0" 259 | Werkzeug = ">=2.0" 260 | 261 | [package.extras] 262 | async = ["asgiref (>=3.2)"] 263 | dotenv = ["python-dotenv"] 264 | 265 | [[package]] 266 | name = "google-api-core" 267 | version = "1.31.2" 268 | description = "Google API client core library" 269 | category = "main" 270 | optional = false 271 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" 272 | 273 | [package.dependencies] 274 | google-auth = ">=1.25.0,<2.0dev" 275 | googleapis-common-protos = ">=1.6.0,<2.0dev" 276 | grpcio = {version = ">=1.29.0,<2.0dev", optional = true, markers = "extra == \"grpc\""} 277 | packaging = ">=14.3" 278 | protobuf = ">=3.12.0" 279 | pytz = "*" 280 | requests = ">=2.18.0,<3.0.0dev" 281 | six = ">=1.13.0" 282 | 283 | [package.extras] 284 | grpc = ["grpcio (>=1.29.0,<2.0dev)"] 285 | grpcgcp = ["grpcio-gcp (>=0.2.2)"] 286 | grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] 287 | 288 | [[package]] 289 | name = "google-api-python-client" 290 | version = "2.20.0" 291 | description = "Google API Client Library for Python" 292 | category = "main" 293 | optional = false 294 | python-versions = ">=3.6" 295 | 296 | [package.dependencies] 297 | google-api-core = ">=1.21.0,<3.0.0dev" 298 | google-auth = ">=1.16.0,<3.0.0dev" 299 | google-auth-httplib2 = ">=0.1.0" 300 | httplib2 = ">=0.15.0,<1dev" 301 | uritemplate = ">=3.0.0,<4dev" 302 | 303 | [[package]] 304 | name = "google-auth" 305 | version = "1.35.0" 306 | description = "Google Authentication Library" 307 | category = "main" 308 | optional = false 309 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" 310 | 311 | [package.dependencies] 312 | cachetools = ">=2.0.0,<5.0" 313 | pyasn1-modules = ">=0.2.1" 314 | rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} 315 | six = ">=1.9.0" 316 | 317 | [package.extras] 318 | aiohttp = ["requests (>=2.20.0,<3.0.0dev)", "aiohttp (>=3.6.2,<4.0.0dev)"] 319 | pyopenssl = ["pyopenssl (>=20.0.0)"] 320 | reauth = ["pyu2f (>=0.1.5)"] 321 | 322 | [[package]] 323 | name = "google-auth-httplib2" 324 | version = "0.1.0" 325 | description = "Google Authentication Library: httplib2 transport" 326 | category = "main" 327 | optional = false 328 | python-versions = "*" 329 | 330 | [package.dependencies] 331 | google-auth = "*" 332 | httplib2 = ">=0.15.0" 333 | six = "*" 334 | 335 | [[package]] 336 | name = "google-cloud-core" 337 | version = "2.0.0" 338 | description = "Google Cloud API client core library" 339 | category = "main" 340 | optional = false 341 | python-versions = ">=3.6" 342 | 343 | [package.dependencies] 344 | google-api-core = ">=1.21.0,<3.0.0dev" 345 | google-auth = ">=1.24.0,<3.0dev" 346 | 347 | [package.extras] 348 | grpc = ["grpcio (>=1.8.2,<2.0dev)"] 349 | 350 | [[package]] 351 | name = "google-cloud-firestore" 352 | version = "2.3.1" 353 | description = "Google Cloud Firestore API client library" 354 | category = "main" 355 | optional = false 356 | python-versions = ">=3.6" 357 | 358 | [package.dependencies] 359 | google-api-core = {version = ">=1.26.0,<3.0.0dev", extras = ["grpc"]} 360 | google-cloud-core = ">=1.4.1,<3.0.0dev" 361 | packaging = ">=14.3" 362 | proto-plus = ">=1.10.0" 363 | 364 | [[package]] 365 | name = "google-cloud-storage" 366 | version = "1.42.1" 367 | description = "Google Cloud Storage API client library" 368 | category = "main" 369 | optional = false 370 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" 371 | 372 | [package.dependencies] 373 | google-api-core = {version = ">=1.29.0,<3.0dev", markers = "python_version >= \"3.6\""} 374 | google-auth = {version = ">=1.25.0,<3.0dev", markers = "python_version >= \"3.6\""} 375 | google-cloud-core = {version = ">=1.6.0,<3.0dev", markers = "python_version >= \"3.6\""} 376 | google-resumable-media = {version = ">=1.3.0,<3.0dev", markers = "python_version >= \"3.6\""} 377 | requests = ">=2.18.0,<3.0.0dev" 378 | 379 | [[package]] 380 | name = "google-crc32c" 381 | version = "1.1.2" 382 | description = "A python wrapper of the C library 'Google CRC32C'" 383 | category = "main" 384 | optional = false 385 | python-versions = ">=3.6" 386 | 387 | [package.dependencies] 388 | cffi = ">=1.0.0" 389 | 390 | [package.extras] 391 | testing = ["pytest"] 392 | 393 | [[package]] 394 | name = "google-resumable-media" 395 | version = "2.0.2" 396 | description = "Utilities for Google Media Downloads and Resumable Uploads" 397 | category = "main" 398 | optional = false 399 | python-versions = ">= 3.6" 400 | 401 | [package.dependencies] 402 | google-crc32c = ">=1.0,<=1.1.2" 403 | 404 | [package.extras] 405 | aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] 406 | requests = ["requests (>=2.18.0,<3.0.0dev)"] 407 | 408 | [[package]] 409 | name = "googleapis-common-protos" 410 | version = "1.53.0" 411 | description = "Common protobufs used in Google APIs" 412 | category = "main" 413 | optional = false 414 | python-versions = ">=3.6" 415 | 416 | [package.dependencies] 417 | protobuf = ">=3.12.0" 418 | 419 | [package.extras] 420 | grpc = ["grpcio (>=1.0.0)"] 421 | 422 | [[package]] 423 | name = "grpcio" 424 | version = "1.40.0" 425 | description = "HTTP/2-based RPC framework" 426 | category = "main" 427 | optional = false 428 | python-versions = "*" 429 | 430 | [package.dependencies] 431 | six = ">=1.5.2" 432 | 433 | [package.extras] 434 | protobuf = ["grpcio-tools (>=1.40.0)"] 435 | 436 | [[package]] 437 | name = "httplib2" 438 | version = "0.19.1" 439 | description = "A comprehensive HTTP client library." 440 | category = "main" 441 | optional = false 442 | python-versions = "*" 443 | 444 | [package.dependencies] 445 | pyparsing = ">=2.4.2,<3" 446 | 447 | [[package]] 448 | name = "identify" 449 | version = "2.2.14" 450 | description = "File identification library for Python" 451 | category = "dev" 452 | optional = false 453 | python-versions = ">=3.6.1" 454 | 455 | [package.extras] 456 | license = ["editdistance-s"] 457 | 458 | [[package]] 459 | name = "idna" 460 | version = "3.2" 461 | description = "Internationalized Domain Names in Applications (IDNA)" 462 | category = "main" 463 | optional = false 464 | python-versions = ">=3.5" 465 | 466 | [[package]] 467 | name = "importlib-metadata" 468 | version = "4.8.1" 469 | description = "Read metadata from Python packages" 470 | category = "main" 471 | optional = false 472 | python-versions = ">=3.6" 473 | 474 | [package.dependencies] 475 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 476 | zipp = ">=0.5" 477 | 478 | [package.extras] 479 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 480 | perf = ["ipython"] 481 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 482 | 483 | [[package]] 484 | name = "iniconfig" 485 | version = "1.1.1" 486 | description = "iniconfig: brain-dead simple config-ini parsing" 487 | category = "dev" 488 | optional = false 489 | python-versions = "*" 490 | 491 | [[package]] 492 | name = "isort" 493 | version = "5.9.3" 494 | description = "A Python utility / library to sort Python imports." 495 | category = "dev" 496 | optional = false 497 | python-versions = ">=3.6.1,<4.0" 498 | 499 | [package.extras] 500 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 501 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 502 | colors = ["colorama (>=0.4.3,<0.5.0)"] 503 | plugins = ["setuptools"] 504 | 505 | [[package]] 506 | name = "itsdangerous" 507 | version = "2.0.1" 508 | description = "Safely pass data to untrusted environments and back." 509 | category = "main" 510 | optional = false 511 | python-versions = ">=3.6" 512 | 513 | [[package]] 514 | name = "jinja2" 515 | version = "3.0.1" 516 | description = "A very fast and expressive template engine." 517 | category = "main" 518 | optional = false 519 | python-versions = ">=3.6" 520 | 521 | [package.dependencies] 522 | MarkupSafe = ">=2.0" 523 | 524 | [package.extras] 525 | i18n = ["Babel (>=2.7)"] 526 | 527 | [[package]] 528 | name = "lazy-object-proxy" 529 | version = "1.6.0" 530 | description = "A fast and thorough lazy object proxy." 531 | category = "dev" 532 | optional = false 533 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 534 | 535 | [[package]] 536 | name = "markupsafe" 537 | version = "2.0.1" 538 | description = "Safely add untrusted strings to HTML/XML markup." 539 | category = "main" 540 | optional = false 541 | python-versions = ">=3.6" 542 | 543 | [[package]] 544 | name = "mccabe" 545 | version = "0.6.1" 546 | description = "McCabe checker, plugin for flake8" 547 | category = "dev" 548 | optional = false 549 | python-versions = "*" 550 | 551 | [[package]] 552 | name = "msgpack" 553 | version = "1.0.2" 554 | description = "MessagePack (de)serializer." 555 | category = "main" 556 | optional = false 557 | python-versions = "*" 558 | 559 | [[package]] 560 | name = "mypy-extensions" 561 | version = "0.4.3" 562 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 563 | category = "dev" 564 | optional = false 565 | python-versions = "*" 566 | 567 | [[package]] 568 | name = "nodeenv" 569 | version = "1.6.0" 570 | description = "Node.js virtual environment builder" 571 | category = "dev" 572 | optional = false 573 | python-versions = "*" 574 | 575 | [[package]] 576 | name = "packaging" 577 | version = "21.0" 578 | description = "Core utilities for Python packages" 579 | category = "main" 580 | optional = false 581 | python-versions = ">=3.6" 582 | 583 | [package.dependencies] 584 | pyparsing = ">=2.0.2" 585 | 586 | [[package]] 587 | name = "pathspec" 588 | version = "0.9.0" 589 | description = "Utility library for gitignore style pattern matching of file paths." 590 | category = "dev" 591 | optional = false 592 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 593 | 594 | [[package]] 595 | name = "platformdirs" 596 | version = "2.3.0" 597 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 598 | category = "dev" 599 | optional = false 600 | python-versions = ">=3.6" 601 | 602 | [package.extras] 603 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 604 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 605 | 606 | [[package]] 607 | name = "pluggy" 608 | version = "1.0.0" 609 | description = "plugin and hook calling mechanisms for python" 610 | category = "dev" 611 | optional = false 612 | python-versions = ">=3.6" 613 | 614 | [package.dependencies] 615 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 616 | 617 | [package.extras] 618 | dev = ["pre-commit", "tox"] 619 | testing = ["pytest", "pytest-benchmark"] 620 | 621 | [[package]] 622 | name = "pre-commit" 623 | version = "2.15.0" 624 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 625 | category = "dev" 626 | optional = false 627 | python-versions = ">=3.6.1" 628 | 629 | [package.dependencies] 630 | cfgv = ">=2.0.0" 631 | identify = ">=1.0.0" 632 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 633 | nodeenv = ">=0.11.1" 634 | pyyaml = ">=5.1" 635 | toml = "*" 636 | virtualenv = ">=20.0.8" 637 | 638 | [[package]] 639 | name = "proto-plus" 640 | version = "1.19.0" 641 | description = "Beautiful, Pythonic protocol buffers." 642 | category = "main" 643 | optional = false 644 | python-versions = ">=3.6" 645 | 646 | [package.dependencies] 647 | protobuf = ">=3.12.0" 648 | 649 | [package.extras] 650 | testing = ["google-api-core[grpc] (>=1.22.2)"] 651 | 652 | [[package]] 653 | name = "protobuf" 654 | version = "3.17.3" 655 | description = "Protocol Buffers" 656 | category = "main" 657 | optional = false 658 | python-versions = "*" 659 | 660 | [package.dependencies] 661 | six = ">=1.9" 662 | 663 | [[package]] 664 | name = "py" 665 | version = "1.10.0" 666 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 667 | category = "dev" 668 | optional = false 669 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 670 | 671 | [[package]] 672 | name = "pyasn1" 673 | version = "0.4.8" 674 | description = "ASN.1 types and codecs" 675 | category = "main" 676 | optional = false 677 | python-versions = "*" 678 | 679 | [[package]] 680 | name = "pyasn1-modules" 681 | version = "0.2.8" 682 | description = "A collection of ASN.1-based protocols modules." 683 | category = "main" 684 | optional = false 685 | python-versions = "*" 686 | 687 | [package.dependencies] 688 | pyasn1 = ">=0.4.6,<0.5.0" 689 | 690 | [[package]] 691 | name = "pycodestyle" 692 | version = "2.7.0" 693 | description = "Python style guide checker" 694 | category = "dev" 695 | optional = false 696 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 697 | 698 | [[package]] 699 | name = "pycparser" 700 | version = "2.20" 701 | description = "C parser in Python" 702 | category = "main" 703 | optional = false 704 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 705 | 706 | [[package]] 707 | name = "pyflakes" 708 | version = "2.3.1" 709 | description = "passive checker of Python programs" 710 | category = "dev" 711 | optional = false 712 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 713 | 714 | [[package]] 715 | name = "pylint" 716 | version = "2.10.2" 717 | description = "python code static checker" 718 | category = "dev" 719 | optional = false 720 | python-versions = "~=3.6" 721 | 722 | [package.dependencies] 723 | astroid = ">=2.7.2,<2.8" 724 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 725 | isort = ">=4.2.5,<6" 726 | mccabe = ">=0.6,<0.7" 727 | platformdirs = ">=2.2.0" 728 | toml = ">=0.7.1" 729 | 730 | [[package]] 731 | name = "pyparsing" 732 | version = "2.4.7" 733 | description = "Python parsing module" 734 | category = "main" 735 | optional = false 736 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 737 | 738 | [[package]] 739 | name = "pytest" 740 | version = "6.2.5" 741 | description = "pytest: simple powerful testing with Python" 742 | category = "dev" 743 | optional = false 744 | python-versions = ">=3.6" 745 | 746 | [package.dependencies] 747 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 748 | attrs = ">=19.2.0" 749 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 750 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 751 | iniconfig = "*" 752 | packaging = "*" 753 | pluggy = ">=0.12,<2.0" 754 | py = ">=1.8.2" 755 | toml = "*" 756 | 757 | [package.extras] 758 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 759 | 760 | [[package]] 761 | name = "pytest-cov" 762 | version = "2.12.1" 763 | description = "Pytest plugin for measuring coverage." 764 | category = "dev" 765 | optional = false 766 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 767 | 768 | [package.dependencies] 769 | coverage = ">=5.2.1" 770 | pytest = ">=4.6" 771 | toml = "*" 772 | 773 | [package.extras] 774 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 775 | 776 | [[package]] 777 | name = "pytest-flask" 778 | version = "1.2.0" 779 | description = "A set of py.test fixtures to test Flask applications." 780 | category = "dev" 781 | optional = false 782 | python-versions = ">=3.5" 783 | 784 | [package.dependencies] 785 | Flask = "*" 786 | pytest = ">=5.2" 787 | Werkzeug = ">=0.7" 788 | 789 | [package.extras] 790 | docs = ["sphinx", "sphinx-rtd-theme"] 791 | 792 | [[package]] 793 | name = "pytest-mock" 794 | version = "3.6.1" 795 | description = "Thin-wrapper around the mock package for easier use with pytest" 796 | category = "dev" 797 | optional = false 798 | python-versions = ">=3.6" 799 | 800 | [package.dependencies] 801 | pytest = ">=5.0" 802 | 803 | [package.extras] 804 | dev = ["pre-commit", "tox", "pytest-asyncio"] 805 | 806 | [[package]] 807 | name = "pytz" 808 | version = "2021.1" 809 | description = "World timezone definitions, modern and historical" 810 | category = "main" 811 | optional = false 812 | python-versions = "*" 813 | 814 | [[package]] 815 | name = "pyyaml" 816 | version = "5.4.1" 817 | description = "YAML parser and emitter for Python" 818 | category = "dev" 819 | optional = false 820 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 821 | 822 | [[package]] 823 | name = "regex" 824 | version = "2021.8.28" 825 | description = "Alternative regular expression module, to replace re." 826 | category = "dev" 827 | optional = false 828 | python-versions = "*" 829 | 830 | [[package]] 831 | name = "reorder-python-imports" 832 | version = "2.6.0" 833 | description = "Tool for reordering python imports" 834 | category = "dev" 835 | optional = false 836 | python-versions = ">=3.6.1" 837 | 838 | [package.dependencies] 839 | "aspy.refactor-imports" = ">=2.1.0" 840 | 841 | [[package]] 842 | name = "requests" 843 | version = "2.26.0" 844 | description = "Python HTTP for Humans." 845 | category = "main" 846 | optional = false 847 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 848 | 849 | [package.dependencies] 850 | certifi = ">=2017.4.17" 851 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 852 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 853 | urllib3 = ">=1.21.1,<1.27" 854 | 855 | [package.extras] 856 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 857 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 858 | 859 | [[package]] 860 | name = "rsa" 861 | version = "4.7.2" 862 | description = "Pure-Python RSA implementation" 863 | category = "main" 864 | optional = false 865 | python-versions = ">=3.5, <4" 866 | 867 | [package.dependencies] 868 | pyasn1 = ">=0.1.3" 869 | 870 | [[package]] 871 | name = "six" 872 | version = "1.16.0" 873 | description = "Python 2 and 3 compatibility utilities" 874 | category = "main" 875 | optional = false 876 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 877 | 878 | [[package]] 879 | name = "toml" 880 | version = "0.10.2" 881 | description = "Python Library for Tom's Obvious, Minimal Language" 882 | category = "dev" 883 | optional = false 884 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 885 | 886 | [[package]] 887 | name = "typed-ast" 888 | version = "1.4.3" 889 | description = "a fork of Python 2 and 3 ast modules with type comment support" 890 | category = "dev" 891 | optional = false 892 | python-versions = "*" 893 | 894 | [[package]] 895 | name = "typing-extensions" 896 | version = "3.10.0.2" 897 | description = "Backported and Experimental Type Hints for Python 3.5+" 898 | category = "main" 899 | optional = false 900 | python-versions = "*" 901 | 902 | [[package]] 903 | name = "uritemplate" 904 | version = "3.0.1" 905 | description = "URI templates" 906 | category = "main" 907 | optional = false 908 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 909 | 910 | [[package]] 911 | name = "urllib3" 912 | version = "1.26.6" 913 | description = "HTTP library with thread-safe connection pooling, file post, and more." 914 | category = "main" 915 | optional = false 916 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 917 | 918 | [package.extras] 919 | brotli = ["brotlipy (>=0.6.0)"] 920 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 921 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 922 | 923 | [[package]] 924 | name = "virtualenv" 925 | version = "20.7.2" 926 | description = "Virtual Python Environment builder" 927 | category = "dev" 928 | optional = false 929 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 930 | 931 | [package.dependencies] 932 | "backports.entry-points-selectable" = ">=1.0.4" 933 | distlib = ">=0.3.1,<1" 934 | filelock = ">=3.0.0,<4" 935 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 936 | platformdirs = ">=2,<3" 937 | six = ">=1.9.0,<2" 938 | 939 | [package.extras] 940 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 941 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 942 | 943 | [[package]] 944 | name = "werkzeug" 945 | version = "2.0.1" 946 | description = "The comprehensive WSGI web application library." 947 | category = "main" 948 | optional = false 949 | python-versions = ">=3.6" 950 | 951 | [package.extras] 952 | watchdog = ["watchdog"] 953 | 954 | [[package]] 955 | name = "wrapt" 956 | version = "1.12.1" 957 | description = "Module for decorators, wrappers and monkey patching." 958 | category = "dev" 959 | optional = false 960 | python-versions = "*" 961 | 962 | [[package]] 963 | name = "zipp" 964 | version = "3.5.0" 965 | description = "Backport of pathlib-compatible object wrapper for zip files" 966 | category = "main" 967 | optional = false 968 | python-versions = ">=3.6" 969 | 970 | [package.extras] 971 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 972 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 973 | 974 | [metadata] 975 | lock-version = "1.1" 976 | python-versions = "^3.7" 977 | content-hash = "3c7b80c0fe7431081ef14bb510e6d96cef573e1fac1f471b8e5dd867f8849721" 978 | 979 | [metadata.files] 980 | appdirs = [ 981 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 982 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 983 | ] 984 | "aspy.refactor-imports" = [ 985 | {file = "aspy.refactor_imports-2.2.0-py2.py3-none-any.whl", hash = "sha256:7a18039d2e8be6b02b4791ce98891deb46b459b575c52ed35ab818c4eaa0c098"}, 986 | {file = "aspy.refactor_imports-2.2.0.tar.gz", hash = "sha256:78ca24122963fd258ebfc4a8dc708d23a18040ee39dca8767675821e84e9ea0a"}, 987 | ] 988 | astroid = [ 989 | {file = "astroid-2.7.3-py3-none-any.whl", hash = "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"}, 990 | {file = "astroid-2.7.3.tar.gz", hash = "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c"}, 991 | ] 992 | atomicwrites = [ 993 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 994 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 995 | ] 996 | attrs = [ 997 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 998 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 999 | ] 1000 | "backports.entry-points-selectable" = [ 1001 | {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, 1002 | {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, 1003 | ] 1004 | black = [ 1005 | {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, 1006 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 1007 | ] 1008 | bump2version = [ 1009 | {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, 1010 | {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, 1011 | ] 1012 | cachecontrol = [ 1013 | {file = "CacheControl-0.12.6-py2.py3-none-any.whl", hash = "sha256:10d056fa27f8563a271b345207402a6dcce8efab7e5b377e270329c62471b10d"}, 1014 | {file = "CacheControl-0.12.6.tar.gz", hash = "sha256:be9aa45477a134aee56c8fac518627e1154df063e85f67d4f83ce0ccc23688e8"}, 1015 | ] 1016 | cached-property = [ 1017 | {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, 1018 | {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, 1019 | ] 1020 | cachetools = [ 1021 | {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, 1022 | {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"}, 1023 | ] 1024 | certifi = [ 1025 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 1026 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 1027 | ] 1028 | cffi = [ 1029 | {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, 1030 | {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, 1031 | {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, 1032 | {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, 1033 | {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, 1034 | {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, 1035 | {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, 1036 | {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, 1037 | {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, 1038 | {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, 1039 | {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, 1040 | {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, 1041 | {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, 1042 | {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, 1043 | {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, 1044 | {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, 1045 | {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, 1046 | {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, 1047 | {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, 1048 | {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, 1049 | {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, 1050 | {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, 1051 | {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, 1052 | {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, 1053 | {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, 1054 | {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, 1055 | {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, 1056 | {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, 1057 | {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, 1058 | {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, 1059 | {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, 1060 | {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, 1061 | {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, 1062 | {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, 1063 | {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, 1064 | {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, 1065 | {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, 1066 | {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, 1067 | {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, 1068 | {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, 1069 | {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, 1070 | {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, 1071 | {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, 1072 | {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, 1073 | {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, 1074 | ] 1075 | cfgv = [ 1076 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 1077 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 1078 | ] 1079 | charset-normalizer = [ 1080 | {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, 1081 | {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, 1082 | ] 1083 | click = [ 1084 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 1085 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 1086 | ] 1087 | colorama = [ 1088 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 1089 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 1090 | ] 1091 | coverage = [ 1092 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 1093 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 1094 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 1095 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 1096 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 1097 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 1098 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 1099 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 1100 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 1101 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 1102 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 1103 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 1104 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 1105 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 1106 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 1107 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 1108 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 1109 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 1110 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 1111 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 1112 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 1113 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 1114 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 1115 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 1116 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 1117 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 1118 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 1119 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 1120 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 1121 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 1122 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 1123 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 1124 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 1125 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 1126 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 1127 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 1128 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 1129 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 1130 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 1131 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 1132 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 1133 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 1134 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 1135 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 1136 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 1137 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 1138 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 1139 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 1140 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 1141 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 1142 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 1143 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 1144 | ] 1145 | distlib = [ 1146 | {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, 1147 | {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, 1148 | ] 1149 | filelock = [ 1150 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 1151 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 1152 | ] 1153 | firebase-admin = [ 1154 | {file = "firebase_admin-4.5.3-py3-none-any.whl", hash = "sha256:471045bf72bb68ccf6d9d19a35836609b7d525fcdac73aa619d274e4c5585e0a"}, 1155 | {file = "firebase_admin-4.5.3.tar.gz", hash = "sha256:f0c1c9a6e56b497c8bbaa55a679a402f79c34c2c24971b11ea909031b520ed32"}, 1156 | ] 1157 | flake8 = [ 1158 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 1159 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 1160 | ] 1161 | flask = [ 1162 | {file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"}, 1163 | {file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"}, 1164 | ] 1165 | google-api-core = [ 1166 | {file = "google-api-core-1.31.2.tar.gz", hash = "sha256:8500aded318fdb235130bf183c726a05a9cb7c4b09c266bd5119b86cdb8a4d10"}, 1167 | {file = "google_api_core-1.31.2-py2.py3-none-any.whl", hash = "sha256:384459a0dc98c1c8cd90b28dc5800b8705e0275a673a7144a513ae80fc77950b"}, 1168 | ] 1169 | google-api-python-client = [ 1170 | {file = "google-api-python-client-2.20.0.tar.gz", hash = "sha256:102dfe41455ac017796b2da2ec144ac7d66c55c369cb7cbc9480e29c046b055f"}, 1171 | {file = "google_api_python_client-2.20.0-py2.py3-none-any.whl", hash = "sha256:b2721b2f78dc974573345c04347bcf5a461fb437575288e3be47583a4cadc0d5"}, 1172 | ] 1173 | google-auth = [ 1174 | {file = "google-auth-1.35.0.tar.gz", hash = "sha256:b7033be9028c188ee30200b204ea00ed82ea1162e8ac1df4aa6ded19a191d88e"}, 1175 | {file = "google_auth-1.35.0-py2.py3-none-any.whl", hash = "sha256:997516b42ecb5b63e8d80f5632c1a61dddf41d2a4c2748057837e06e00014258"}, 1176 | ] 1177 | google-auth-httplib2 = [ 1178 | {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, 1179 | {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, 1180 | ] 1181 | google-cloud-core = [ 1182 | {file = "google-cloud-core-2.0.0.tar.gz", hash = "sha256:90ee99648ccf9e11a16781a7fc58d13e58f662b439c737d48c24ef18662c2702"}, 1183 | {file = "google_cloud_core-2.0.0-py2.py3-none-any.whl", hash = "sha256:31785d1e1d02f90ad3f1b020d4aed63db4865c3394ff7c128a296b6995eef31f"}, 1184 | ] 1185 | google-cloud-firestore = [ 1186 | {file = "google-cloud-firestore-2.3.1.tar.gz", hash = "sha256:8f8e513cae105ba17c4d3949091bd816cbf6075c6ac91b1f7acb4d40359f8a5a"}, 1187 | {file = "google_cloud_firestore-2.3.1-py2.py3-none-any.whl", hash = "sha256:cb25263e6c76cedd25fca8e137c9bc07a0760d5d14ec985a2becc84ef8bc67ca"}, 1188 | ] 1189 | google-cloud-storage = [ 1190 | {file = "google-cloud-storage-1.42.1.tar.gz", hash = "sha256:b37ec5b0cd69aacb09270674c4c14873898cbc77624d17fef41ec0cb08004866"}, 1191 | {file = "google_cloud_storage-1.42.1-py2.py3-none-any.whl", hash = "sha256:b701c1bbfa14d3e2bb6417908f456d7147ca201a8953347eea9c2327f1be4719"}, 1192 | ] 1193 | google-crc32c = [ 1194 | {file = "google-crc32c-1.1.2.tar.gz", hash = "sha256:dff5bd1236737f66950999d25de7a78144548ebac7788d30ada8c1b6ead60b27"}, 1195 | {file = "google_crc32c-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8ed8f6dc4f55850cba2eb22b78902ad37f397ee02692d3b8e00842e9af757321"}, 1196 | {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:110157fb19ab5db15603debfaf5fcfbac9627576787d9caf8618ff96821a7a1f"}, 1197 | {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:80abca603187093ea089cd1215c3779040dda55d3cdabc0cd5ea0e10df7bff99"}, 1198 | {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6789db0b12aab12a0f04de22ed8412dfa5f6abd5a342ea19f15355064e1cc387"}, 1199 | {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ea170341a4a9078a067b431044cd56c73553425833a7c2bb81734777a230ad4b"}, 1200 | {file = "google_crc32c-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:a64e0e8ed6076a8d867fc4622ad821c55eba8dff1b48b18f56b7c2392e22ab9d"}, 1201 | {file = "google_crc32c-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9372211acbcc207f63ffaffea1d05f3244a21311e4710721ffff3e8b7a0d24d0"}, 1202 | {file = "google_crc32c-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0ae3cf54e0d4d83c8af1afe96fc0970fbf32f1b29275f3bfd44ce25c4b622a2b"}, 1203 | {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:34a97937f164147aefa53c3277364fd3bfa7fd244cbebbd5a976fa8325fb496b"}, 1204 | {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:91ad96ee2958311d0bb75ffe5c25c87fb521ef547c09e04a8bb6143e75fb1367"}, 1205 | {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b5ea1055fe470334ced844270e7c808b04fe31e3e6394675daa77f6789ca9eff"}, 1206 | {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:e6458c41236d37cb982120b070ebcc115687c852bee24cdd18792da2640cf44d"}, 1207 | {file = "google_crc32c-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:e5af77656e8d367701f40f80a91c985ca43319f322f0a36ba9f93909d0bc4cb2"}, 1208 | {file = "google_crc32c-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ae7b9e7e2ca1b06c3a68b6ef223947a52c30ffae329b1a2be3402756073f2732"}, 1209 | {file = "google_crc32c-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:7c5138ed2e815189ba524756e027ac5833365e86115b1c2e6d9e833974a58d82"}, 1210 | {file = "google_crc32c-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6c8a712ffae56c805ca732b735af02860b246bed2c1acb38ea954a8b2dc4581"}, 1211 | {file = "google_crc32c-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:49838ede42592154f9fcd21d07c7a43a67b00a36e252f82ae72542fde09dc51f"}, 1212 | {file = "google_crc32c-1.1.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ef2ed6d0ac4de4ac602903e203eccd25ec8e37f1446fe1a3d2953a658035e0a5"}, 1213 | {file = "google_crc32c-1.1.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:51f4aa06125bf0641f65fb83268853545dbeb36b98ccfec69ef57dcb6b73b176"}, 1214 | {file = "google_crc32c-1.1.2-cp38-cp38-win32.whl", hash = "sha256:1dc6904c0d958f43102c85d70792cca210d3d051ddbeecd0eff10abcd981fdfa"}, 1215 | {file = "google_crc32c-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:298a9a922d35b123a73be80233d0f19c6ea01f008743561a8937f9dd83fb586b"}, 1216 | {file = "google_crc32c-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ab2b31395fbeeae6d15c98bd7f8b9fb76a18f18f87adc11b1f6dbe8f90d8382f"}, 1217 | {file = "google_crc32c-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d4a0d4fb938c2c3c0076445c9bd1215a3bd3df557b88d8b05ec2889ca0c92f8d"}, 1218 | {file = "google_crc32c-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d0630670d27785d7e610e72752dc8087436d00d2c7115e149c0a754babb56d3e"}, 1219 | {file = "google_crc32c-1.1.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:364eb36e8d9d34542c17b0c410035b0557edd4300a92ed736b237afaa0fd6dae"}, 1220 | {file = "google_crc32c-1.1.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:0dd9b61d0c63043b013349c9ec8a83ec2b05c96410c5bc257da5d0de743fc171"}, 1221 | {file = "google_crc32c-1.1.2-cp39-cp39-win32.whl", hash = "sha256:92ed6062792b989e84621e07a5f3d37da9cc3153b77d23a582921f14863af31d"}, 1222 | {file = "google_crc32c-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:78cf5b1bd30f3a6033b41aa4ce8c796870bc4645a15d3ef47a4b05d31b0a6dc1"}, 1223 | ] 1224 | google-resumable-media = [ 1225 | {file = "google-resumable-media-2.0.2.tar.gz", hash = "sha256:36d682161fdcbfa29681212c210fabecbf6849a505a0cbc54b7f70a10a5278a2"}, 1226 | {file = "google_resumable_media-2.0.2-py2.py3-none-any.whl", hash = "sha256:91f41314433601f94d485c1f56adfa2a7db538d53c95e0994be68cd1a17314b5"}, 1227 | ] 1228 | googleapis-common-protos = [ 1229 | {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"}, 1230 | {file = "googleapis_common_protos-1.53.0-py2.py3-none-any.whl", hash = "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0"}, 1231 | ] 1232 | grpcio = [ 1233 | {file = "grpcio-1.40.0-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:6f8f581787e739945e6cda101f312ea8a7e7082bdbb4993901eb828da6a49092"}, 1234 | {file = "grpcio-1.40.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:a4389e26a8f9338ca91effdc5436dfec67d6ecd296368dba115799ae8f8e5bdb"}, 1235 | {file = "grpcio-1.40.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fb06708e3d173e387326abcd5182d52beb60e049db5c3d317bd85509e938afdc"}, 1236 | {file = "grpcio-1.40.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:f06e07161c21391682bfcac93a181a037a8aa3d561546690e9d0501189729aac"}, 1237 | {file = "grpcio-1.40.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:5ff0dcf66315f3f00e1a8eb7244c6a49bdb0cc59bef4fb65b9db8adbd78e6acb"}, 1238 | {file = "grpcio-1.40.0-cp35-cp35m-win32.whl", hash = "sha256:ba9dd97ea1738be3e81d34e6bab8ff91a0b80668a4ec81454b283d3c828cebde"}, 1239 | {file = "grpcio-1.40.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e12d776a240fee3ebd002519c02d165d94ec636d3fe3d6185b361bfc9a2d3106"}, 1240 | {file = "grpcio-1.40.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:6b9b432f5665dfc802187384693b6338f05c7fc3707ebf003a89bd5132074e27"}, 1241 | {file = "grpcio-1.40.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:886d056f5101ac513f4aefe4d21a816d98ee3f9a8e77fc3bcb4ae1a3a24efe26"}, 1242 | {file = "grpcio-1.40.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b1b34e5a6f1285d1576099c663dae28c07b474015ed21e35a243aff66a0c2aed"}, 1243 | {file = "grpcio-1.40.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:17ed13d43450ef9d1f9b78cc932bcf42844ca302235b93026dfd07fb5208d146"}, 1244 | {file = "grpcio-1.40.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:e19de138199502d575fcec5cf68ae48815a6efe7e5c0d0b8c97eba8c77ae9f0e"}, 1245 | {file = "grpcio-1.40.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:a812164ceb48cb62c3217bd6245274e693c624cc2ac0c1b11b4cea96dab054dd"}, 1246 | {file = "grpcio-1.40.0-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:eedc8c3514c10b6f11c6f406877e424ca29610883b97bb97e33b1dd2a9077f6c"}, 1247 | {file = "grpcio-1.40.0-cp36-cp36m-win32.whl", hash = "sha256:1708a0ba90c798b4313f541ffbcc25ed47e790adaafb02111204362723dabef0"}, 1248 | {file = "grpcio-1.40.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d760a66c9773780837915be85a39d2cd4ab42ef32657c5f1d28475e23ab709fc"}, 1249 | {file = "grpcio-1.40.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:8a35b5f87247c893b01abf2f4f7493a18c2c5bf8eb3923b8dd1654d8377aa1a7"}, 1250 | {file = "grpcio-1.40.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:45704b9b5b85f9bcb027f90f2563d11d995c1b870a9ee4b3766f6c7ff6fc3505"}, 1251 | {file = "grpcio-1.40.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4967949071c9e435f9565ec2f49700cebeda54836a04710fe21f7be028c0125a"}, 1252 | {file = "grpcio-1.40.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:1f9ccc9f5c0d5084d1cd917a0b5ff0142a8d269d0755592d751f8ce9e7d3d7f1"}, 1253 | {file = "grpcio-1.40.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:5729ca9540049f52c2e608ca110048cfabab3aeaa0d9f425361d9f8ba8506cac"}, 1254 | {file = "grpcio-1.40.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:edddc849bed3c5dfe215a9f9532a9bd9f670b57d7b8af603be80148b4c69e9a8"}, 1255 | {file = "grpcio-1.40.0-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:49155dfdf725c0862c428039123066b25ce61bd38ce50a21ce325f1735aac1bd"}, 1256 | {file = "grpcio-1.40.0-cp37-cp37m-win32.whl", hash = "sha256:913916823efa2e487b2ee9735b7759801d97fd1974bacdb1900e3bbd17f7d508"}, 1257 | {file = "grpcio-1.40.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24277aab99c346ca36a1aa8589a0624e19a8e6f2b74c83f538f7bb1cc5ee8dbc"}, 1258 | {file = "grpcio-1.40.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:a66a30513d2e080790244a7ac3d7a3f45001f936c5c2c9613e41e2a5d7a11794"}, 1259 | {file = "grpcio-1.40.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:e2367f2b18dd4ba64cdcd9f626a920f9ec2e8228630839dc8f4a424d461137ea"}, 1260 | {file = "grpcio-1.40.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:27dee6dcd1c04c4e9ceea49f6143003569292209d2c24ca100166660805e2440"}, 1261 | {file = "grpcio-1.40.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d271e52038dec0db7c39ad9303442d6087c55e09b900e2931b86e837cf0cbc2e"}, 1262 | {file = "grpcio-1.40.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:41e250ec7cd7523bf49c815b5509d5821728c26fac33681d4b0d1f5f34f59f06"}, 1263 | {file = "grpcio-1.40.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:33dc4259fecb96e6eac20f760656b911bcb1616aa3e58b3a1d2f125714a2f5d3"}, 1264 | {file = "grpcio-1.40.0-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:72b7b8075ee822dad4b39c150d73674c1398503d389e38981e9e35a894c476de"}, 1265 | {file = "grpcio-1.40.0-cp38-cp38-win32.whl", hash = "sha256:a93490e6eff5fce3748fb2757cb4273dc21eb1b56732b8c9640fd82c1997b215"}, 1266 | {file = "grpcio-1.40.0-cp38-cp38-win_amd64.whl", hash = "sha256:d3b4b41eb0148fca3e6e6fc61d1332a7e8e7c4074fb0d1543f0b255d7f5f1588"}, 1267 | {file = "grpcio-1.40.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:fbe3b66bfa2c2f94535f6063f6db62b5b150d55a120f2f9e1175d3087429c4d9"}, 1268 | {file = "grpcio-1.40.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:ecfd80e8ea03c46b3ea7ed37d2040fcbfe739004b9e4329b8b602d06ac6fb113"}, 1269 | {file = "grpcio-1.40.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d487b4daf84a14741ca1dc1c061ffb11df49d13702cd169b5837fafb5e84d9c0"}, 1270 | {file = "grpcio-1.40.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c26de909cfd54bacdb7e68532a1591a128486af47ee3a5f828df9aa2165ae457"}, 1271 | {file = "grpcio-1.40.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:1d9eabe2eb2f78208f9ae67a591f73b024488449d4e0a5b27c7fca2d6901a2d4"}, 1272 | {file = "grpcio-1.40.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:4c2baa438f51152c9b7d0835ff711add0b4bc5056c0f5df581a6112153010696"}, 1273 | {file = "grpcio-1.40.0-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:bf114be0023b145f7101f392a344692c1efd6de38a610c54a65ed3cba035e669"}, 1274 | {file = "grpcio-1.40.0-cp39-cp39-win32.whl", hash = "sha256:5f6d6b638698fa6decf7f040819aade677b583eaa21b43366232cb254a2bbac8"}, 1275 | {file = "grpcio-1.40.0-cp39-cp39-win_amd64.whl", hash = "sha256:005fe14e67291498989da67d454d805be31d57a988af28ed3a2a0a7cabb05c53"}, 1276 | {file = "grpcio-1.40.0.tar.gz", hash = "sha256:3d172158fe886a2604db1b6e17c2de2ab465fe0fe36aba2ec810ca8441cefe3a"}, 1277 | ] 1278 | httplib2 = [ 1279 | {file = "httplib2-0.19.1-py3-none-any.whl", hash = "sha256:2ad195faf9faf079723f6714926e9a9061f694d07724b846658ce08d40f522b4"}, 1280 | {file = "httplib2-0.19.1.tar.gz", hash = "sha256:0b12617eeca7433d4c396a100eaecfa4b08ee99aa881e6df6e257a7aad5d533d"}, 1281 | ] 1282 | identify = [ 1283 | {file = "identify-2.2.14-py2.py3-none-any.whl", hash = "sha256:113a76a6ba614d2a3dd408b3504446bcfac0370da5995aa6a17fd7c6dffde02d"}, 1284 | {file = "identify-2.2.14.tar.gz", hash = "sha256:32f465f3c48083f345ad29a9df8419a4ce0674bf4a8c3245191d65c83634bdbf"}, 1285 | ] 1286 | idna = [ 1287 | {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, 1288 | {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, 1289 | ] 1290 | importlib-metadata = [ 1291 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 1292 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 1293 | ] 1294 | iniconfig = [ 1295 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 1296 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 1297 | ] 1298 | isort = [ 1299 | {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, 1300 | {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, 1301 | ] 1302 | itsdangerous = [ 1303 | {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, 1304 | {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, 1305 | ] 1306 | jinja2 = [ 1307 | {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, 1308 | {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, 1309 | ] 1310 | lazy-object-proxy = [ 1311 | {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, 1312 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, 1313 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, 1314 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, 1315 | {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, 1316 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, 1317 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, 1318 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, 1319 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, 1320 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, 1321 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, 1322 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, 1323 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, 1324 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, 1325 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, 1326 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, 1327 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, 1328 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, 1329 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, 1330 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, 1331 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, 1332 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, 1333 | ] 1334 | markupsafe = [ 1335 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 1336 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 1337 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 1338 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 1339 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 1340 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 1341 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 1342 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 1343 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 1344 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 1345 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 1346 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 1347 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 1348 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 1349 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 1350 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 1351 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 1352 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 1353 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 1354 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 1355 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 1356 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 1357 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 1358 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 1359 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 1360 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 1361 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 1362 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 1363 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 1364 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 1365 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 1366 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 1367 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 1368 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 1369 | ] 1370 | mccabe = [ 1371 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 1372 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 1373 | ] 1374 | msgpack = [ 1375 | {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9"}, 1376 | {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192"}, 1377 | {file = "msgpack-1.0.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841"}, 1378 | {file = "msgpack-1.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"}, 1379 | {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326"}, 1380 | {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439"}, 1381 | {file = "msgpack-1.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f"}, 1382 | {file = "msgpack-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2"}, 1383 | {file = "msgpack-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc"}, 1384 | {file = "msgpack-1.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54"}, 1385 | {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87"}, 1386 | {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c"}, 1387 | {file = "msgpack-1.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1"}, 1388 | {file = "msgpack-1.0.2-cp37-cp37m-win32.whl", hash = "sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a"}, 1389 | {file = "msgpack-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b"}, 1390 | {file = "msgpack-1.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06"}, 1391 | {file = "msgpack-1.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c"}, 1392 | {file = "msgpack-1.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e"}, 1393 | {file = "msgpack-1.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83"}, 1394 | {file = "msgpack-1.0.2-cp38-cp38-win32.whl", hash = "sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9"}, 1395 | {file = "msgpack-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009"}, 1396 | {file = "msgpack-1.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694"}, 1397 | {file = "msgpack-1.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759"}, 1398 | {file = "msgpack-1.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887"}, 1399 | {file = "msgpack-1.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e"}, 1400 | {file = "msgpack-1.0.2-cp39-cp39-win32.whl", hash = "sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33"}, 1401 | {file = "msgpack-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f"}, 1402 | {file = "msgpack-1.0.2.tar.gz", hash = "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984"}, 1403 | ] 1404 | mypy-extensions = [ 1405 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1406 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1407 | ] 1408 | nodeenv = [ 1409 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 1410 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 1411 | ] 1412 | packaging = [ 1413 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 1414 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 1415 | ] 1416 | pathspec = [ 1417 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 1418 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 1419 | ] 1420 | platformdirs = [ 1421 | {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, 1422 | {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, 1423 | ] 1424 | pluggy = [ 1425 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1426 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1427 | ] 1428 | pre-commit = [ 1429 | {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, 1430 | {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, 1431 | ] 1432 | proto-plus = [ 1433 | {file = "proto-plus-1.19.0.tar.gz", hash = "sha256:ce6695ce804383ad6f392c4bb1874c323896290a1f656560de36416ba832d91e"}, 1434 | {file = "proto_plus-1.19.0-py3-none-any.whl", hash = "sha256:df7c71c08dc06403bdb0fba58cf9bf5f217198f6488c26b768f81e03a738c059"}, 1435 | ] 1436 | protobuf = [ 1437 | {file = "protobuf-3.17.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8"}, 1438 | {file = "protobuf-3.17.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637"}, 1439 | {file = "protobuf-3.17.3-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0"}, 1440 | {file = "protobuf-3.17.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62"}, 1441 | {file = "protobuf-3.17.3-cp35-cp35m-win32.whl", hash = "sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6"}, 1442 | {file = "protobuf-3.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037"}, 1443 | {file = "protobuf-3.17.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71"}, 1444 | {file = "protobuf-3.17.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4"}, 1445 | {file = "protobuf-3.17.3-cp36-cp36m-win32.whl", hash = "sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9"}, 1446 | {file = "protobuf-3.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d"}, 1447 | {file = "protobuf-3.17.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b"}, 1448 | {file = "protobuf-3.17.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764"}, 1449 | {file = "protobuf-3.17.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c"}, 1450 | {file = "protobuf-3.17.3-cp37-cp37m-win32.whl", hash = "sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5"}, 1451 | {file = "protobuf-3.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683"}, 1452 | {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, 1453 | {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, 1454 | {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, 1455 | {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, 1456 | {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, 1457 | {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, 1458 | {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, 1459 | {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, 1460 | ] 1461 | py = [ 1462 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 1463 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 1464 | ] 1465 | pyasn1 = [ 1466 | {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, 1467 | {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, 1468 | {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, 1469 | {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, 1470 | {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, 1471 | {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, 1472 | {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, 1473 | {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, 1474 | {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, 1475 | {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, 1476 | {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, 1477 | {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, 1478 | {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, 1479 | ] 1480 | pyasn1-modules = [ 1481 | {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, 1482 | {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"}, 1483 | {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"}, 1484 | {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"}, 1485 | {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"}, 1486 | {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, 1487 | {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"}, 1488 | {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"}, 1489 | {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"}, 1490 | {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"}, 1491 | {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"}, 1492 | {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, 1493 | {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, 1494 | ] 1495 | pycodestyle = [ 1496 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 1497 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 1498 | ] 1499 | pycparser = [ 1500 | {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, 1501 | {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, 1502 | ] 1503 | pyflakes = [ 1504 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 1505 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 1506 | ] 1507 | pylint = [ 1508 | {file = "pylint-2.10.2-py3-none-any.whl", hash = "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"}, 1509 | {file = "pylint-2.10.2.tar.gz", hash = "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1"}, 1510 | ] 1511 | pyparsing = [ 1512 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 1513 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 1514 | ] 1515 | pytest = [ 1516 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 1517 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 1518 | ] 1519 | pytest-cov = [ 1520 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 1521 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 1522 | ] 1523 | pytest-flask = [ 1524 | {file = "pytest-flask-1.2.0.tar.gz", hash = "sha256:46fde652f77777bf02dc91205aec4ce20cdf2acbbbd66a918ab91f5c14693d3d"}, 1525 | {file = "pytest_flask-1.2.0-py3-none-any.whl", hash = "sha256:fe25b39ad0db09c3d1fe728edecf97ced85e774c775db259a6d25f0270a4e7c9"}, 1526 | ] 1527 | pytest-mock = [ 1528 | {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, 1529 | {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, 1530 | ] 1531 | pytz = [ 1532 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 1533 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 1534 | ] 1535 | pyyaml = [ 1536 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 1537 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 1538 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 1539 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 1540 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 1541 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 1542 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 1543 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 1544 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 1545 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 1546 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 1547 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 1548 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 1549 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 1550 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 1551 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 1552 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 1553 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 1554 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 1555 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 1556 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 1557 | ] 1558 | regex = [ 1559 | {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, 1560 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, 1561 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, 1562 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, 1563 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, 1564 | {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"}, 1565 | {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, 1566 | {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, 1567 | {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, 1568 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, 1569 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, 1570 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, 1571 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, 1572 | {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"}, 1573 | {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, 1574 | {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, 1575 | {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, 1576 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, 1577 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, 1578 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, 1579 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, 1580 | {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"}, 1581 | {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, 1582 | {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, 1583 | {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, 1584 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, 1585 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, 1586 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, 1587 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, 1588 | {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"}, 1589 | {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, 1590 | {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, 1591 | {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, 1592 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, 1593 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, 1594 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, 1595 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, 1596 | {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"}, 1597 | {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, 1598 | {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, 1599 | {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, 1600 | ] 1601 | reorder-python-imports = [ 1602 | {file = "reorder_python_imports-2.6.0-py2.py3-none-any.whl", hash = "sha256:54a3afd594a3959b10f7eb8b54ef453eb2b5176eb7b01c111cb1893ff9a2c685"}, 1603 | {file = "reorder_python_imports-2.6.0.tar.gz", hash = "sha256:f4dc03142bdb57625e64299aea80e9055ce0f8b719f8f19c217a487c9fa9379e"}, 1604 | ] 1605 | requests = [ 1606 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 1607 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 1608 | ] 1609 | rsa = [ 1610 | {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, 1611 | {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"}, 1612 | ] 1613 | six = [ 1614 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1615 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1616 | ] 1617 | toml = [ 1618 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1619 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1620 | ] 1621 | typed-ast = [ 1622 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 1623 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 1624 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 1625 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 1626 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 1627 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 1628 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 1629 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 1630 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 1631 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 1632 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 1633 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 1634 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 1635 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 1636 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 1637 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 1638 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 1639 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 1640 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 1641 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 1642 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 1643 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 1644 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 1645 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 1646 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 1647 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 1648 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 1649 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 1650 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 1651 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 1652 | ] 1653 | typing-extensions = [ 1654 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 1655 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 1656 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 1657 | ] 1658 | uritemplate = [ 1659 | {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, 1660 | {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, 1661 | ] 1662 | urllib3 = [ 1663 | {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, 1664 | {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, 1665 | ] 1666 | virtualenv = [ 1667 | {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, 1668 | {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, 1669 | ] 1670 | werkzeug = [ 1671 | {file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"}, 1672 | {file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"}, 1673 | ] 1674 | wrapt = [ 1675 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 1676 | ] 1677 | zipp = [ 1678 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 1679 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 1680 | ] 1681 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "flask-firebase-admin" 3 | version = "0.2.4" 4 | description = "Firebase for Flask" 5 | authors = ["Andrew Ross "] 6 | license = "MIT" 7 | 8 | readme = "README.md" 9 | 10 | homepage = "https://github.com/andrewrosss/flask-firebase-admin" 11 | # repository = "https://github.com/andrewrosss/flask-firebase-admin" 12 | # documentation = "https://github.com/andrewrosss/flask-firebase-admin" 13 | 14 | keywords = ["flask", "firebase", "admin", "auth"] 15 | 16 | classifiers = [ 17 | "Programming Language :: Python :: 3.0", 18 | "License :: OSI Approved :: MIT License", 19 | "Intended Audience :: Developers", 20 | "Environment :: Web Environment", 21 | "Operating System :: OS Independent", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 24 | "Framework :: Flask", 25 | "Development Status :: 4 - Beta", 26 | ] 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.7" 30 | flask = ">=1.0,<3.0" 31 | firebase-admin = "^4.0.0" 32 | 33 | [tool.poetry.dev-dependencies] 34 | black = "^20.8b1" 35 | flake8 = "^3.8.3" 36 | pylint = "^2.5.3" 37 | reorder-python-imports = "^2.3.5" 38 | bump2version = "^1.0.0" 39 | pytest-mock = "^3.5.1" 40 | pytest-flask = "^1.2.0" 41 | pytest-cov = "^2.11.1" 42 | pre-commit = "^2.15.0" 43 | 44 | [build-system] 45 | requires = ["poetry>=0.12"] 46 | build-backend = "poetry.masonry.api" 47 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "**/venv", 4 | "**/__pycache__" 5 | ], 6 | "stubPath": "typings" 7 | } 8 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import flask 4 | import pytest 5 | from firebase_admin import auth 6 | from pytest_mock import MockerFixture 7 | 8 | import flask_firebase_admin 9 | 10 | 11 | @pytest.fixture 12 | def app(request): 13 | app = flask.Flask(request.module.__name__) 14 | app.testing = True 15 | return app 16 | 17 | 18 | AppFixture = flask.Flask 19 | 20 | 21 | @pytest.fixture 22 | def initialize_app(mocker: MockerFixture): 23 | return mocker.patch("firebase_admin.initialize_app") 24 | 25 | 26 | InitializeAppFixture = MagicMock 27 | 28 | 29 | @pytest.fixture 30 | def try_initialize_app(mocker: MockerFixture): 31 | return mocker.patch("flask_firebase_admin.flask_firebase_admin.try_initialize_app") 32 | 33 | 34 | TryInitializeAppFixture = MagicMock 35 | 36 | 37 | @pytest.fixture 38 | def verify_id_token(mocker: MockerFixture): 39 | return mocker.patch("firebase_admin.auth.verify_id_token") 40 | 41 | 42 | VerifyIdTokenFixture = MagicMock 43 | 44 | 45 | @pytest.fixture 46 | def decode_token(mocker: MockerFixture): 47 | dt = mocker.patch( 48 | "flask_firebase_admin.flask_firebase_admin.FirebaseAdmin.decode_token", 49 | ) 50 | dt.return_value = {"email": "test@email.com"} 51 | return 52 | 53 | 54 | DecodeTokenFixture = MagicMock 55 | 56 | 57 | @pytest.fixture 58 | def decode_token_raises(mocker: MockerFixture): 59 | dt = mocker.patch( 60 | "flask_firebase_admin.flask_firebase_admin.FirebaseAdmin.decode_token", 61 | ) 62 | dt.side_effect = auth.InvalidIdTokenError("test message") 63 | return 64 | 65 | 66 | DecodeTokenRaisesFixture = MagicMock 67 | 68 | 69 | @pytest.fixture 70 | def get_app(mocker: MockerFixture): 71 | return mocker.patch("firebase_admin.get_app") 72 | 73 | 74 | GetAppFixture = MagicMock 75 | 76 | 77 | @pytest.fixture 78 | def sample_app( 79 | app: AppFixture, 80 | try_initialize_app: TryInitializeAppFixture, 81 | decode_token: DecodeTokenFixture, 82 | ): 83 | admin = flask_firebase_admin.FirebaseAdmin(app) 84 | 85 | @app.route("/protected", methods=["GET", "POST"]) 86 | @admin.jwt_required 87 | def protected(): 88 | # we now access the JWT payload using request.firebase_jwt 89 | email = flask.request.jwt_payload["email"] # type: ignore 90 | return {"message": f"Hello {email}!"} 91 | 92 | return app 93 | 94 | 95 | SampleAppFixture = flask.Flask 96 | 97 | 98 | @pytest.fixture 99 | def sample_app_no_valid_token( 100 | app: AppFixture, 101 | try_initialize_app: TryInitializeAppFixture, 102 | decode_token_raises: DecodeTokenRaisesFixture, 103 | ): 104 | admin = flask_firebase_admin.FirebaseAdmin(app) 105 | 106 | @app.route("/protected", methods=["GET", "POST"]) 107 | @admin.jwt_required 108 | def protected(): 109 | # we now access the JWT payload using request.firebase_jwt 110 | email = flask.request.jwt_payload["email"] # type: ignore 111 | return {"message": f"Hello {email}!"} 112 | 113 | return app 114 | 115 | 116 | SampleAppNoValidTokenFixture = flask.Flask 117 | -------------------------------------------------------------------------------- /tests/flask_firebase_admin_test.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import firebase_admin 4 | import pytest 5 | from pytest_mock.plugin import MockerFixture 6 | 7 | import flask_firebase_admin 8 | import flask_firebase_admin.flask_firebase_admin as ffa 9 | from tests.conftest import AppFixture 10 | from tests.conftest import GetAppFixture 11 | from tests.conftest import InitializeAppFixture 12 | from tests.conftest import TryInitializeAppFixture 13 | from tests.conftest import VerifyIdTokenFixture 14 | 15 | 16 | class TestDefaultConfig: 17 | def test_init_with___init__( 18 | self, 19 | app: AppFixture, 20 | try_initialize_app: TryInitializeAppFixture, 21 | ): 22 | # create and initialize the extension 23 | admin = flask_firebase_admin.FirebaseAdmin(app) 24 | 25 | assert admin.app is not None 26 | assert app.config["FIREBASE_ADMIN_APP"] is None 27 | assert app.config["FIREBASE_ADMIN_AUTHORIZATION_SCHEME"] == "Bearer" 28 | assert app.config["FIREBASE_ADMIN_CHECK_REVOKED"] is True 29 | assert app.config["FIREBASE_ADMIN_PAYLOAD_ATTR"] == "jwt_payload" 30 | assert app.config["FIREBASE_ADMIN_CREDENTIAL"] is None 31 | assert app.config["FIREBASE_ADMIN_OPTIONS"] is None 32 | assert app.config["FIREBASE_ADMIN_NAME"] == firebase_admin._DEFAULT_APP_NAME 33 | assert app.config["FIREBASE_ADMIN_RAISE_IF_APP_EXISTS"] is True 34 | try_initialize_app.assert_called_once_with( 35 | None, 36 | None, 37 | firebase_admin._DEFAULT_APP_NAME, 38 | True, 39 | ) 40 | assert admin.admin is not None 41 | 42 | def test_init_with_init_app( 43 | self, 44 | app: AppFixture, 45 | try_initialize_app: TryInitializeAppFixture, 46 | ): 47 | # create the extension 48 | admin = flask_firebase_admin.FirebaseAdmin() 49 | 50 | assert admin.app is None 51 | assert "FIREBASE_ADMIN_APP" not in app.config 52 | assert "FIREBASE_ADMIN_AUTHORIZATION_SCHEME" not in app.config 53 | assert "FIREBASE_ADMIN_CHECK_REVOKED" not in app.config 54 | assert "FIREBASE_ADMIN_PAYLOAD_ATTR" not in app.config 55 | assert "FIREBASE_ADMIN_CREDENTIAL" not in app.config 56 | assert "FIREBASE_ADMIN_OPTIONS" not in app.config 57 | assert "FIREBASE_ADMIN_NAME" not in app.config 58 | assert "FIREBASE_ADMIN_RAISE_IF_APP_EXISTS" not in app.config 59 | assert admin.app is None 60 | try_initialize_app.assert_not_called() 61 | 62 | with pytest.raises(AttributeError): 63 | admin.admin 64 | 65 | # initialize the extension 66 | admin.init_app(app) 67 | 68 | assert app.config["FIREBASE_ADMIN_APP"] is None 69 | assert app.config["FIREBASE_ADMIN_AUTHORIZATION_SCHEME"] == "Bearer" 70 | assert app.config["FIREBASE_ADMIN_CHECK_REVOKED"] is True 71 | assert app.config["FIREBASE_ADMIN_PAYLOAD_ATTR"] == "jwt_payload" 72 | assert app.config["FIREBASE_ADMIN_CREDENTIAL"] is None 73 | assert app.config["FIREBASE_ADMIN_OPTIONS"] is None 74 | assert app.config["FIREBASE_ADMIN_NAME"] == firebase_admin._DEFAULT_APP_NAME 75 | assert app.config["FIREBASE_ADMIN_RAISE_IF_APP_EXISTS"] is True 76 | try_initialize_app.assert_called_once_with( 77 | None, 78 | None, 79 | firebase_admin._DEFAULT_APP_NAME, 80 | True, 81 | ) 82 | assert admin.admin is not None 83 | 84 | def test_expected_number_of_config_parameter(self, app: AppFixture, initialize_app): 85 | # this check is a reminder that if more configuration 86 | # is added then TestDefaultConfig needs to be updated 87 | flask_firebase_admin.FirebaseAdmin(app) 88 | assert len([k for k in app.config if k.startswith("FIREBASE_ADMIN")]) == 8 89 | 90 | 91 | class TestCustomConfig: 92 | def test_firebase_admin_app( 93 | self, 94 | app: AppFixture, 95 | try_initialize_app: TryInitializeAppFixture, 96 | ): 97 | admin_app = "this-is-my-admin-app" 98 | app.config["FIREBASE_ADMIN_APP"] = admin_app 99 | admin = flask_firebase_admin.FirebaseAdmin(app) 100 | 101 | try_initialize_app.assert_not_called() 102 | # whatever was passed as config should simply be forwarded to FirebaseAdmin 103 | assert admin.admin == admin_app 104 | # becauase we explicitly specificied FIREBASE_ADMIN_APP the following 105 | # config should be ignored/left unconfigured 106 | assert "FIREBASE_ADMIN_CREDENTIAL" not in app.config 107 | assert "FIREBASE_ADMIN_OPTIONS" not in app.config 108 | assert "FIREBASE_ADMIN_NAME" not in app.config 109 | assert "FIREBASE_ADMIN_RAISE_IF_APP_EXISTS" not in app.config 110 | 111 | def test_firebase_firebase_admin_credentials( 112 | self, 113 | app: AppFixture, 114 | try_initialize_app: TryInitializeAppFixture, 115 | ): 116 | cred = "my-creds" 117 | options = "my-options" 118 | name = "my-name" 119 | raise_if_app_exists = False 120 | app.config["FIREBASE_ADMIN_CREDENTIAL"] = cred 121 | app.config["FIREBASE_ADMIN_OPTIONS"] = options 122 | app.config["FIREBASE_ADMIN_NAME"] = name 123 | app.config["FIREBASE_ADMIN_RAISE_IF_APP_EXISTS"] = raise_if_app_exists 124 | 125 | admin = flask_firebase_admin.FirebaseAdmin(app) 126 | 127 | assert admin.admin is try_initialize_app.return_value 128 | try_initialize_app.assert_called_once_with( 129 | cred, 130 | options, 131 | name, 132 | raise_if_app_exists, 133 | ) 134 | 135 | 136 | class TestJWTRequired: 137 | protected_route = "/protected" 138 | 139 | def test_no_auth_header_provided(self, sample_app, client): 140 | expected_message = "No credentials provided" 141 | expected_status_code = 401 142 | 143 | get_response = client.get(self.protected_route) 144 | assert get_response.status_code == expected_status_code 145 | assert get_response.json["error"]["message"] == expected_message 146 | 147 | post_response = client.post(self.protected_route) 148 | assert post_response.status_code == expected_status_code 149 | assert post_response.json["error"]["message"] == expected_message 150 | 151 | def test_bad_header_format(self, sample_app, client): 152 | expected_message = ( 153 | "Invalid authorization header format. Expected: Bearer " 154 | ) 155 | expected_status_code = 401 156 | 157 | get_response = client.get( 158 | self.protected_route, 159 | headers={"Authorization": "{{TOKEN}}"}, 160 | ) 161 | assert get_response.status_code == expected_status_code 162 | assert get_response.json["error"]["message"] == expected_message 163 | 164 | post_response = client.post( 165 | self.protected_route, 166 | headers={"Authorization": "{{TOKEN}}"}, 167 | ) 168 | assert post_response.status_code == expected_status_code 169 | assert post_response.json["error"]["message"] == expected_message 170 | 171 | def test_bad_auth_scheme(self, sample_app, client): 172 | expected_message = "Invalid authorization scheme. Expected: Bearer" 173 | expected_status_code = 401 174 | 175 | get_response = client.get( 176 | self.protected_route, 177 | headers={"Authorization": "JWT {{TOKEN}}"}, 178 | ) 179 | assert get_response.status_code == expected_status_code 180 | assert get_response.json["error"]["message"] == expected_message 181 | 182 | post_response = client.post( 183 | self.protected_route, 184 | headers={"Authorization": "JWT {{TOKEN}}"}, 185 | ) 186 | assert post_response.status_code == expected_status_code 187 | assert post_response.json["error"]["message"] == expected_message 188 | 189 | def test_bad_token(self, sample_app_no_valid_token, client): 190 | expected_message = "Token validation Failed" 191 | expected_status_code = 401 192 | 193 | get_response = client.get( 194 | self.protected_route, 195 | headers={"Authorization": "Bearer {{TOKEN}}"}, 196 | ) 197 | assert get_response.status_code == expected_status_code 198 | assert get_response.json["error"]["message"] == expected_message 199 | 200 | post_response = client.post( 201 | self.protected_route, 202 | headers={"Authorization": "Bearer {{TOKEN}}"}, 203 | ) 204 | assert post_response.status_code == expected_status_code 205 | assert post_response.json["error"]["message"] == expected_message 206 | 207 | def test_valid_token(self, sample_app, client): 208 | expected_message = "Hello test@email.com!" 209 | expected_status_code = 200 210 | 211 | get_response = client.get( 212 | self.protected_route, 213 | headers={"Authorization": "Bearer {{TOKEN}}"}, 214 | ) 215 | assert get_response.status_code == expected_status_code 216 | assert get_response.json["message"] == expected_message 217 | 218 | post_response = client.post( 219 | self.protected_route, 220 | headers={"Authorization": "Bearer {{TOKEN}}"}, 221 | ) 222 | assert post_response.status_code == expected_status_code 223 | assert post_response.json["message"] == expected_message 224 | 225 | 226 | class TestDecodeToken: 227 | @pytest.mark.parametrize(["check_revoked"], [[True], [False]]) 228 | def test_method_calls_verify_id_token( 229 | self, 230 | app: AppFixture, 231 | try_initialize_app: TryInitializeAppFixture, # to mock call to initialize_app 232 | verify_id_token: VerifyIdTokenFixture, 233 | check_revoked, 234 | ): 235 | app.config["FIREBASE_ADMIN_CHECK_REVOKED"] = check_revoked 236 | admin = flask_firebase_admin.FirebaseAdmin(app) 237 | 238 | token = "abc" 239 | decoded_token = admin.decode_token(token) 240 | 241 | verify_id_token.assert_called_once_with(token, admin.admin, check_revoked) 242 | assert decoded_token is verify_id_token.return_value 243 | 244 | 245 | class TestMake401: 246 | @pytest.mark.parametrize( 247 | ["message", "auth_scheme", "expected_header"], 248 | [ 249 | ("my-error-msg", "AuthScheme", 'AuthScheme realm="API", charset="UTF-8"'), 250 | ("another-error", "JWT", 'JWT realm="API", charset="UTF-8"'), 251 | ], 252 | ) 253 | def test_generated_response_object( 254 | self, 255 | app: AppFixture, 256 | message, 257 | auth_scheme, 258 | expected_header, 259 | ): 260 | app.config["FIREBASE_ADMIN_AUTHORIZATION_SCHEME"] = auth_scheme 261 | 262 | # we don't need a fully initialized extension 263 | admin = flask_firebase_admin.FirebaseAdmin() 264 | 265 | response = admin.make_401(message) 266 | 267 | assert response.headers["WWW-Authenticate"] == expected_header 268 | assert response.status_code == 401 269 | assert ( 270 | response.is_json 271 | and response.json["error"]["message"] == message # type: ignore 272 | ) 273 | 274 | 275 | class TestTryInitializeApp: 276 | def test_calls_firebase_admin_initialize_app( 277 | self, 278 | initialize_app: InitializeAppFixture, 279 | ): 280 | cred, options, name = "my-creds", "my-options", "my-name" 281 | ffa.try_initialize_app(cred, options, name) 282 | initialize_app.assert_called_once_with(cred, options=options, name=name) 283 | 284 | def test_raises_RuntimeError_by_default_when_admin_app_already_initialize( 285 | self, 286 | initialize_app: InitializeAppFixture, 287 | ): 288 | initialize_app.side_effect = ValueError() 289 | cred, options, name = "my-creds", "my-options", "my-name" 290 | with pytest.raises(RuntimeError): 291 | ffa.try_initialize_app(cred, options, name) 292 | initialize_app.assert_called_once_with(cred, options=options, name=name) 293 | 294 | def test_suppresses_ValueError_when_admin_app_already_exists_if_configured( 295 | self, 296 | initialize_app: InitializeAppFixture, 297 | get_app: GetAppFixture, 298 | ): 299 | initialize_app.side_effect = ValueError() 300 | cred, options, name = "my-creds", "my-options", "my-name" 301 | raise_if_app_exists = False 302 | 303 | admin = ffa.try_initialize_app(cred, options, name, raise_if_app_exists) 304 | 305 | initialize_app.assert_called_once_with(cred, options=options, name=name) 306 | get_app.assert_called_once_with(name) 307 | assert admin is get_app.return_value 308 | 309 | 310 | # FIREBASE_ADMIN_CHECK_REVOKED 311 | # FIREBASE_ADMIN_PAYLOAD_ATTR 312 | # FIREBASE_ADMIN_CREDENTIAL 313 | # FIREBASE_ADMIN_OPTIONS 314 | # FIREBASE_ADMIN_NAME 315 | # FIREBASE_ADMIN_RAISE_IF_APP_EXISTS 316 | 317 | 318 | class TestParseHeaderCredentials: 319 | @pytest.mark.parametrize( 320 | ["header", "expected"], 321 | [ 322 | ("", (None, None)), # empty string 323 | ("abc", (None, None)), # No space-separated values 324 | ("Bearer abc def", (None, None)), # too many space-separated values 325 | ], 326 | ) 327 | def test_badly_formatted(self, header: str, expected: Tuple[None, None]): 328 | assert expected == ffa.parse_header_credentials(header) 329 | 330 | @pytest.mark.parametrize( 331 | ["header", "expected"], 332 | [ 333 | ("Bearer abc", ("Bearer", "abc")), 334 | ("part1 part2", ("part1", "part2")), 335 | ], 336 | ) 337 | def test_correctly_formatted(self, header: str, expected: Tuple[str, str]): 338 | assert expected == ffa.parse_header_credentials(header) 339 | 340 | 341 | class TestGetAuthorizationHeader: 342 | def test_function_calls_request_headers_get(self, mocker: MockerFixture): 343 | req = mocker.MagicMock() 344 | ffa.get_authorization_header(req) 345 | 346 | req.headers.get.assert_called_once_with("Authorization") 347 | --------------------------------------------------------------------------------