├── tests ├── __init__.py ├── test-data │ ├── hello.txt │ └── screenshot.png ├── test_utils.py ├── test_validators.py ├── test_data.py ├── common.py ├── test_multiple_tus.py ├── test_views.py └── test_tus.py ├── aiohttp_tus ├── py.typed ├── __init__.py ├── annotations.py ├── validators.py ├── constants.py ├── utils.py ├── tus.py ├── views.py └── data.py ├── examples ├── __init__.py ├── uploads │ ├── __init__.py │ ├── constants.py │ ├── views.py │ ├── utils.py │ ├── app.py │ └── templates │ │ └── index.html ├── aiohttp_tus_app.py └── README.rst ├── .python-version ├── poetry.toml ├── docs ├── authors.rst ├── changelog.rst ├── requirements.in ├── api.rst ├── index.rst ├── Makefile ├── meta.rst ├── requirements.txt ├── conf.py └── usage.rst ├── .flake8 ├── AUTHORS.rst ├── .editorconfig ├── mypy.ini ├── LICENSE ├── Makefile ├── CHANGELOG.rst ├── pyproject.toml ├── .pre-commit-config.yaml ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── README.rst └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aiohttp_tus/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.1 2 | -------------------------------------------------------------------------------- /tests/test-data/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true 4 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: meta.rst 2 | .. include:: ../AUTHORS.rst 3 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: meta.rst 2 | .. include:: ../CHANGELOG.rst 3 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | aiohttp 2 | Sphinx==3.0.4 3 | sphinx_autodoc_typehints 4 | -------------------------------------------------------------------------------- /examples/uploads/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import create_app 2 | 3 | __all__ = ("create_app",) 4 | -------------------------------------------------------------------------------- /tests/test-data/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okumy/aiohttp-tus/HEAD/tests/test-data/screenshot.png -------------------------------------------------------------------------------- /examples/uploads/constants.py: -------------------------------------------------------------------------------- 1 | APP_UPLOAD_PATH_KEY = "aiohttp_tus_upload_path" 2 | DEFAULT_CLIENT_MAX_SIZE = "4MB" 3 | -------------------------------------------------------------------------------- /aiohttp_tus/__init__.py: -------------------------------------------------------------------------------- 1 | from .tus import setup_tus 2 | 3 | __all__ = ("setup_tus",) 4 | __license__ = "BSD-3-Clause" 5 | __version__ = "1.0.0" 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | application-import-names = aiohttp_tus, tests 3 | import-order-style = smarkets 4 | inline-quotes = double 5 | max-complexity = 15 6 | max-line-length = 88 7 | multiline-quotes = """ 8 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. include:: meta.rst 2 | 3 | ============= 4 | API Reference 5 | ============= 6 | 7 | aiohttp_tus 8 | =========== 9 | 10 | .. autofunction:: aiohttp_tus.setup_tus 11 | 12 | aiohttp_tus.data 13 | ================ 14 | 15 | .. autoclass:: aiohttp_tus.data.Resource 16 | -------------------------------------------------------------------------------- /examples/uploads/views.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | from aiohttp_jinja2 import render_template 3 | 4 | 5 | async def index(request: web.Request) -> web.Response: 6 | return render_template( 7 | "index.html", request, {"chunk_size": request.app._client_max_size - 1} 8 | ) 9 | -------------------------------------------------------------------------------- /examples/uploads/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import humanfriendly 4 | 5 | from .constants import DEFAULT_CLIENT_MAX_SIZE 6 | 7 | 8 | def get_client_max_size() -> int: 9 | return humanfriendly.parse_size( # type: ignore 10 | os.getenv("AIOHTTP_CLIENT_MAX_SIZE") or DEFAULT_CLIENT_MAX_SIZE 11 | ) 12 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Authors & Contributors 3 | ====================== 4 | 5 | ``aiohttp-tus`` is a `@okumy `_ project, which never 6 | happened without its authors & contributors, who listed below. 7 | 8 | - `Mikhail Kashkin `_ 9 | - `Igor Davydenko `_ 10 | - `Alwin Wang `_ 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{.babelrc,.compressrc,.eslintrc}] 11 | indent_size = 2 12 | 13 | [*.{css,cfg,html,ini,js,json,md,service,toml,yaml,yml,xml}] 14 | indent_size = 2 15 | 16 | [*.{elm,nginx,py,rst,sh}] 17 | indent_size = 4 18 | 19 | [Makefile] 20 | indent_style = tab 21 | tab_width = 4 22 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from multidict import CIMultiDict 3 | 4 | from aiohttp_tus.utils import parse_upload_metadata 5 | from tests.common import TEST_UPLOAD_METADATA, TEST_UPLOAD_METADATA_HEADER 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "metadata_header, expected", 10 | (("", CIMultiDict({})), (TEST_UPLOAD_METADATA_HEADER, TEST_UPLOAD_METADATA)), 11 | ) 12 | def test_parse_upload_metadata(metadata_header, expected): 13 | assert parse_upload_metadata(metadata_header) == expected 14 | -------------------------------------------------------------------------------- /tests/test_validators.py: -------------------------------------------------------------------------------- 1 | import attr 2 | 3 | from aiohttp_tus.validators import check_file_name, validate_upload_metadata 4 | from tests.common import TEST_CONFIG, TEST_DATA_PATH, TEST_UPLOAD_METADATA 5 | 6 | 7 | def test_check_file_name(): 8 | config = attr.evolve(TEST_CONFIG, upload_path=TEST_DATA_PATH) 9 | assert check_file_name(TEST_UPLOAD_METADATA, config=config) == "hello.txt" 10 | 11 | 12 | def test_validate_upload_metadata(): 13 | assert validate_upload_metadata(TEST_UPLOAD_METADATA) == TEST_UPLOAD_METADATA 14 | -------------------------------------------------------------------------------- /aiohttp_tus/annotations.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Mapping 2 | 3 | try: 4 | from aiohttp.web_middlewares import _Handler as Handler 5 | except ImportError: 6 | from aiohttp.web_middlewares import Handler 7 | 8 | Decorator = Callable[[Handler], Handler] 9 | 10 | DictStrAny = Dict[str, Any] 11 | DictStrBytes = Dict[str, bytes] 12 | DictStrStr = Dict[str, str] 13 | 14 | JsonDumps = Callable[[Any], str] 15 | JsonLoads = Callable[[str], Any] 16 | 17 | MappingStrBytes = Mapping[str, bytes] 18 | MappingStrStr = Mapping[str, str] 19 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: meta.rst 2 | .. include:: ../README.rst 3 | 4 | Installation 5 | ============ 6 | 7 | .. code-block:: bash 8 | 9 | pip install aiohttp-tus 10 | 11 | Or using `poetry `_: 12 | 13 | .. code-block:: bash 14 | 15 | poetry add aiohttp-tus 16 | 17 | License 18 | ======= 19 | 20 | *aiohttp-tus* is licensed under the terms of `BSD License 21 | `_. 22 | 23 | Contents 24 | ======== 25 | 26 | .. toctree:: 27 | :maxdepth: 3 28 | 29 | usage 30 | api 31 | authors 32 | changelog 33 | -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aiohttp_tus.data import get_resource_url, get_upload_url 4 | from tests.common import TEST_CONFIG 5 | 6 | 7 | def test_get_resource_url(): 8 | assert get_resource_url("/uploads") == r"/uploads/{resource_uid}" 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "resource_url, expected", 13 | ( 14 | (r"/uploads/{resource_uid}", "/uploads"), 15 | ("/uploads/19404e82-8008-4d64-9e97-023100c114c2", "/uploads"), 16 | ), 17 | ) 18 | def test_get_upload_url(resource_url, expected): 19 | assert get_upload_url(resource_url) == expected 20 | 21 | 22 | def test_upload_url_id(): 23 | assert TEST_CONFIG.upload_url_id == "L3VwbG9hZHM_" 24 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = aiohttp-middlewares 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /aiohttp_tus/validators.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional 3 | 4 | from aiohttp import web 5 | 6 | from .annotations import MappingStrBytes 7 | from .data import Config 8 | 9 | 10 | def check_file_name( 11 | valid_metadata: MappingStrBytes, *, config: Config 12 | ) -> Optional[str]: 13 | path = Path(valid_metadata["filename"].decode()) 14 | if any(config.upload_path.glob(f"{path.stem}.*")): 15 | return path.name 16 | return None 17 | 18 | 19 | def validate_upload_metadata(upload_metadata: MappingStrBytes) -> MappingStrBytes: 20 | if not upload_metadata.get("filename"): 21 | raise web.HTTPNotFound(text="Upload metadata missed filename value") 22 | return upload_metadata 23 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | allow_untyped_globals = True 3 | check_untyped_defs = True 4 | disallow_any_decorated = True 5 | disallow_any_generics = True 6 | disallow_incomplete_defs = True 7 | disallow_untyped_calls = True 8 | disallow_untyped_decorators = True 9 | disallow_untyped_defs = True 10 | follow_imports = normal 11 | follow_imports_for_stubs = True 12 | ignore_missing_imports = True 13 | namespace_packages = True 14 | show_column_numbers = True 15 | show_error_codes = True 16 | warn_redundant_casts = True 17 | warn_return_any = True 18 | warn_unused_ignores = True 19 | 20 | [mypy-conftest] 21 | ignore_errors = True 22 | 23 | [mypy-migrations.*] 24 | ignore_errors = True 25 | 26 | [mypy-tests.*] 27 | ignore_errors = True 28 | 29 | [mypy-*.tests.*] 30 | ignore_errors = True 31 | -------------------------------------------------------------------------------- /docs/meta.rst: -------------------------------------------------------------------------------- 1 | .. meta:: 2 | :description: Documentation of tus.io server implementation for aiohttp.web applications. 3 | :og:title: aiohttp-tus 4 | :og:description: Documentation of tus.io server implementation for aiohttp.web applications. 5 | :og:type: website 6 | :og:image: https://repository-images.githubusercontent.com/245408929/c407b000-695b-11ea-802d-c72f699ce95a 7 | :og:image:width: 1280 8 | :og:image:height: 640 9 | :og:image:alt: aiohttp-tus 10 | :twitter:card: summary_large_image 11 | :twitter:image:src: https://repository-images.githubusercontent.com/245408929/c407b000-695b-11ea-802d-c72f699ce95a 12 | :twitter:title: aiohttp-tus 13 | :twitter:description: Documentation of tus.io server implementation for aiohttp.web applications. 14 | -------------------------------------------------------------------------------- /aiohttp_tus/constants.py: -------------------------------------------------------------------------------- 1 | APP_TUS_CONFIG_KEY = "tus_config" 2 | 3 | HEADER_CACHE_CONTROL = "Cache-Control" 4 | HEADER_CONTENT_LENGTH = "Content-Length" 5 | HEADER_LOCATION = "Location" 6 | HEADER_TUS_EXTENSION = "Tus-Extension" 7 | HEADER_TUS_FILE_EXISTS = "Tus-File-Exists" 8 | HEADER_TUS_FILE_NAME = "Tus-File-Name" 9 | HEADER_TUS_MAX_SIZE = "Tus-Max-Size" 10 | HEADER_TUS_RESUMABLE = "Tus-Resumable" 11 | HEADER_TUS_TEMP_FILENAME = "Tus-Temp-Filename" 12 | HEADER_TUS_VERSION = "Tus-Version" 13 | HEADER_UPLOAD_LENGTH = "Upload-Length" 14 | HEADER_UPLOAD_METADATA = "Upload-Metadata" 15 | HEADER_UPLOAD_OFFSET = "Upload-Offset" 16 | 17 | TUS_API_VERSION = "1.0.0" 18 | TUS_API_VERSION_SUPPORTED = "1.0.0" 19 | TUS_API_EXTENSIONS = ("creation", "termination", "file-check") 20 | TUS_MAX_FILE_SIZE = 4294967296 # 4GB 21 | 22 | BASE_HEADERS = { 23 | HEADER_TUS_RESUMABLE: TUS_API_VERSION, 24 | HEADER_TUS_VERSION: TUS_API_VERSION_SUPPORTED, 25 | } 26 | -------------------------------------------------------------------------------- /examples/aiohttp_tus_app.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | from pathlib import Path 4 | from typing import List 5 | 6 | from aiohttp import web 7 | 8 | from aiohttp_tus import setup_tus 9 | 10 | 11 | def create_app(argv: List[str] = None) -> web.Application: 12 | upload_path = Path(tempfile.gettempdir()) / "aiohttp-tus-app-uploads" 13 | upload_path.mkdir(mode=0o755, exist_ok=True) 14 | 15 | print("aiohttp_tus test app") 16 | print(f"Uploading files to {upload_path.absolute()}\n") 17 | 18 | app = setup_tus(web.Application(), upload_path=upload_path) 19 | 20 | app["aiohttp_tus_upload_path"] = upload_path 21 | app.on_shutdown.append(remove_upload_path) 22 | 23 | return app 24 | 25 | 26 | async def remove_upload_path(app: web.Application) -> None: 27 | upload_path = app["aiohttp_tus_upload_path"] 28 | print(f"\nRemoving {upload_path.absolute()} directory") 29 | shutil.rmtree(upload_path) 30 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from aiohttp.test_utils import TestClient 4 | from multidict import CIMultiDict 5 | 6 | from aiohttp_tus.data import Config 7 | 8 | 9 | rel = Path(__file__).parent 10 | 11 | TEST_CHUNK_SIZE = 10240 12 | 13 | TEST_DATA_PATH = rel / "test-data" 14 | TEST_FILE_NAME = "hello.txt" 15 | TEST_FILE_PATH = TEST_DATA_PATH / TEST_FILE_NAME 16 | 17 | TEST_SCREENSHOT_NAME = "screenshot.png" 18 | TEST_SCREENSHOT_PATH = TEST_DATA_PATH / TEST_SCREENSHOT_NAME 19 | 20 | TEST_UPLOAD_PATH = rel / "test-uploads" 21 | TEST_UPLOAD_URL = "/uploads" 22 | TEST_CONFIG = Config(upload_path=TEST_UPLOAD_PATH, upload_url=TEST_UPLOAD_URL) 23 | 24 | TEST_UPLOAD_METADATA_HEADER = "Content-Type dGV4dC9wbGFpbg==, Filename aGVsbG8udHh0" 25 | TEST_UPLOAD_METADATA_HEADERS = CIMultiDict( 26 | {"Upload-Metadata": TEST_UPLOAD_METADATA_HEADER} 27 | ) 28 | TEST_UPLOAD_METADATA = CIMultiDict( 29 | {"Content-Type": b"text/plain", "Filename": b"hello.txt"} 30 | ) 31 | 32 | 33 | def get_upload_url(client: TestClient, upload_url: str) -> str: 34 | return f"http://{client.host}:{client.port}{upload_url}" 35 | -------------------------------------------------------------------------------- /tests/test_multiple_tus.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from functools import partial 3 | from pathlib import Path 4 | 5 | import tus 6 | from aiohttp import web 7 | 8 | from aiohttp_tus import setup_tus 9 | from tests.common import get_upload_url, TEST_FILE_NAME, TEST_FILE_PATH 10 | 11 | 12 | NUMBER = 5 13 | 14 | 15 | async def test_mutltiple_tus_upload_urls(aiohttp_client, loop): 16 | upload = partial(tus.upload, file_name=TEST_FILE_NAME) 17 | 18 | with tempfile.TemporaryDirectory(prefix="aiohttp_tus") as temp_path: 19 | base_path = Path(temp_path) 20 | app = web.Application() 21 | 22 | for idx in range(1, NUMBER + 1): 23 | setup_tus( 24 | app, upload_path=base_path / str(idx), upload_url=f"/{idx}/uploads" 25 | ) 26 | 27 | client = await aiohttp_client(app) 28 | 29 | for idx in range(1, NUMBER + 1): 30 | with open(TEST_FILE_PATH, "rb") as handler: 31 | await loop.run_in_executor( 32 | None, upload, handler, get_upload_url(client, f"/{idx}/uploads") 33 | ) 34 | 35 | expected_path = base_path / str(idx) / TEST_FILE_NAME 36 | assert expected_path.exists() 37 | assert expected_path.read_bytes() == TEST_FILE_PATH.read_bytes() 38 | -------------------------------------------------------------------------------- /examples/uploads/app.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | from pathlib import Path 4 | from typing import List 5 | 6 | import jinja2 7 | from aiohttp import web 8 | from aiohttp_jinja2 import setup as setup_jinja2 9 | 10 | from aiohttp_tus import setup_tus 11 | from . import views 12 | from .constants import APP_UPLOAD_PATH_KEY 13 | from .utils import get_client_max_size 14 | 15 | 16 | def create_app(argv: List[str] = None) -> web.Application: 17 | upload_path = Path(tempfile.gettempdir()) / "aiohttp-tus-example-uploads" 18 | upload_path.mkdir(mode=0o755, exist_ok=True) 19 | 20 | app = setup_tus( 21 | web.Application(client_max_size=get_client_max_size()), upload_path=upload_path, 22 | ) 23 | app[APP_UPLOAD_PATH_KEY] = upload_path 24 | setup_jinja2( 25 | app, loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates") 26 | ) 27 | 28 | app.router.add_get("/", views.index) 29 | app.on_shutdown.append(remove_upload_path) 30 | 31 | print("aiohttp-tus example app") 32 | print(f"Uploading files to {upload_path.absolute()}\n") 33 | 34 | return app 35 | 36 | 37 | async def remove_upload_path(app: web.Application) -> None: 38 | upload_path = app[APP_UPLOAD_PATH_KEY] 39 | print(f"\nRemoving {upload_path.absolute()}") 40 | shutil.rmtree(upload_path) 41 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | aiohttp-tus Examples 3 | ==================== 4 | 5 | aiohttp_tus_app 6 | =============== 7 | 8 | To illustrate that uploading via `tus.py `_ library 9 | works well. 10 | 11 | To run, 12 | 13 | .. code-block:: bash 14 | 15 | make -C .. EXAMPLE=aiohttp_tus_app example 16 | 17 | After, upload large files as, 18 | 19 | .. code-block:: bash 20 | 21 | cd .. && poetry run tus-upload --chunk-size=1000000 /path/to/large-file http://localhost:8300/uploads 22 | 23 | Then check that files uploaded to upload directory. 24 | 25 | uploads 26 | ======= 27 | 28 | To illustrate that uploading via `Uppy `_ JavaScript library works 29 | as expected. 30 | 31 | To run, 32 | 33 | .. code-block:: bash 34 | 35 | make -C .. EXAMPLE=uploads example 36 | 37 | After, open ``http://localhost:8080`` and try to upload some files. All uploads will be 38 | available in temporary directory. 39 | 40 | Chunk Size 41 | ---------- 42 | 43 | This example uses chunks size of 4MB, but you might want customize things by setting up 44 | other chunk size via ``AIOHTTP_CLIENT_MAX_SIZE`` env var. 45 | 46 | For example, 47 | 48 | .. code-block:: bash 49 | 50 | AIOHTTP_CLIENT_MAX_SIZE=10MB make -C .. EXAMPLE=uploads example 51 | 52 | will set `client_max_size `_ 53 | param to ``10_000_000`` bytes. 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Okumy 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from aiohttp import web 3 | from aiohttp.test_utils import TestClient 4 | 5 | from aiohttp_tus import setup_tus 6 | from tests.common import TEST_UPLOAD_PATH, TEST_UPLOAD_URL 7 | 8 | 9 | @pytest.fixture 10 | def tus_test_client(aiohttp_client): 11 | async def factory() -> TestClient: 12 | return await aiohttp_client( 13 | setup_tus( 14 | web.Application(), 15 | upload_path=TEST_UPLOAD_PATH, 16 | upload_url=TEST_UPLOAD_URL, 17 | ) 18 | ) 19 | 20 | return factory 21 | 22 | 23 | async def test_start_upload(tus_test_client): 24 | client = await tus_test_client() 25 | response = await client.post(TEST_UPLOAD_URL) 26 | assert response.status == 500 27 | assert ( 28 | await response.text() 29 | == "Received file upload for unsupported file transfer protocol" 30 | ) 31 | 32 | 33 | async def test_upload_options_200(tus_test_client): 34 | client = await tus_test_client() 35 | response = await client.options(TEST_UPLOAD_URL) 36 | assert response.status == 200 37 | assert await response.text() == "" 38 | 39 | 40 | async def test_upload_options_204(tus_test_client): 41 | client = await tus_test_client() 42 | response = await client.options(TEST_UPLOAD_URL, headers={"Tus-Resumable": "1.0.0"}) 43 | assert response.status == 204 44 | 45 | headers = response.headers 46 | assert headers["Tus-Resumable"] == "1.0.0" 47 | assert headers["Tus-Version"] == "1.0.0" 48 | assert headers["Tus-Extension"] == "creation,termination,file-check" 49 | assert headers["Tus-Max-Size"] == "4294967296" 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: \ 2 | clean \ 3 | coveralls \ 4 | distclean \ 5 | docs \ 6 | example \ 7 | install \ 8 | lint \ 9 | lint-only \ 10 | list-outdated \ 11 | test \ 12 | test-only 13 | 14 | # Project constants 15 | PROJECT = aiohttp_tus 16 | DOCS_DIR = ./docs 17 | 18 | # Project vars 19 | POETRY ?= poetry 20 | PRE_COMMIT ?= pre-commit 21 | PYTHON ?= $(POETRY) run python 22 | TOX ?= tox 23 | 24 | # Docs vars 25 | DOCS_HOST ?= localhost 26 | DOCS_PORT ?= 8242 27 | 28 | # Example vars 29 | AIOHTTP_PORT ?= 8300 30 | 31 | all: install 32 | 33 | clean: 34 | find . \( -name __pycache__ -o -type d -empty \) -exec rm -rf {} + 2> /dev/null 35 | 36 | distclean: clean 37 | rm -rf build/ dist/ *.egg*/ .tox/ .venv/ .install 38 | 39 | docs: .install 40 | $(PYTHON) -m pip install -r docs/requirements.txt 41 | $(POETRY) run sphinx-autobuild -B -H $(DOCS_HOST) -p $(DOCS_PORT) -b html $(DOCS_DIR)/ $(DOCS_DIR)/_build/ 42 | 43 | example: .install 44 | ifeq ($(EXAMPLE),) 45 | # EXAMPLE env var is required, e.g. `make EXAMPLE=uploads example` 46 | @exit 1 47 | else 48 | $(PYTHON) -m aiohttp.web --port $(AIOHTTP_PORT) examples.$(EXAMPLE):create_app 49 | endif 50 | 51 | install: .install 52 | .install: pyproject.toml poetry.toml poetry.lock 53 | $(POETRY) install 54 | touch $@ 55 | 56 | lint: install lint-only 57 | 58 | lint-only: 59 | SKIP=$(SKIP) $(PRE_COMMIT) run --all $(HOOK) 60 | 61 | list-outdated: install 62 | $(POETRY) show -o 63 | 64 | poetry.toml: 65 | $(POETRY) config --local virtualenvs.create true 66 | $(POETRY) config --local virtualenvs.in-project true 67 | 68 | test: install clean lint test-only 69 | 70 | test-only: 71 | -rm -rf tests/test-uploads/ 72 | TOXENV=$(TOXENV) $(TOX) $(TOX_ARGS) -- $(TEST_ARGS) 73 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile docs/requirements.in 6 | # 7 | aiohttp==3.7.4 8 | # via -r requirements.in 9 | alabaster==0.7.12 10 | # via sphinx 11 | async-timeout==3.0.1 12 | # via aiohttp 13 | attrs==19.3.0 14 | # via aiohttp 15 | babel==2.9.1 16 | # via sphinx 17 | certifi==2020.4.5.2 18 | # via requests 19 | chardet==3.0.4 20 | # via 21 | # aiohttp 22 | # requests 23 | docutils==0.16 24 | # via sphinx 25 | idna==2.9 26 | # via 27 | # requests 28 | # yarl 29 | imagesize==1.2.0 30 | # via sphinx 31 | jinja2==2.11.2 32 | # via sphinx 33 | markupsafe==1.1.1 34 | # via jinja2 35 | multidict==4.7.6 36 | # via 37 | # aiohttp 38 | # yarl 39 | packaging==20.4 40 | # via sphinx 41 | pygments==2.6.1 42 | # via sphinx 43 | pyparsing==2.4.7 44 | # via packaging 45 | pytz==2020.1 46 | # via babel 47 | requests==2.23.0 48 | # via sphinx 49 | six==1.15.0 50 | # via packaging 51 | snowballstemmer==2.0.0 52 | # via sphinx 53 | sphinx==3.0.4 54 | # via 55 | # -r requirements.in 56 | # sphinx-autodoc-typehints 57 | sphinx-autodoc-typehints==1.10.3 58 | # via -r requirements.in 59 | sphinxcontrib-applehelp==1.0.2 60 | # via sphinx 61 | sphinxcontrib-devhelp==1.0.2 62 | # via sphinx 63 | sphinxcontrib-htmlhelp==1.0.3 64 | # via sphinx 65 | sphinxcontrib-jsmath==1.0.1 66 | # via sphinx 67 | sphinxcontrib-qthelp==1.0.3 68 | # via sphinx 69 | sphinxcontrib-serializinghtml==1.1.4 70 | # via sphinx 71 | typing-extensions==3.10.0.2 72 | # via aiohttp 73 | urllib3==1.25.9 74 | # via requests 75 | yarl==1.4.2 76 | # via aiohttp 77 | 78 | # The following packages are considered to be unsafe in a requirements file: 79 | # setuptools 80 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 1.1.0 (2022-01-04) 6 | ================== 7 | 8 | - Fix that support aiohttp >3.8 9 | 10 | 11 | 1.0.0 (2020-06-08) 12 | ================== 13 | 14 | - Final **1.0.0** release, which marks ``aiohttp-tus`` as library ready to be used in 15 | production 16 | 17 | 1.0.0rc1 (2020-04-02) 18 | ===================== 19 | 20 | - Fix upload large files via multiple chunks 21 | 22 | 1.0.0rc0 (2020-03-26) 23 | ===================== 24 | 25 | - Add example to ensure that upload via `Uppy `_ JavaScript library 26 | works as expected 27 | - Fix resuming uploads by passing missed ``Upload-Length`` header: 28 | `#5 `_ 29 | - Add documentation about `CORS Headers `_ 30 | - Allow to provide upload resource name, which can be lately used for URL reversing 31 | 32 | 1.0.0b2 (2020-03-18) 33 | ==================== 34 | 35 | - Ensure trailing slash upload URLs working as well 36 | 37 | 1.0.0b1 (2020-03-18) 38 | ==================== 39 | 40 | - Add brief documentation 41 | - Use canonical upload URL for tus config mapping 42 | 43 | 1.0.0b0 (2020-03-15) 44 | ==================== 45 | 46 | - Allow to setup tus upload URLs multiple times for one ``aiohttp.web`` application 47 | - Allow to call callback after upload is done 48 | - Provide many unit tests for tus views 49 | 50 | 1.0.0a1 (2020-03-12) 51 | ==================== 52 | 53 | - Allow to decorate upload views for authentication or other (for example *to check 54 | whether entity for upload exists or not*) needs 55 | - Allow to upload on named upload paths, when using named upload URLs 56 | - Ensure named upload URLs (e.g. ``/user/{username}/uploads``) works as well 57 | - Ensure package is typed by adding ``py.typed`` 58 | 59 | 1.0.0a0 (2020-03-11) 60 | ==================== 61 | 62 | - First public release with minimal valuable coverage of ``tus.io`` protocol for 63 | ``aiohttp.web`` applications 64 | -------------------------------------------------------------------------------- /aiohttp_tus/utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | from pathlib import Path 4 | 5 | from aiohttp import web 6 | from multidict import CIMultiDict 7 | 8 | from .annotations import DictStrBytes, MappingStrBytes 9 | from .data import Config, get_config, get_resource_path, Resource 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def get_resource(request: web.Request) -> Resource: 16 | return Resource.from_metadata( 17 | config=get_config(request), match_info=request.match_info, 18 | ) 19 | 20 | 21 | def get_resource_or_404(request: web.Request) -> Resource: 22 | try: 23 | return get_resource(request) 24 | except IOError: 25 | logger.warning( 26 | "Unable to read resource metadata by requested UID", 27 | extra={"resource_uid": request.match_info["resource_uid"]}, 28 | ) 29 | raise web.HTTPNotFound(text="") 30 | 31 | 32 | def get_resource_or_410(request: web.Request) -> Resource: 33 | try: 34 | resource = get_resource(request) 35 | if not get_resource_path( 36 | config=get_config(request), match_info=request.match_info, uid=resource.uid 37 | ).exists(): 38 | raise IOError(f"{resource.uid} does not exist") 39 | except IOError: 40 | logger.warning( 41 | "Attempt to continue upload of removed resource", 42 | extra={"file_name" "resource_uid": resource.uid}, 43 | ) 44 | raise web.HTTPGone(text="") 45 | return resource 46 | 47 | 48 | async def on_upload_done( 49 | *, request: web.Request, config: Config, resource: Resource, file_path: Path 50 | ) -> None: 51 | if not config.on_upload_done: 52 | return 53 | 54 | await config.on_upload_done(request, resource, file_path) 55 | 56 | 57 | def parse_upload_metadata(metadata_header: str) -> MappingStrBytes: 58 | metadata: DictStrBytes = {} 59 | 60 | for item in metadata_header.split(","): 61 | if not item: 62 | continue 63 | key, value = item.split() 64 | metadata[key] = base64.b64decode(value) 65 | 66 | return CIMultiDict(metadata) 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line_length = 88 3 | 4 | [tool.coverage.run] 5 | branch = true 6 | source = ["aiohttp_tus"] 7 | 8 | [tool.coverage.paths] 9 | source = ["aiohttp_tus/"] 10 | 11 | [tool.coverage.report] 12 | fail_under = 85 13 | skip_covered = true 14 | show_missing = true 15 | 16 | [tool.poetry] 17 | name = "aiohttp-tus" 18 | version = "1.1.0" 19 | description = "tus.io protocol implementation for aiohttp.web applications" 20 | authors = ["Igor Davydenko "] 21 | license = "BSD-3-Clause" 22 | readme = "README.rst" 23 | repository = "https://github.com/okumy/aiohttp-tus" 24 | documentation = "https://aiohttp-tus.readthedocs.io/" 25 | keywords = ["aiohttp", "tus", "tus.io"] 26 | classifiers = [ 27 | "Development Status :: 5 - Production/Stable", 28 | "Environment :: Web Environment", 29 | "Framework :: AsyncIO", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: BSD License", 32 | "Operating System :: OS Independent", 33 | "Topic :: Internet :: WWW/HTTP", 34 | "Topic :: Utilities", 35 | "Typing :: Typed" 36 | ] 37 | 38 | [tool.poetry.dependencies] 39 | python = "^3.10" 40 | aiohttp = "^3.8.1" 41 | 42 | [tool.poetry.dev-dependencies] 43 | aiohttp-jinja2 = "^1.2.0" 44 | async_generator = {version = "^1.10", python = "~3.6"} 45 | coverage = {extras = ["toml"], version = "^6.2"} 46 | humanfriendly = "^10.0" 47 | pytest = "^6.2.5" 48 | pytest-aiohttp = "^0.3.0" 49 | pytest-asyncio = "^0.16.0" 50 | pytest-cov = "^3.0.0" 51 | sphinx-autobuild = "^0.7.1" 52 | "tus.py" = "^1.3.4" 53 | 54 | [tool.tox] 55 | legacy_tox_ini = """ 56 | [tox] 57 | envlist = py36,py37,py38 58 | skipsdist = True 59 | 60 | [gh-actions] 61 | python = 62 | 3.8: py38 63 | 3.9: py39 64 | 3.10: py310 65 | 66 | [testenv] 67 | passenv = 68 | PYTHONPATH 69 | PYTEST_ADDOPTS 70 | setenv = 71 | PYTHONPATH=. 72 | skip_install = True 73 | whitelist_externals = poetry 74 | commands_pre = 75 | poetry install 76 | commands = 77 | poetry run python -m pytest tests/ --cov --no-cov-on-fail 78 | """ 79 | 80 | [build-system] 81 | requires = ["poetry>=0.12"] 82 | build-backend = "poetry.masonry.api" 83 | -------------------------------------------------------------------------------- /examples/uploads/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aiohttp-tus & uppy.io example 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 |

16 | aiohttp-tus & 17 | uppy.io example 18 |

19 | 20 |
    21 |
  • aiohttp-tus version: 1.0.0rc0
  • 22 |
  • Uppy.io version: 1.10.1
  • 23 |
24 | 25 |
26 |
27 | 28 |
29 |

Uploaded files:

30 |
    31 |
    32 | 33 |
    34 | Allowed to upload up to 10 files per session. 35 |
    36 | 37 | 38 | 39 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | minimum_pre_commit_version: "1.17.0" 2 | repos: 3 | - repo: "https://github.com/commitizen-tools/commitizen" 4 | rev: "v1.23.0" 5 | hooks: 6 | - id: "commitizen" 7 | language_version: "python3.8" 8 | stages: ["commit-msg"] 9 | 10 | - repo: "https://github.com/psf/black" 11 | rev: "19.10b0" 12 | hooks: 13 | - id: "black" 14 | name: "Format code (black)" 15 | language_version: "python3.8" 16 | exclude: ^docs/.*$ 17 | 18 | - repo: "https://github.com/asottile/blacken-docs" 19 | rev: "v1.7.0" 20 | hooks: 21 | - id: "blacken-docs" 22 | name: "Format docs (blacken-docs)" 23 | language_version: "python3.8" 24 | args: ["-l", "64"] 25 | additional_dependencies: 26 | - "black==19.10b0" 27 | 28 | - repo: "https://github.com/pre-commit/pre-commit-hooks" 29 | rev: "v3.1.0" 30 | hooks: 31 | - id: "end-of-file-fixer" 32 | - id: "trailing-whitespace" 33 | - id: "check-json" 34 | - id: "check-toml" 35 | - id: "check-yaml" 36 | 37 | - repo: "https://gitlab.com/PyCQA/flake8" 38 | rev: "3.8.3" 39 | hooks: 40 | - id: "flake8" 41 | name: "Lint code (flake8)" 42 | language_version: "python3.8" 43 | additional_dependencies: &flake8_additional_dependencies 44 | - "flake8==3.8.3" 45 | - "flake8-broken-line==0.2.1" 46 | - "flake8-bugbear==20.1.4" 47 | - "flake8-builtins==1.5.3" 48 | - "flake8-comprehensions==3.2.3" 49 | - "flake8-eradicate==0.4.0" 50 | - "flake8-import-order==0.18.1" 51 | - "flake8-mutable==1.2.0" 52 | - "flake8-pie==0.5.0" 53 | - "flake8-quotes==3.2.0" 54 | - "flake8-string-format==0.3.0" 55 | - "flake8-tidy-imports==4.1.0" 56 | - "flake8-variables-names==0.0.3" 57 | - "pep8-naming==0.11.1" 58 | exclude: ^docs/.*$ 59 | 60 | - repo: "https://github.com/asottile/yesqa" 61 | rev: "v1.2.1" 62 | hooks: 63 | - id: yesqa 64 | name: "Lint code (yesqa)" 65 | language_version: "python3.8" 66 | additional_dependencies: *flake8_additional_dependencies 67 | exclude: ^docs/.*$ 68 | 69 | - repo: "https://github.com/pre-commit/mirrors-mypy" 70 | rev: "v0.782" 71 | hooks: 72 | - id: "mypy" 73 | name: "Lint code (mypy)" 74 | language_version: "python3.8" 75 | args: ["--python-executable=.venv/bin/python3"] 76 | exclude: ^docs/.*$ 77 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aiohttp-tus documentation build configuration file, created by 5 | # sphinx-quickstart on Sun May 14 20:32:17 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | 21 | import re 22 | import sys 23 | from pathlib import Path 24 | 25 | 26 | rel = Path(__file__).parent.parent 27 | sys.path.insert(0, str(rel)) 28 | 29 | INIT_PY = (rel / "aiohttp_tus" / "__init__.py").read_text() 30 | VERSION = re.findall('__version__ = "([^"]+)"', INIT_PY)[0] 31 | 32 | 33 | extensions = [ 34 | "alabaster", 35 | "sphinx.ext.autodoc", 36 | "sphinx_autodoc_typehints", 37 | "sphinx.ext.intersphinx", 38 | "sphinx.ext.coverage", 39 | "sphinx.ext.viewcode", 40 | ] 41 | 42 | templates_path = ["_templates"] 43 | source_suffix = ".rst" 44 | master_doc = "index" 45 | 46 | project = "aiohttp-tus" 47 | description = "tus.io server implementation for aiohttp.web applications" 48 | copyright = "2020, Okumy" 49 | author = "Igor Davydenko" 50 | 51 | version = ".".join(VERSION.split(".")[:2]) 52 | release = VERSION 53 | 54 | language = "en" 55 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 56 | pygments_style = "sphinx" 57 | 58 | html_theme = "alabaster" 59 | html_theme_options = { 60 | "logo_name": True, 61 | "description": description, 62 | "github_user": "okumy", 63 | "github_repo": project, 64 | "github_banner": True, 65 | "github_button": True, 66 | "github_type": "star", 67 | "fixed_sidebar": True, 68 | } 69 | html_static_path = ["_static"] 70 | html_sidebars = {"**": ["about.html", "localtoc.html", "searchbox.html"]} 71 | 72 | htmlhelp_basename = "aiohttp-tusdoc" 73 | latex_elements = {} 74 | latex_documents = [ 75 | (master_doc, "aiohttp-tus.tex", "aiohttp-tus Documentation", "Okumy", "manual",) 76 | ] 77 | 78 | man_pages = [(master_doc, "aiohttp-tus", "aiohttp-tus Documentation", [author], 1,)] 79 | 80 | texinfo_documents = [ 81 | ( 82 | master_doc, 83 | "aiohttp-tus", 84 | "aiohttp-tus Documentation", 85 | author, 86 | "aiohttp-tus", 87 | description, 88 | "Miscellaneous", 89 | ) 90 | ] 91 | 92 | intersphinx_mapping = { 93 | "https://docs.python.org/3/": None, 94 | "https://aiohttp.readthedocs.io/en/stable/": None, 95 | } 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | #.python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # End of https://www.toptal.com/developers/gitignore/api/python 140 | 141 | # make install target 142 | .install 143 | 144 | # Test Uploads 145 | tests/test-uploads/ 146 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "ci" 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | tags: ["v*"] 7 | pull_request: 8 | branches: ["master"] 9 | 10 | env: 11 | DEV_PYTHON_VERSION: "3.8" 12 | POETRY_VERSION: "1.0.9" 13 | TOX_GH_ACTIONS_VERSION: "1.3.0" 14 | TOX_VERSION: "3.16.1" 15 | TWINE_VERSION: "3.1.1" 16 | 17 | jobs: 18 | dev: 19 | name: "Verify package is installed well" 20 | 21 | strategy: 22 | matrix: 23 | os: ["ubuntu-latest", "macos-latest"] 24 | 25 | runs-on: "${{ matrix.os }}" 26 | 27 | steps: 28 | - uses: "actions/checkout@v2.3.1" 29 | 30 | - name: "Install Python" 31 | uses: "actions/setup-python@v2.0.1" 32 | with: 33 | python-version: "${{ env.DEV_PYTHON_VERSION }}" 34 | 35 | - name: "Install poetry" 36 | uses: "dschep/install-poetry-action@v1.3" 37 | with: 38 | version: "${{ env.POETRY_VERSION }}" 39 | 40 | - name: "Install package" 41 | run: "poetry install --no-dev" 42 | 43 | - name: "Import package" 44 | run: "poetry run python -c 'import aiohttp_tus'" 45 | 46 | test: 47 | name: "Python ${{ matrix.python-version }}" 48 | 49 | strategy: 50 | matrix: 51 | python-version: ["3.6", "3.7", "3.8"] 52 | 53 | runs-on: "ubuntu-latest" 54 | 55 | steps: 56 | - uses: "actions/checkout@v2.3.1" 57 | 58 | - name: "Install Python" 59 | uses: "actions/setup-python@v2.0.1" 60 | with: 61 | python-version: "${{ matrix.python-version }}" 62 | 63 | - name: "Install poetry" 64 | uses: "dschep/install-poetry-action@v1.3" 65 | with: 66 | version: "${{ env.POETRY_VERSION }}" 67 | 68 | - name: "Install package" 69 | run: | 70 | poetry install 71 | python -m pip install tox==${{ env.TOX_VERSION }} tox-gh-actions==${{ env.TOX_GH_ACTIONS_VERSION }} 72 | 73 | - name: "Cache pre-commit" 74 | if: "matrix.python-version == 'env.DEV_PYTHON_VERSION'" 75 | uses: "actions/cache@v2.0.0" 76 | with: 77 | path: "~/.cache/pre-commit" 78 | key: "pre-commit-${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}" 79 | 80 | - name: "Run pre-commit" 81 | if: "matrix.python-version == 'env.DEV_PYTHON_VERSION'" 82 | uses: "pre-commit/action@v2.0.0" 83 | 84 | - name: "Run tests" 85 | run: "python -m tox" 86 | 87 | package: 88 | needs: "test" 89 | name: "Build & deploy package" 90 | 91 | runs-on: "ubuntu-latest" 92 | 93 | steps: 94 | - uses: "actions/checkout@v2.3.1" 95 | 96 | - name: "Install Python" 97 | uses: "actions/setup-python@v2.0.1" 98 | with: 99 | python-version: "${{ env.DEV_PYTHON_VERSION }}" 100 | 101 | - name: "Install poetry" 102 | uses: "dschep/install-poetry-action@v1.3" 103 | with: 104 | version: "${{ env.POETRY_VERSION }}" 105 | 106 | - name: "Build package" 107 | run: "poetry build" 108 | 109 | - name: "Check package" 110 | run: | 111 | python -m pip install twine==${{ env.TWINE_VERSION }} 112 | python -m twine check dist/* 113 | 114 | - name: "Publish package" 115 | if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')" 116 | uses: "pypa/gh-action-pypi-publish@v1.3.1" 117 | with: 118 | user: "${{ secrets.PYPI_USERNAME }}" 119 | password: "${{ secrets.PYPI_PASSWORD }}" 120 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | aiohttp-tus 3 | =========== 4 | 5 | .. image:: https://github.com/pylotcode/aiohttp-tus/workflows/ci/badge.svg 6 | :target: https://github.com/pylotcode/aiohttp-tus/actions?query=workflow%3A%22ci%22 7 | :alt: CI Workflow 8 | 9 | .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white 10 | :target: https://github.com/pre-commit/pre-commit 11 | :alt: pre-commit 12 | 13 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 14 | :target: https://github.com/psf/black 15 | :alt: black 16 | 17 | .. image:: https://img.shields.io/pypi/v/aiohttp-tus.svg 18 | :target: https://pypi.org/project/aiohttp-tus/ 19 | :alt: Latest Version 20 | 21 | .. image:: https://img.shields.io/pypi/pyversions/aiohttp-tus.svg 22 | :target: https://pypi.org/project/aiohttp-tus/ 23 | :alt: Python versions 24 | 25 | .. image:: https://img.shields.io/pypi/l/aiohttp-tus.svg 26 | :target: https://github.com/pylotcode/aiohttp-tus/blob/master/LICENSE 27 | :alt: BSD License 28 | 29 | .. image:: https://readthedocs.org/projects/aiohttp-tus/badge/?version=latest 30 | :target: http://aiohttp-tus.readthedocs.org/en/latest/ 31 | :alt: Documentation 32 | 33 | `tus.io `_ server implementation for 34 | `aiohttp.web `_ applications. 35 | 36 | For uploading large files, please consider using 37 | `aiotus `_ (Python 3.7+) library instead. 38 | 39 | - Works on Python 3.6+ 40 | - Works with aiohttp 3.5+ 41 | - BSD licensed 42 | - Latest documentation `on Read The Docs 43 | `_ 44 | - Source, issues, and pull requests `on GitHub 45 | `_ 46 | 47 | Quickstart 48 | ========== 49 | 50 | Code belows shows how to enable tus-compatible uploads on ``/uploads`` URL for 51 | ``aiohttp.web`` application. After upload, files will be available at ``../uploads`` 52 | directory. 53 | 54 | .. code-block:: python 55 | 56 | from pathlib import Path 57 | 58 | from aiohttp import web 59 | from aiohttp_tus import setup_tus 60 | 61 | 62 | app = setup_tus( 63 | web.Application(), 64 | upload_url="/uploads", 65 | upload_path=Path(__file__).parent.parent / "uploads", 66 | ) 67 | 68 | Chunk Size 69 | ========== 70 | 71 | Please, make sure to configure ``client_max_size`` for ``aiohttp.web`` Application and 72 | supply proper ``chunkSize`` for Uppy.io or other tus.io client. 73 | 74 | `Documentation `__ 75 | 76 | CORS Headers 77 | ============ 78 | 79 | To setup CORS headers you need to use `cors_middleware `_ 80 | from `aiohttp-middlewares`_ package. `aiohttp-cors `_ 81 | library not supported cause of 82 | `aio-libs/aiohttp-cors#241 `_ 83 | issue. 84 | 85 | `Documentation `__ 86 | 87 | .. _aiohttp-middlewares: https://pypi.org/project/aiohttp-middlewares/ 88 | 89 | Reverse proxy and HTTPS 90 | ======================= 91 | 92 | When ``aiohttp`` application deployed under the reverse proxy (such as nginx) with HTTPS 93 | support, it is needed to use `https_middleware `_ 94 | from `aiohttp-middlewares`_ package to ensure that ``web.Request`` instance has proper 95 | schema. 96 | 97 | `Documentation `__ 98 | 99 | Examples 100 | ======== 101 | 102 | `examples/ `_ directory 103 | contains several examples, which illustrate how to use ``aiohttp-tus`` with some tus.io 104 | clients, such as `tus.py `_ and 105 | `Uppy.io `_. 106 | -------------------------------------------------------------------------------- /aiohttp_tus/tus.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from aiohttp import web 5 | 6 | from . import views 7 | from .annotations import Decorator, Handler, JsonDumps, JsonLoads 8 | from .constants import APP_TUS_CONFIG_KEY 9 | from .data import Config, get_resource_url, ResourceCallback, set_config 10 | 11 | 12 | def setup_tus( 13 | app: web.Application, 14 | *, 15 | upload_path: Path, 16 | upload_url: str = "/uploads", 17 | upload_resource_name: str = None, 18 | allow_overwrite_files: bool = False, 19 | decorator: Decorator = None, 20 | on_upload_done: ResourceCallback = None, 21 | json_dumps: JsonDumps = json.dumps, 22 | json_loads: JsonLoads = json.loads, 23 | ) -> web.Application: 24 | """Setup tus protocol server implementation for aiohttp.web application. 25 | 26 | It is a cornerstone of ``aiohttp-tus`` library and in most cases only thing 27 | developers need to know for setting up tus.io server for aiohttp.web application. 28 | 29 | :param app: :class:`aiohttp.web.Application` instance 30 | :param upload_path: 31 | :class:`pathlib.Path` instance to point the directory where to store uploaded 32 | files. Please, esnure that given directory is exists before application start 33 | and is writeable for current user. 34 | 35 | It is possible to prepend any ``match_info`` param from named URL. 36 | :param upload_url: 37 | tus.io upload URL. Can be plain as ``/uploads`` or named as 38 | ``/users/{username}/uploads``. By default: ``"/uploads"`` 39 | :param upload_resource_name: 40 | By default ``aiohttp-tus`` will provide auto name for the upload resource, as 41 | well as for the chunk resource. But sometimes it might be useful to provide 42 | exact name, which can lately be used for URL reversing. 43 | :param allow_overwrite_files: 44 | When enabled allow to overwrite already uploaded files. This may harm 45 | consistency of stored data, cause please use this param with caution. By 46 | default: ``False`` 47 | :param decorator: 48 | In case of guarding upload views it might be useful to decorate them with 49 | given decorator function. By default: ``None`` (which means **ANY** client will 50 | able to upload files) 51 | :param on_upload_done: 52 | Coroutine to call after upload is done. Coroutine will receive three arguments: 53 | ``request``, ``resource`` & ``file_path``. Request is current 54 | :class:`aiohttp.web.Request` instance. Resource will contain all data about 55 | uploaded resource such as file name, file size 56 | (:class:`aiohttp_tus.data.Resource` instance). While file path will contain 57 | :class:`pathlib.Path` instance of uploaded file. 58 | :param json_dumps: 59 | To store resource metadata between chunk uploads ``aiohttp-tus`` using JSON 60 | files, stored into ``upload_path / ".metadata"`` directory. 61 | 62 | To dump the data builtin Python function used: :func:`json.dumps`, but you 63 | might customize things if interested in using ``ujson``, ``orjson``, 64 | ``rapidjson`` or other implementation. 65 | :param json_loads: 66 | Similarly to ``json_dumps``, but for loading data from JSON metadata files. 67 | By default: :func:`json.loads` 68 | """ 69 | 70 | def decorate(handler: Handler) -> Handler: 71 | if decorator is None: 72 | return handler 73 | return decorator(handler) 74 | 75 | # Ensure support of multiple tus upload URLs for one application 76 | app.setdefault(APP_TUS_CONFIG_KEY, {}) 77 | 78 | # Need to find out canonical dynamic resource URL if any and use it for storing 79 | # tus config into the app 80 | canonical_upload_url = web.DynamicResource(upload_url).canonical 81 | 82 | # Store tus config in application 83 | config = Config( 84 | upload_path=upload_path, 85 | upload_url=upload_url, 86 | upload_resource_name=upload_resource_name, 87 | allow_overwrite_files=allow_overwrite_files, 88 | on_upload_done=on_upload_done, 89 | json_dumps=json_dumps, 90 | json_loads=json_loads, 91 | ) 92 | set_config(app, canonical_upload_url, config) 93 | 94 | # Views for upload management 95 | upload_resource = app.router.add_resource( 96 | upload_url, name=config.resource_tus_upload_name 97 | ) 98 | upload_resource.add_route("OPTIONS", views.upload_options) 99 | upload_resource.add_route("GET", decorate(views.upload_details)) 100 | upload_resource.add_route("POST", decorate(views.start_upload)) 101 | 102 | # Views for resource management 103 | resource_resource = app.router.add_resource( 104 | get_resource_url(upload_url), name=config.resource_tus_resource_name 105 | ) 106 | resource_resource.add_route("HEAD", decorate(views.resource_details)) 107 | resource_resource.add_route("DELETE", decorate(views.delete_resource)) 108 | resource_resource.add_route("PATCH", decorate(views.upload_resource)) 109 | 110 | return app 111 | -------------------------------------------------------------------------------- /aiohttp_tus/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import attr 4 | from aiohttp import web 5 | 6 | from . import constants 7 | from .annotations import DictStrStr 8 | from .data import get_config, Resource 9 | from .utils import ( 10 | get_resource_or_404, 11 | get_resource_or_410, 12 | on_upload_done, 13 | parse_upload_metadata, 14 | ) 15 | from .validators import check_file_name, validate_upload_metadata 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | async def delete_resource(request: web.Request) -> web.Response: 22 | """Delete resource if user canceled the upload.""" 23 | # Ensure resource exists 24 | resource = get_resource_or_404(request) 25 | 26 | # Remove resource file and its metadata 27 | config = get_config(request) 28 | match_info = request.match_info 29 | resource.delete(config=config, match_info=match_info) 30 | resource.delete_metadata(config=config, match_info=match_info) 31 | 32 | return web.Response(status=204, headers=constants.BASE_HEADERS) 33 | 34 | 35 | async def resource_details(request: web.Request) -> web.Response: 36 | """Request resource offset if it is present.""" 37 | # Ensure resource exists 38 | resource = get_resource_or_404(request) 39 | return web.Response( 40 | status=200, 41 | text="", 42 | headers={ 43 | **constants.BASE_HEADERS, 44 | constants.HEADER_CACHE_CONTROL: "no-store", 45 | constants.HEADER_UPLOAD_OFFSET: str(resource.offset), 46 | constants.HEADER_UPLOAD_LENGTH: str(resource.file_size), 47 | }, 48 | ) 49 | 50 | 51 | async def start_upload(request: web.Request) -> web.Response: 52 | """Start uploading file with tus protocol.""" 53 | # Ensure Tus-Resumable header exists 54 | if not request.headers.get(constants.HEADER_TUS_RESUMABLE): 55 | logger.warning( 56 | "Wrong headers for start upload view", extra={"headers": request.headers} 57 | ) 58 | raise web.HTTPInternalServerError( 59 | text="Received file upload for unsupported file transfer protocol" 60 | ) 61 | 62 | config = get_config(request) 63 | headers = constants.BASE_HEADERS.copy() 64 | 65 | # Ensure upload metadata header is valid one 66 | metadata_header = request.headers.get(constants.HEADER_UPLOAD_METADATA) or "" 67 | valid_metadata = validate_upload_metadata(parse_upload_metadata(metadata_header)) 68 | file_name = check_file_name(valid_metadata, config=config) 69 | 70 | # If file name already exists in the storage - do not allow attempt to overwrite it 71 | if file_name and not config.allow_overwrite_files: 72 | raise web.HTTPConflict(headers=headers) 73 | 74 | if not file_name: 75 | file_name = valid_metadata["filename"].decode() 76 | 77 | # Prepare resource for the upload 78 | resource = Resource( 79 | file_name=file_name, 80 | file_size=int(request.headers.get(constants.HEADER_UPLOAD_LENGTH) or 0), 81 | offset=0, 82 | metadata_header=metadata_header, 83 | ) 84 | 85 | # Save resource and its metadata 86 | match_info = request.match_info 87 | try: 88 | resource.initial_save(config=config, match_info=match_info) 89 | resource.save_metadata(config=config, match_info=match_info) 90 | # In case if file system is not able to store given files - abort the upload 91 | except IOError: 92 | logger.error( 93 | "Unable to create file", 94 | exc_info=True, 95 | extra={ 96 | "file_name": file_name, 97 | "resource": attr.asdict(resource), 98 | "upload_path": config.upload_path.absolute(), 99 | }, 100 | ) 101 | raise web.HTTPInternalServerError( 102 | text="Unexpected error on uploading file", headers=headers 103 | ) 104 | 105 | # Specify resource headers for tus client 106 | headers[constants.HEADER_LOCATION] = str( 107 | request.url.join( 108 | request.app.router[config.resource_tus_resource_name].url_for( 109 | **request.match_info, resource_uid=resource.uid 110 | ) 111 | ) 112 | ) 113 | headers[constants.HEADER_TUS_TEMP_FILENAME] = resource.uid 114 | 115 | return web.Response(status=201, text="", headers=headers) 116 | 117 | 118 | async def upload_details(request: web.Request) -> web.Response: 119 | """Check whether requested filename already started to upload or not.""" 120 | valid_metadata = validate_upload_metadata(parse_upload_metadata(request.headers)) 121 | file_name = check_file_name(valid_metadata, config=get_config(request)) 122 | 123 | headers: DictStrStr = {} 124 | if file_name is not None: 125 | headers[constants.HEADER_TUS_FILE_EXISTS] = "true" 126 | headers[constants.HEADER_TUS_FILE_NAME] = file_name 127 | else: 128 | headers[constants.HEADER_TUS_FILE_EXISTS] = "false" 129 | 130 | return web.Response(status=200, text="", headers=headers) 131 | 132 | 133 | async def upload_options(request: web.Request) -> web.Response: 134 | """List tus protocol supported options.""" 135 | if not request.headers.get(constants.HEADER_TUS_RESUMABLE): 136 | return web.Response(status=200, text="") 137 | return web.Response( 138 | status=204, 139 | headers={ 140 | **constants.BASE_HEADERS, 141 | constants.HEADER_TUS_EXTENSION: ",".join(constants.TUS_API_EXTENSIONS), 142 | constants.HEADER_TUS_MAX_SIZE: str(constants.TUS_MAX_FILE_SIZE), 143 | }, 144 | ) 145 | 146 | 147 | async def upload_resource(request: web.Request) -> web.Response: 148 | """Upload resource chunk. 149 | 150 | Read resource metadata and save another chunk to the resource. If this is a final 151 | chunk, move resource to original file name and remove resource metadata. 152 | """ 153 | # Ensure resource metadata is readable and resource file exists as well 154 | resource = get_resource_or_410(request) 155 | 156 | # Ensure resource offset equals to expected upload offset 157 | upload_offset = int(request.headers.get(constants.HEADER_UPLOAD_OFFSET) or 0) 158 | if upload_offset != resource.offset: 159 | raise web.HTTPConflict(headers=constants.BASE_HEADERS) 160 | 161 | # Save current chunk to the resource 162 | config = get_config(request) 163 | match_info = request.match_info 164 | resource.save(config=config, match_info=match_info, chunk=await request.read()) 165 | 166 | # If this is a final chunk - complete upload 167 | chunk_size = int(request.headers.get(constants.HEADER_CONTENT_LENGTH) or 0) 168 | next_offset = resource.offset + chunk_size 169 | if next_offset == resource.file_size: 170 | file_path = resource.complete(config=config, match_info=match_info) 171 | await on_upload_done( 172 | request=request, config=config, resource=resource, file_path=file_path 173 | ) 174 | # But if it is not - store new metadata 175 | else: 176 | next_resource = attr.evolve(resource, offset=next_offset) 177 | next_resource.save_metadata(config=config, match_info=match_info) 178 | 179 | # Return upload headers 180 | return web.Response( 181 | status=204, 182 | headers={ 183 | **constants.BASE_HEADERS, 184 | constants.HEADER_TUS_TEMP_FILENAME: resource.uid, 185 | constants.HEADER_UPLOAD_OFFSET: str(next_offset), 186 | }, 187 | ) 188 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. include:: meta.rst 2 | 3 | ===== 4 | Usage 5 | ===== 6 | 7 | Default 8 | ======= 9 | 10 | To allow upload files to ``../uploads`` directory for all clients via ``/uploads`` URL, 11 | 12 | .. code-block:: python 13 | 14 | from pathlib import Path 15 | 16 | from aiohttp import web 17 | from aiohttp_tus import setup_tus 18 | 19 | 20 | app = setup_tus( 21 | web.Application(), 22 | upload_path=Path(__file__).parent.parent / "uploads", 23 | ) 24 | 25 | Understanding tus.io Chunk Size 26 | =============================== 27 | 28 | By default, `Uppy `_ and some other tus.io clients do not setup chunk 29 | size and tries to upload as large chunk, as possible. However as 30 | :class:`aiohttp.web.Application` setting up ``client_max_size`` on app initialization 31 | you might need to configure server to receive larger chunks as well as setup tus.io 32 | client to use respected chunk sizes. 33 | 34 | Examples below shown on how to config different parts to upload files with chunk size 35 | of **4MB** (``4_000_000`` bytes) 36 | 37 | aiohttp.web configuration 38 | ------------------------- 39 | 40 | .. code-block:: python 41 | 42 | from aiohttp import web 43 | from aiohttp_tus import setup_tus 44 | 45 | app = web.Application(client_max_size=4_000_000) 46 | 47 | nginx configuration 48 | ------------------- 49 | 50 | .. code-block:: nginx 51 | 52 | location ~ ^/uploads.*$ { 53 | client_max_body_size 4M; 54 | proxy_pass http://localhost:8080; 55 | } 56 | 57 | tus.py configuration 58 | -------------------- 59 | 60 | .. code-block:: bash 61 | 62 | tus-upload --chunk-size=4000000 \ 63 | /path/to/large-file http://localhost:8080/uploads 64 | 65 | uppy.io Configuration 66 | --------------------- 67 | 68 | .. code-block:: javascript 69 | 70 | uppy.use(Uppy.Tus, { 71 | endpoint: "http://localhost:8080/uploads", 72 | chunkSize: 3999999 73 | }) 74 | 75 | .. important:: 76 | To make `Uppy.Tus `_ plugin work you need to specify 77 | chunk size **at least 1 byte smaller** than ``client_max_size``. If you'll provide 78 | chunk size equals to client max size upload will not work properly. 79 | 80 | CORS Headers 81 | ============ 82 | 83 | At a moment (`May 17 2020`), ``aiohttp-tus`` supports setting up CORS Headers for 84 | ``aiohttp.web`` application only via `cors_middleware `_ 85 | from `aiohttp-middlewares`_ package. 86 | 87 | .. _aiohttp-middlewares: https://aiohttp-middlewares.readthedocs.io/ 88 | 89 | As ``aiohttp-tus`` registers `OPTIONS` handlers it doesn't work with 90 | `aiohttp-cors `_ library cause of known issue 91 | `aio-libs/aiohttp-cors#241 `_. 92 | (`Full discussion `_) 93 | 94 | To enable CORS Headers for your ``aiohttp.web`` application, which is using 95 | ``aiohttp-tus``, you need to, 96 | 97 | 1. Install `aiohttp-middlewares`_ 98 | 2. In your `app.py`, 99 | 100 | .. code-block:: python 101 | 102 | from pathlib import Path 103 | 104 | from aiohttp import web 105 | from aiohttp_middlewares import cors_middleware 106 | from aiohttp_tus import setup_tus 107 | 108 | 109 | # Allow CORS Headers for requests from http://localhost:3000 110 | app = web.Application( 111 | middlewares=( 112 | cors_middleware(origins=("http://localhost:3000",)), 113 | ) 114 | ) 115 | setup_tus( 116 | app, upload_path=Path(__file__).parent.parent / "uploads", 117 | ) 118 | 119 | Reverse proxy and HTTPS 120 | ======================= 121 | 122 | When aiohttp web application with ``aiohttp-tus`` deployed under the reverse proxy 123 | (such as nginx), with HTTPS support **you need to** setup 124 | `https_middleware `_ 125 | from `aiohttp-middlewares`_ package to ensure that :class:`aiohttp.web.Request` 126 | instance has proper schema. 127 | 128 | To use HTTPS middleware you need to, 129 | 130 | 1. Install `aiohttp-middlewares`_ 131 | 2. In `app.py`, 132 | 133 | .. code-block:: python 134 | 135 | from pathlib import Path 136 | 137 | from aiohttp import web 138 | from aiohttp_middlewares import https_middleware 139 | from aiohttp_tus import setup_tus 140 | 141 | app = web.Application(middlewares=(https_middleware(),)) 142 | setup_tus( 143 | app, upload_path=Path(__file__).parent.parent / "uploads" 144 | ) 145 | 146 | User Uploads 147 | ============ 148 | 149 | To allow upload files to ``/files/{username}`` directory only for authenticated users 150 | via ``/users/{username}/uploads`` URL, 151 | 152 | .. code-block:: python 153 | 154 | from aiohttp_tus.annotations import Handler 155 | 156 | 157 | def upload_user_required(handler: Handler) -> Handler: 158 | async def decorator(request: web.Request) -> web.Response: 159 | # Change ``is_user_authenticated`` call to actual call, 160 | # checking whether user authetnicated for given request 161 | # or not 162 | if not is_user_authenticated(request): 163 | raise web.HTTPForbidden() 164 | return await handler(request) 165 | 166 | return decorator 167 | 168 | 169 | app = setup_tus( 170 | web.Application(), 171 | upload_path=Path("/files") / r"{username}", 172 | upload_url=r"/users/{username}/uploads", 173 | decorator=upload_user_required, 174 | ) 175 | 176 | On Upload Done Callback 177 | ======================= 178 | 179 | There is a possibility to run any coroutine after upload is done. Example below, 180 | illustrates how to achieve that, 181 | 182 | .. code-block:: python 183 | 184 | from aiohttp_tus.data import Resource 185 | 186 | 187 | async def notify_on_upload( 188 | request: web.Request, resource: Resource, file_path: Path, 189 | ) -> None: 190 | redis = request.config_dict["redis"] 191 | await redis.rpush("uploaded_files", resource.file_name) 192 | 193 | 194 | app = setup_tus( 195 | web.Application(), 196 | upload_path=Path(__file__).parent.parent / "uploads", 197 | on_upload_done=notify_on_upload, 198 | ) 199 | 200 | Mutliple TUS upload URLs 201 | ======================== 202 | 203 | It is possible to setup multiple TUS upload URLs. Example below illustrates, how to 204 | achieve anonymous & authenticated uploads in same time for one 205 | :class:`aiohttp.web.Application` instance. 206 | 207 | .. code-block:: python 208 | 209 | app = web.Application() 210 | base_upload_path = Path(__file__).parent.parent / "uploads" 211 | 212 | # Anonymous users uploads 213 | setup_tus(app, upload_path=base_upload_path / "anonymous") 214 | 215 | # Authenticated users uploads 216 | setup_tus( 217 | app, 218 | upload_path=base_upload_path / r"{username}", 219 | upload_url=r"/users/{username}/uploads", 220 | decorator=upload_user_required, 221 | ) 222 | 223 | Upload resource name 224 | ==================== 225 | 226 | In most cases there is no need to specify :class:`aiohttp.web.Resource` name for upload 227 | resource, but when it is necessary, it is possible to specify custom 228 | ``upload_resource_name`` and lately use it for URL reversing. 229 | 230 | Example below illustrates how to achieve it, 231 | 232 | In ``app.py``, 233 | 234 | .. code-block:: python 235 | 236 | setup_tus( 237 | web.Application(), 238 | upload_path=( 239 | Path(__file__).parent.parent / "uploads" / r"{username}" 240 | ), 241 | upload_url="/user/{username}/uploads", 242 | upload_resource_name="user_upload", 243 | ) 244 | 245 | In ``views.py``, 246 | 247 | .. code-block:: python 248 | 249 | async def user_profile(request: web.Request) -> web.Response: 250 | upload_url = request.app.router["uploads"].url_for( 251 | username=request.match_info["username"] 252 | ) 253 | return aiohttp_jinja2.render( 254 | "users/profile.html", 255 | request, 256 | {"upload_url": upload_url}, 257 | ) 258 | -------------------------------------------------------------------------------- /aiohttp_tus/data.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import shutil 4 | import uuid 5 | from contextlib import suppress 6 | from pathlib import Path 7 | from typing import Awaitable, Callable, Optional, Tuple 8 | 9 | import attr 10 | from aiohttp import web 11 | 12 | from .annotations import DictStrAny, JsonDumps, JsonLoads 13 | from .constants import APP_TUS_CONFIG_KEY 14 | 15 | 16 | @attr.dataclass(frozen=True, slots=True) 17 | class Config: 18 | upload_path: Path 19 | upload_url: str 20 | 21 | upload_resource_name: Optional[str] = None 22 | 23 | allow_overwrite_files: bool = False 24 | on_upload_done: Optional["ResourceCallback"] = None 25 | 26 | mkdir_mode: int = 0o755 27 | 28 | json_dumps: JsonDumps = json.dumps 29 | json_loads: JsonLoads = json.loads 30 | 31 | def resolve_metadata_path(self, match_info: web.UrlMappingMatchInfo) -> Path: 32 | metadata_path = self.resolve_upload_path(match_info) / ".metadata" 33 | metadata_path.mkdir(mode=self.mkdir_mode, parents=True, exist_ok=True) 34 | return metadata_path 35 | 36 | def resolve_resources_path(self, match_info: web.UrlMappingMatchInfo) -> Path: 37 | resources_path = self.resolve_upload_path(match_info) / ".resources" 38 | resources_path.mkdir(mode=self.mkdir_mode, parents=True, exist_ok=True) 39 | return resources_path 40 | 41 | def resolve_upload_path(self, match_info: web.UrlMappingMatchInfo) -> Path: 42 | return Path(str(self.upload_path.absolute()).format(**match_info)) 43 | 44 | @property 45 | def resource_tus_resource_name(self) -> str: 46 | return ( 47 | f"tus_resource_{self.upload_url_id}" 48 | if self.upload_resource_name is None 49 | else f"{self.upload_resource_name}_resource" 50 | ) 51 | 52 | @property 53 | def resource_tus_upload_name(self) -> str: 54 | return ( 55 | f"tus_upload_{self.upload_url_id}" 56 | if self.upload_resource_name is None 57 | else self.upload_resource_name 58 | ) 59 | 60 | @property 61 | def upload_url_id(self) -> str: 62 | return ( 63 | base64.urlsafe_b64encode(self.upload_url.encode("utf-8")) 64 | .decode("utf-8") 65 | .replace("=", "_") 66 | ) 67 | 68 | 69 | @attr.dataclass(frozen=True, slots=True) 70 | class Resource: 71 | """Dataclass to store resource metadata. 72 | 73 | Given dataclass used internally in between resource chunk uploads and is passed 74 | to ``on_upload_done`` callback if one is defined at :func:`aiohttp_tus.setup_tus` 75 | call. 76 | 77 | :param uid: Resource UUID. By default: ``str(uuid.uuid4())`` 78 | :param file_name: Resource file name. 79 | :param file_size: Resource file size. 80 | :param offset: Current resource offset. 81 | :param metadata_header: Metadata header sent on initiating resource upload. 82 | """ 83 | 84 | file_name: str 85 | file_size: int 86 | offset: int 87 | metadata_header: str 88 | 89 | uid: str = attr.Factory(lambda: str(uuid.uuid4())) 90 | 91 | def complete(self, *, config: Config, match_info: web.UrlMappingMatchInfo) -> Path: 92 | resource_path = get_resource_path( 93 | config=config, match_info=match_info, uid=self.uid 94 | ) 95 | file_path = get_file_path( 96 | config=config, match_info=match_info, file_name=self.file_name 97 | ) 98 | 99 | # Python 3.5-3.8 requires to have source as string. 100 | # More details: https://bugs.python.org/issue32689 101 | shutil.move(str(resource_path), file_path) 102 | self.delete_metadata(config=config, match_info=match_info) 103 | 104 | return file_path 105 | 106 | def delete(self, *, config: Config, match_info: web.UrlMappingMatchInfo) -> bool: 107 | return delete_path( 108 | get_resource_path(config=config, match_info=match_info, uid=self.uid) 109 | ) 110 | 111 | def delete_metadata( 112 | self, *, config: Config, match_info: web.UrlMappingMatchInfo 113 | ) -> int: 114 | return delete_path( 115 | get_resource_metadata_path( 116 | config=config, match_info=match_info, uid=self.uid 117 | ) 118 | ) 119 | 120 | @classmethod 121 | def from_metadata( 122 | cls, *, config: Config, match_info: web.UrlMappingMatchInfo 123 | ) -> "Resource": 124 | uid = match_info["resource_uid"] 125 | path = get_resource_metadata_path(config=config, match_info=match_info, uid=uid) 126 | data = config.json_loads(path.read_text()) 127 | return cls( 128 | uid=data["uid"], 129 | file_name=data["file_name"], 130 | file_size=data["file_size"], 131 | offset=data["offset"], 132 | metadata_header=data["metadata_header"], 133 | ) 134 | 135 | def initial_save( 136 | self, *, config: Config, match_info: web.UrlMappingMatchInfo 137 | ) -> Tuple[Path, int]: 138 | return self.save( 139 | config=config, 140 | match_info=match_info, 141 | chunk=b"\0", 142 | mode="wb", 143 | offset=self.file_size - 1 if self.file_size > 0 else 0, 144 | ) 145 | 146 | def save( 147 | self, 148 | *, 149 | config: Config, 150 | match_info: web.UrlMappingMatchInfo, 151 | chunk: bytes, 152 | mode: str = None, 153 | offset: int = None, 154 | ) -> Tuple[Path, int]: 155 | path = get_resource_path(config=config, match_info=match_info, uid=self.uid) 156 | with open(path, mode if mode is not None else "r+b") as handler: 157 | handler.seek(offset if offset is not None else self.offset) 158 | chunk_size = handler.write(chunk) 159 | return (path, chunk_size) 160 | 161 | def save_metadata( 162 | self, *, config: Config, match_info: web.UrlMappingMatchInfo 163 | ) -> Tuple[Path, DictStrAny]: 164 | path = get_resource_metadata_path( 165 | config=config, match_info=match_info, uid=self.uid 166 | ) 167 | 168 | data = attr.asdict(self) 169 | path.write_text(config.json_dumps(data)) 170 | 171 | return (path, data) 172 | 173 | 174 | ResourceCallback = Callable[[web.Request, Resource, Path], Awaitable[None]] 175 | 176 | 177 | def delete_path(path: Path) -> bool: 178 | if path.exists(): 179 | path.unlink() 180 | return True 181 | return False 182 | 183 | 184 | def get_config(request: web.Request) -> Config: 185 | route = request.match_info.route 186 | 187 | container = request.config_dict[APP_TUS_CONFIG_KEY] 188 | info = route.get_info() 189 | 190 | config_key = info.get("formatter") or info["path"] 191 | if config_key.endswith(r"/{resource_uid}"): 192 | config_key = get_upload_url(config_key) 193 | 194 | try: 195 | with suppress(KeyError): 196 | return container[config_key] # type: ignore 197 | return container[f"{config_key}/"] # type: ignore 198 | except KeyError: 199 | raise KeyError("Unable to find aiohttp_tus config for specified URL") 200 | 201 | 202 | def get_file_path( 203 | *, config: Config, match_info: web.UrlMappingMatchInfo, file_name: str 204 | ) -> Path: 205 | return config.resolve_upload_path(match_info) / file_name 206 | 207 | 208 | def get_resource_path( 209 | *, config: Config, match_info: web.UrlMappingMatchInfo, uid: str 210 | ) -> Path: 211 | return config.resolve_resources_path(match_info) / uid 212 | 213 | 214 | def get_resource_metadata_path( 215 | *, config: Config, match_info: web.UrlMappingMatchInfo, uid: str 216 | ) -> Path: 217 | return config.resolve_metadata_path(match_info) / f"{uid}.json" 218 | 219 | 220 | def get_resource_url(upload_url: str) -> str: 221 | return "/".join((upload_url.rstrip("/"), r"{resource_uid}")) 222 | 223 | 224 | def get_upload_url(resource_url: str) -> str: 225 | return resource_url.rsplit("/", 1)[0] 226 | 227 | 228 | def set_config(app: web.Application, upload_url: str, config: Config) -> None: 229 | if upload_url in app[APP_TUS_CONFIG_KEY]: 230 | raise ValueError( 231 | f"Upload URL {upload_url!r} already registered for the application. " 232 | "Please pass other `upload_url` keyword argument in `setup_tus` function." 233 | ) 234 | app[APP_TUS_CONFIG_KEY][upload_url] = config 235 | -------------------------------------------------------------------------------- /tests/test_tus.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from functools import partial 3 | 4 | try: 5 | from contextlib import asynccontextmanager 6 | except ImportError: 7 | from async_generator import asynccontextmanager 8 | 9 | import pytest 10 | import tus 11 | from aiohttp import hdrs, web 12 | from aiohttp.test_utils import TestClient 13 | 14 | from aiohttp_tus import setup_tus 15 | from aiohttp_tus.annotations import Decorator, Handler 16 | from aiohttp_tus.constants import APP_TUS_CONFIG_KEY 17 | from aiohttp_tus.data import Config, ResourceCallback 18 | from tests.common import ( 19 | get_upload_url, 20 | TEST_CHUNK_SIZE, 21 | TEST_FILE_NAME, 22 | TEST_FILE_PATH, 23 | TEST_SCREENSHOT_NAME, 24 | TEST_SCREENSHOT_PATH, 25 | TEST_UPLOAD_URL, 26 | ) 27 | 28 | 29 | SECRET_TOKEN = "secret-token" 30 | 31 | 32 | @pytest.fixture 33 | def aiohttp_test_client(tmp_path, aiohttp_client): 34 | @asynccontextmanager 35 | async def factory( 36 | *, 37 | upload_url: str, 38 | upload_resource_name: str = None, 39 | upload_suffix: str = None, 40 | allow_overwrite_files: bool = False, 41 | on_upload_done: ResourceCallback = None, 42 | decorator: Decorator = None, 43 | ) -> TestClient: 44 | upload_path = tmp_path / "aiohttp_tus" 45 | app = setup_tus( 46 | web.Application(), 47 | upload_path=upload_path / upload_suffix if upload_suffix else upload_path, 48 | upload_url=upload_url, 49 | upload_resource_name=upload_resource_name, 50 | allow_overwrite_files=allow_overwrite_files, 51 | on_upload_done=on_upload_done, 52 | decorator=decorator, 53 | ) 54 | try: 55 | yield await aiohttp_client(app) 56 | finally: 57 | shutil.rmtree(upload_path, ignore_errors=True) 58 | 59 | return factory 60 | 61 | 62 | def login_required(handler: Handler) -> Handler: 63 | async def decorator(request: web.Request) -> web.StreamResponse: 64 | header = request.headers.get(hdrs.AUTHORIZATION) 65 | if header is None or header != f"Token {SECRET_TOKEN}": 66 | raise web.HTTPForbidden() 67 | return await handler(request) 68 | 69 | return decorator 70 | 71 | 72 | async def test_decorated_upload_200(aiohttp_test_client, loop): 73 | upload = partial( 74 | tus.upload, 75 | file_name=TEST_FILE_NAME, 76 | headers={"Authorization": "Token secret-token"}, 77 | ) 78 | 79 | async with aiohttp_test_client( 80 | upload_url=TEST_UPLOAD_URL, decorator=login_required 81 | ) as client: 82 | with open(TEST_FILE_PATH, "rb") as handler: 83 | await loop.run_in_executor( 84 | None, upload, handler, get_upload_url(client, TEST_UPLOAD_URL) 85 | ) 86 | 87 | 88 | async def test_decorated_upload_403(aiohttp_test_client, loop): 89 | upload = partial( 90 | tus.upload, 91 | file_name=TEST_FILE_NAME, 92 | headers={"Authorization": "Token not-secret-token"}, 93 | ) 94 | 95 | async with aiohttp_test_client( 96 | upload_url=TEST_UPLOAD_URL, decorator=login_required 97 | ) as client: 98 | with open(TEST_FILE_PATH, "rb") as handler: 99 | with pytest.raises(tus.TusError): 100 | await loop.run_in_executor( 101 | None, upload, handler, get_upload_url(client, TEST_UPLOAD_URL) 102 | ) 103 | 104 | 105 | async def test_on_upload_callback(aiohttp_test_client, loop): 106 | data = {} 107 | upload = partial(tus.upload, file_name=TEST_FILE_NAME) 108 | 109 | async def on_upload_done(request, resource, file_path): 110 | data[resource.file_name] = file_path 111 | 112 | async with aiohttp_test_client( 113 | upload_url=TEST_UPLOAD_URL, on_upload_done=on_upload_done 114 | ) as client: 115 | with open(TEST_FILE_PATH, "rb") as handler: 116 | await loop.run_in_executor( 117 | None, upload, handler, get_upload_url(client, TEST_UPLOAD_URL) 118 | ) 119 | 120 | assert TEST_FILE_NAME in data 121 | 122 | 123 | async def test_overwrite_file_allowed(aiohttp_test_client, loop): 124 | upload = partial(tus.upload, file_name=TEST_FILE_NAME) 125 | 126 | async with aiohttp_test_client( 127 | upload_url=TEST_UPLOAD_URL, allow_overwrite_files=True 128 | ) as client: 129 | tus_upload_url = get_upload_url(client, TEST_UPLOAD_URL) 130 | 131 | with open(TEST_FILE_PATH, "rb") as handler: 132 | await loop.run_in_executor(None, upload, handler, tus_upload_url) 133 | 134 | with open(TEST_FILE_PATH, "rb") as handler: 135 | await loop.run_in_executor(None, upload, handler, tus_upload_url) 136 | 137 | 138 | async def test_overwrite_file_disallowed(aiohttp_test_client, loop): 139 | upload = partial(tus.upload, file_name=TEST_FILE_NAME) 140 | 141 | async with aiohttp_test_client( 142 | upload_url=TEST_UPLOAD_URL, allow_overwrite_files=False 143 | ) as client: 144 | tus_upload_url = get_upload_url(client, TEST_UPLOAD_URL) 145 | 146 | with open(TEST_FILE_PATH, "rb") as handler: 147 | await loop.run_in_executor(None, upload, handler, tus_upload_url) 148 | 149 | with pytest.raises(tus.TusError): 150 | with open(TEST_FILE_PATH, "rb") as handler: 151 | await loop.run_in_executor(None, upload, handler, tus_upload_url) 152 | 153 | 154 | @pytest.mark.parametrize( 155 | "upload_url, canonical_upload_url, upload_suffix, tus_upload_url, match_info", 156 | ( 157 | (TEST_UPLOAD_URL, TEST_UPLOAD_URL, None, TEST_UPLOAD_URL, {}), 158 | (f"{TEST_UPLOAD_URL}/", f"{TEST_UPLOAD_URL}/", None, f"{TEST_UPLOAD_URL}/", {}), 159 | ( 160 | r"/user/{username}/uploads", 161 | r"/user/{username}/uploads", 162 | None, 163 | "/user/playpauseanddtop/uploads", 164 | {}, 165 | ), 166 | ( 167 | r"/user/{username}/uploads/", 168 | r"/user/{username}/uploads/", 169 | None, 170 | "/user/playpauseanddtop/uploads/", 171 | {}, 172 | ), 173 | ( 174 | r"/user/{username:([a-zA-Z0-9_-])+}/uploads", 175 | r"/user/{username}/uploads", 176 | None, 177 | "/user/playpauseanddtop/uploads", 178 | {}, 179 | ), 180 | ( 181 | r"/user/{username:([a-zA-Z0-9_-])+}/uploads/", 182 | r"/user/{username}/uploads/", 183 | None, 184 | "/user/playpauseanddtop/uploads/", 185 | {}, 186 | ), 187 | ( 188 | r"/user/{username}/uploads", 189 | r"/user/{username}/uploads", 190 | r"{username}", 191 | "/user/playpauseandstop/uploads", 192 | {"username": "playpauseandstop"}, 193 | ), 194 | ( 195 | r"/user/{username}/uploads/", 196 | r"/user/{username}/uploads/", 197 | r"{username}", 198 | "/user/playpauseandstop/uploads/", 199 | {"username": "playpauseandstop"}, 200 | ), 201 | ), 202 | ) 203 | async def test_upload( 204 | aiohttp_test_client, 205 | loop, 206 | upload_url, 207 | canonical_upload_url, 208 | upload_suffix, 209 | tus_upload_url, 210 | match_info, 211 | ): 212 | upload = partial(tus.upload, file_name=TEST_FILE_NAME) 213 | 214 | async with aiohttp_test_client( 215 | upload_url=upload_url, upload_suffix=upload_suffix 216 | ) as client: 217 | with open(TEST_FILE_PATH, "rb") as handler: 218 | await loop.run_in_executor( 219 | None, upload, handler, get_upload_url(client, tus_upload_url) 220 | ) 221 | 222 | config: Config = client.app[APP_TUS_CONFIG_KEY][canonical_upload_url] 223 | expected_upload_path = config.resolve_upload_path(match_info) / TEST_FILE_NAME 224 | assert expected_upload_path.exists() 225 | assert expected_upload_path.read_bytes() == TEST_FILE_PATH.read_bytes() 226 | 227 | 228 | @pytest.mark.parametrize( 229 | "chunk_size", (TEST_CHUNK_SIZE, TEST_CHUNK_SIZE * 2, TEST_CHUNK_SIZE * 4) 230 | ) 231 | async def test_upload_large_file(aiohttp_test_client, loop, chunk_size): 232 | upload = partial(tus.upload, file_name=TEST_SCREENSHOT_NAME, chunk_size=chunk_size) 233 | 234 | async with aiohttp_test_client(upload_url=TEST_UPLOAD_URL) as client: 235 | with open(TEST_SCREENSHOT_PATH, "rb") as handler: 236 | await loop.run_in_executor( 237 | None, upload, handler, get_upload_url(client, TEST_UPLOAD_URL) 238 | ) 239 | 240 | config: Config = client.app[APP_TUS_CONFIG_KEY]["/uploads"] 241 | expected_upload_path = config.resolve_upload_path({}) / TEST_SCREENSHOT_NAME 242 | assert expected_upload_path.exists() 243 | assert expected_upload_path.read_bytes() == TEST_SCREENSHOT_PATH.read_bytes() 244 | 245 | 246 | async def test_upload_resource_name(aiohttp_test_client, loop): 247 | upload = partial(tus.upload, file_name=TEST_FILE_NAME) 248 | 249 | async with aiohttp_test_client( 250 | upload_url=TEST_UPLOAD_URL, upload_resource_name="upload" 251 | ) as client: 252 | upload_url = client.app.router["upload"].url_for() 253 | with open(TEST_FILE_PATH, "rb") as handler: 254 | await loop.run_in_executor( 255 | None, upload, handler, get_upload_url(client, upload_url) 256 | ) 257 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiohttp" 3 | version = "3.8.1" 4 | description = "Async http client/server framework (asyncio)" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | aiosignal = ">=1.1.2" 11 | async-timeout = ">=4.0.0a3,<5.0" 12 | attrs = ">=17.3.0" 13 | charset-normalizer = ">=2.0,<3.0" 14 | frozenlist = ">=1.1.1" 15 | multidict = ">=4.5,<7.0" 16 | yarl = ">=1.0,<2.0" 17 | 18 | [package.extras] 19 | speedups = ["aiodns", "brotli", "cchardet"] 20 | 21 | [[package]] 22 | name = "aiohttp-jinja2" 23 | version = "1.5" 24 | description = "jinja2 template renderer for aiohttp.web (http server for asyncio)" 25 | category = "dev" 26 | optional = false 27 | python-versions = ">=3.6" 28 | 29 | [package.dependencies] 30 | aiohttp = ">=3.6.3" 31 | jinja2 = ">=3.0.0" 32 | 33 | [[package]] 34 | name = "aiosignal" 35 | version = "1.2.0" 36 | description = "aiosignal: a list of registered asynchronous callbacks" 37 | category = "main" 38 | optional = false 39 | python-versions = ">=3.6" 40 | 41 | [package.dependencies] 42 | frozenlist = ">=1.1.0" 43 | 44 | [[package]] 45 | name = "argh" 46 | version = "0.26.2" 47 | description = "An unobtrusive argparse wrapper with natural syntax" 48 | category = "dev" 49 | optional = false 50 | python-versions = "*" 51 | 52 | [[package]] 53 | name = "async-timeout" 54 | version = "4.0.2" 55 | description = "Timeout context manager for asyncio programs" 56 | category = "main" 57 | optional = false 58 | python-versions = ">=3.6" 59 | 60 | [[package]] 61 | name = "atomicwrites" 62 | version = "1.4.0" 63 | description = "Atomic file writes." 64 | category = "dev" 65 | optional = false 66 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 67 | 68 | [[package]] 69 | name = "attrs" 70 | version = "21.4.0" 71 | description = "Classes Without Boilerplate" 72 | category = "main" 73 | optional = false 74 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 75 | 76 | [package.extras] 77 | 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", "cloudpickle"] 78 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 79 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 80 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 81 | 82 | [[package]] 83 | name = "certifi" 84 | version = "2021.10.8" 85 | description = "Python package for providing Mozilla's CA Bundle." 86 | category = "dev" 87 | optional = false 88 | python-versions = "*" 89 | 90 | [[package]] 91 | name = "charset-normalizer" 92 | version = "2.0.9" 93 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 94 | category = "main" 95 | optional = false 96 | python-versions = ">=3.5.0" 97 | 98 | [package.extras] 99 | unicode_backport = ["unicodedata2"] 100 | 101 | [[package]] 102 | name = "colorama" 103 | version = "0.4.4" 104 | description = "Cross-platform colored terminal text." 105 | category = "dev" 106 | optional = false 107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 108 | 109 | [[package]] 110 | name = "coverage" 111 | version = "6.2" 112 | description = "Code coverage measurement for Python" 113 | category = "dev" 114 | optional = false 115 | python-versions = ">=3.6" 116 | 117 | [package.dependencies] 118 | tomli = {version = "*", optional = true, markers = "extra == \"toml\""} 119 | 120 | [package.extras] 121 | toml = ["tomli"] 122 | 123 | [[package]] 124 | name = "frozenlist" 125 | version = "1.2.0" 126 | description = "A list-like structure which implements collections.abc.MutableSequence" 127 | category = "main" 128 | optional = false 129 | python-versions = ">=3.6" 130 | 131 | [[package]] 132 | name = "humanfriendly" 133 | version = "10.0" 134 | description = "Human friendly output for text interfaces using Python" 135 | category = "dev" 136 | optional = false 137 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 138 | 139 | [package.dependencies] 140 | pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} 141 | 142 | [[package]] 143 | name = "idna" 144 | version = "3.3" 145 | description = "Internationalized Domain Names in Applications (IDNA)" 146 | category = "main" 147 | optional = false 148 | python-versions = ">=3.5" 149 | 150 | [[package]] 151 | name = "iniconfig" 152 | version = "1.1.1" 153 | description = "iniconfig: brain-dead simple config-ini parsing" 154 | category = "dev" 155 | optional = false 156 | python-versions = "*" 157 | 158 | [[package]] 159 | name = "jinja2" 160 | version = "3.0.3" 161 | description = "A very fast and expressive template engine." 162 | category = "dev" 163 | optional = false 164 | python-versions = ">=3.6" 165 | 166 | [package.dependencies] 167 | MarkupSafe = ">=2.0" 168 | 169 | [package.extras] 170 | i18n = ["Babel (>=2.7)"] 171 | 172 | [[package]] 173 | name = "livereload" 174 | version = "2.6.3" 175 | description = "Python LiveReload is an awesome tool for web developers" 176 | category = "dev" 177 | optional = false 178 | python-versions = "*" 179 | 180 | [package.dependencies] 181 | six = "*" 182 | tornado = {version = "*", markers = "python_version > \"2.7\""} 183 | 184 | [[package]] 185 | name = "markupsafe" 186 | version = "2.0.1" 187 | description = "Safely add untrusted strings to HTML/XML markup." 188 | category = "dev" 189 | optional = false 190 | python-versions = ">=3.6" 191 | 192 | [[package]] 193 | name = "multidict" 194 | version = "5.2.0" 195 | description = "multidict implementation" 196 | category = "main" 197 | optional = false 198 | python-versions = ">=3.6" 199 | 200 | [[package]] 201 | name = "packaging" 202 | version = "21.3" 203 | description = "Core utilities for Python packages" 204 | category = "dev" 205 | optional = false 206 | python-versions = ">=3.6" 207 | 208 | [package.dependencies] 209 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 210 | 211 | [[package]] 212 | name = "pathtools" 213 | version = "0.1.2" 214 | description = "File system general utilities" 215 | category = "dev" 216 | optional = false 217 | python-versions = "*" 218 | 219 | [[package]] 220 | name = "pluggy" 221 | version = "1.0.0" 222 | description = "plugin and hook calling mechanisms for python" 223 | category = "dev" 224 | optional = false 225 | python-versions = ">=3.6" 226 | 227 | [package.extras] 228 | dev = ["pre-commit", "tox"] 229 | testing = ["pytest", "pytest-benchmark"] 230 | 231 | [[package]] 232 | name = "port-for" 233 | version = "0.3.1" 234 | description = "Utility that helps with local TCP ports managment. It can find an unused TCP localhost port and remember the association." 235 | category = "dev" 236 | optional = false 237 | python-versions = "*" 238 | 239 | [[package]] 240 | name = "py" 241 | version = "1.11.0" 242 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 243 | category = "dev" 244 | optional = false 245 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 246 | 247 | [[package]] 248 | name = "pyparsing" 249 | version = "3.0.6" 250 | description = "Python parsing module" 251 | category = "dev" 252 | optional = false 253 | python-versions = ">=3.6" 254 | 255 | [package.extras] 256 | diagrams = ["jinja2", "railroad-diagrams"] 257 | 258 | [[package]] 259 | name = "pyreadline3" 260 | version = "3.3" 261 | description = "A python implementation of GNU readline." 262 | category = "dev" 263 | optional = false 264 | python-versions = "*" 265 | 266 | [[package]] 267 | name = "pytest" 268 | version = "6.2.5" 269 | description = "pytest: simple powerful testing with Python" 270 | category = "dev" 271 | optional = false 272 | python-versions = ">=3.6" 273 | 274 | [package.dependencies] 275 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 276 | attrs = ">=19.2.0" 277 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 278 | iniconfig = "*" 279 | packaging = "*" 280 | pluggy = ">=0.12,<2.0" 281 | py = ">=1.8.2" 282 | toml = "*" 283 | 284 | [package.extras] 285 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 286 | 287 | [[package]] 288 | name = "pytest-aiohttp" 289 | version = "0.3.0" 290 | description = "pytest plugin for aiohttp support" 291 | category = "dev" 292 | optional = false 293 | python-versions = "*" 294 | 295 | [package.dependencies] 296 | aiohttp = ">=2.3.5" 297 | pytest = "*" 298 | 299 | [[package]] 300 | name = "pytest-asyncio" 301 | version = "0.16.0" 302 | description = "Pytest support for asyncio." 303 | category = "dev" 304 | optional = false 305 | python-versions = ">= 3.6" 306 | 307 | [package.dependencies] 308 | pytest = ">=5.4.0" 309 | 310 | [package.extras] 311 | testing = ["coverage", "hypothesis (>=5.7.1)"] 312 | 313 | [[package]] 314 | name = "pytest-cov" 315 | version = "3.0.0" 316 | description = "Pytest plugin for measuring coverage." 317 | category = "dev" 318 | optional = false 319 | python-versions = ">=3.6" 320 | 321 | [package.dependencies] 322 | coverage = {version = ">=5.2.1", extras = ["toml"]} 323 | pytest = ">=4.6" 324 | 325 | [package.extras] 326 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 327 | 328 | [[package]] 329 | name = "pyyaml" 330 | version = "6.0" 331 | description = "YAML parser and emitter for Python" 332 | category = "dev" 333 | optional = false 334 | python-versions = ">=3.6" 335 | 336 | [[package]] 337 | name = "requests" 338 | version = "2.27.0" 339 | description = "Python HTTP for Humans." 340 | category = "dev" 341 | optional = false 342 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 343 | 344 | [package.dependencies] 345 | certifi = ">=2017.4.17" 346 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 347 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 348 | urllib3 = ">=1.21.1,<1.27" 349 | 350 | [package.extras] 351 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 352 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 353 | 354 | [[package]] 355 | name = "six" 356 | version = "1.16.0" 357 | description = "Python 2 and 3 compatibility utilities" 358 | category = "dev" 359 | optional = false 360 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 361 | 362 | [[package]] 363 | name = "sphinx-autobuild" 364 | version = "0.7.1" 365 | description = "Watch a Sphinx directory and rebuild the documentation when a change is detected. Also includes a livereload enabled web server." 366 | category = "dev" 367 | optional = false 368 | python-versions = "*" 369 | 370 | [package.dependencies] 371 | argh = ">=0.24.1" 372 | livereload = ">=2.3.0" 373 | pathtools = ">=0.1.2" 374 | port-for = "0.3.1" 375 | PyYAML = ">=3.10" 376 | tornado = ">=3.2" 377 | watchdog = ">=0.7.1" 378 | 379 | [[package]] 380 | name = "toml" 381 | version = "0.10.2" 382 | description = "Python Library for Tom's Obvious, Minimal Language" 383 | category = "dev" 384 | optional = false 385 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 386 | 387 | [[package]] 388 | name = "tomli" 389 | version = "2.0.0" 390 | description = "A lil' TOML parser" 391 | category = "dev" 392 | optional = false 393 | python-versions = ">=3.7" 394 | 395 | [[package]] 396 | name = "tornado" 397 | version = "6.1" 398 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 399 | category = "dev" 400 | optional = false 401 | python-versions = ">= 3.5" 402 | 403 | [[package]] 404 | name = "tus.py" 405 | version = "1.3.4" 406 | description = "tus (resumable file upload protocol) client" 407 | category = "dev" 408 | optional = false 409 | python-versions = "*" 410 | 411 | [package.dependencies] 412 | requests = "*" 413 | 414 | [[package]] 415 | name = "urllib3" 416 | version = "1.26.7" 417 | description = "HTTP library with thread-safe connection pooling, file post, and more." 418 | category = "dev" 419 | optional = false 420 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 421 | 422 | [package.extras] 423 | brotli = ["brotlipy (>=0.6.0)"] 424 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 425 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 426 | 427 | [[package]] 428 | name = "watchdog" 429 | version = "2.1.6" 430 | description = "Filesystem events monitoring" 431 | category = "dev" 432 | optional = false 433 | python-versions = ">=3.6" 434 | 435 | [package.extras] 436 | watchmedo = ["PyYAML (>=3.10)"] 437 | 438 | [[package]] 439 | name = "yarl" 440 | version = "1.7.2" 441 | description = "Yet another URL library" 442 | category = "main" 443 | optional = false 444 | python-versions = ">=3.6" 445 | 446 | [package.dependencies] 447 | idna = ">=2.0" 448 | multidict = ">=4.0" 449 | 450 | [metadata] 451 | lock-version = "1.1" 452 | python-versions = "^3.10" 453 | content-hash = "3d99f20edce0e6eb5dca7ca71c67d632f735cf3bc82a4a162410880513502609" 454 | 455 | [metadata.files] 456 | aiohttp = [ 457 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, 458 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, 459 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, 460 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, 461 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, 462 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, 463 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, 464 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, 465 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, 466 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, 467 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, 468 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, 469 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, 470 | {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, 471 | {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, 472 | {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, 473 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, 474 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, 475 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, 476 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, 477 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, 478 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, 479 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, 480 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, 481 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, 482 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, 483 | {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, 484 | {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, 485 | {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, 486 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, 487 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, 488 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, 489 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, 490 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, 491 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, 492 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, 493 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, 494 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, 495 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, 496 | {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, 497 | {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, 498 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, 499 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, 500 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, 501 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, 502 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, 503 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, 504 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, 505 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, 506 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, 507 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, 508 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, 509 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, 510 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, 511 | {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, 512 | {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, 513 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, 514 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, 515 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, 516 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, 517 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, 518 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, 519 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, 520 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, 521 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, 522 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, 523 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, 524 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, 525 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, 526 | {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, 527 | {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, 528 | {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, 529 | ] 530 | aiohttp-jinja2 = [ 531 | {file = "aiohttp-jinja2-1.5.tar.gz", hash = "sha256:7c3ba5eac060b691f4e50534af2d79fca2a75712ebd2b25e6fcb1295859f910b"}, 532 | {file = "aiohttp_jinja2-1.5-py3-none-any.whl", hash = "sha256:b55c0ed167b0cc4b6d6a50fb2299a44beb5dc4aec9df21305b91a5484694cf74"}, 533 | ] 534 | aiosignal = [ 535 | {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, 536 | {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, 537 | ] 538 | argh = [ 539 | {file = "argh-0.26.2-py2.py3-none-any.whl", hash = "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3"}, 540 | {file = "argh-0.26.2.tar.gz", hash = "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65"}, 541 | ] 542 | async-timeout = [ 543 | {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, 544 | {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, 545 | ] 546 | atomicwrites = [ 547 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 548 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 549 | ] 550 | attrs = [ 551 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 552 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 553 | ] 554 | certifi = [ 555 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 556 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 557 | ] 558 | charset-normalizer = [ 559 | {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, 560 | {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, 561 | ] 562 | colorama = [ 563 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 564 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 565 | ] 566 | coverage = [ 567 | {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, 568 | {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, 569 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, 570 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, 571 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, 572 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, 573 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, 574 | {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, 575 | {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, 576 | {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, 577 | {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, 578 | {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, 579 | {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, 580 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, 581 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, 582 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, 583 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, 584 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, 585 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, 586 | {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, 587 | {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, 588 | {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, 589 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, 590 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, 591 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, 592 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, 593 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, 594 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, 595 | {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, 596 | {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, 597 | {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, 598 | {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, 599 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, 600 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, 601 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, 602 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, 603 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, 604 | {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, 605 | {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, 606 | {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, 607 | {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, 608 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, 609 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, 610 | {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, 611 | {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, 612 | {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, 613 | {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, 614 | ] 615 | frozenlist = [ 616 | {file = "frozenlist-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:977a1438d0e0d96573fd679d291a1542097ea9f4918a8b6494b06610dfeefbf9"}, 617 | {file = "frozenlist-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8d86547a5e98d9edd47c432f7a14b0c5592624b496ae9880fb6332f34af1edc"}, 618 | {file = "frozenlist-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:181754275d5d32487431a0a29add4f897968b7157204bc1eaaf0a0ce80c5ba7d"}, 619 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5df31bb2b974f379d230a25943d9bf0d3bc666b4b0807394b131a28fca2b0e5f"}, 620 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4766632cd8a68e4f10f156a12c9acd7b1609941525569dd3636d859d79279ed3"}, 621 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16eef427c51cb1203a7c0ab59d1b8abccaba9a4f58c4bfca6ed278fc896dc193"}, 622 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:01d79515ed5aa3d699b05f6bdcf1fe9087d61d6b53882aa599a10853f0479c6c"}, 623 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:28e164722ea0df0cf6d48c4d5bdf3d19e87aaa6dfb39b0ba91153f224b912020"}, 624 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e63ad0beef6ece06475d29f47d1f2f29727805376e09850ebf64f90777962792"}, 625 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41de4db9b9501679cf7cddc16d07ac0f10ef7eb58c525a1c8cbff43022bddca4"}, 626 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c6a9d84ee6427b65a81fc24e6ef589cb794009f5ca4150151251c062773e7ed2"}, 627 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f5f3b2942c3b8b9bfe76b408bbaba3d3bb305ee3693e8b1d631fe0a0d4f93673"}, 628 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c98d3c04701773ad60d9545cd96df94d955329efc7743fdb96422c4b669c633b"}, 629 | {file = "frozenlist-1.2.0-cp310-cp310-win32.whl", hash = "sha256:72cfbeab7a920ea9e74b19aa0afe3b4ad9c89471e3badc985d08756efa9b813b"}, 630 | {file = "frozenlist-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:11ff401951b5ac8c0701a804f503d72c048173208490c54ebb8d7bb7c07a6d00"}, 631 | {file = "frozenlist-1.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b46f997d5ed6d222a863b02cdc9c299101ee27974d9bbb2fd1b3c8441311c408"}, 632 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351686ca020d1bcd238596b1fa5c8efcbc21bffda9d0efe237aaa60348421e2a"}, 633 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfbaa08cf1452acad9cb1c1d7b89394a41e712f88df522cea1a0f296b57782a0"}, 634 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ae2f5e9fa10805fb1c9adbfefaaecedd9e31849434be462c3960a0139ed729"}, 635 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6790b8d96bbb74b7a6f4594b6f131bd23056c25f2aa5d816bd177d95245a30e3"}, 636 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:41f62468af1bd4e4b42b5508a3fe8cc46a693f0cdd0ca2f443f51f207893d837"}, 637 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:ec6cf345771cdb00791d271af9a0a6fbfc2b6dd44cb753f1eeaa256e21622adb"}, 638 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:14a5cef795ae3e28fb504b73e797c1800e9249f950e1c964bb6bdc8d77871161"}, 639 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8b54cdd2fda15467b9b0bfa78cee2ddf6dbb4585ef23a16e14926f4b076dfae4"}, 640 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f025f1d6825725b09c0038775acab9ae94264453a696cc797ce20c0769a7b367"}, 641 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:84e97f59211b5b9083a2e7a45abf91cfb441369e8bb6d1f5287382c1c526def3"}, 642 | {file = "frozenlist-1.2.0-cp36-cp36m-win32.whl", hash = "sha256:c5328ed53fdb0a73c8a50105306a3bc013e5ca36cca714ec4f7bd31d38d8a97f"}, 643 | {file = "frozenlist-1.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9ade70aea559ca98f4b1b1e5650c45678052e76a8ab2f76d90f2ac64180215a2"}, 644 | {file = "frozenlist-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0d3ffa8772464441b52489b985d46001e2853a3b082c655ec5fad9fb6a3d618"}, 645 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3457f8cf86deb6ce1ba67e120f1b0128fcba1332a180722756597253c465fc1d"}, 646 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a72eecf37eface331636951249d878750db84034927c997d47f7f78a573b72b"}, 647 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:acc4614e8d1feb9f46dd829a8e771b8f5c4b1051365d02efb27a3229048ade8a"}, 648 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:87521e32e18a2223311afc2492ef2d99946337da0779ddcda77b82ee7319df59"}, 649 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b4c7665a17c3a5430edb663e4ad4e1ad457614d1b2f2b7f87052e2ef4fa45ca"}, 650 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ed58803563a8c87cf4c0771366cf0ad1aa265b6b0ae54cbbb53013480c7ad74d"}, 651 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa44c4740b4e23fcfa259e9dd52315d2b1770064cde9507457e4c4a65a04c397"}, 652 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2de5b931701257d50771a032bba4e448ff958076380b049fd36ed8738fdb375b"}, 653 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6e105013fa84623c057a4381dc8ea0361f4d682c11f3816cc80f49a1f3bc17c6"}, 654 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:705c184b77565955a99dc360f359e8249580c6b7eaa4dc0227caa861ef46b27a"}, 655 | {file = "frozenlist-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:a37594ad6356e50073fe4f60aa4187b97d15329f2138124d252a5a19c8553ea4"}, 656 | {file = "frozenlist-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:25b358aaa7dba5891b05968dd539f5856d69f522b6de0bf34e61f133e077c1a4"}, 657 | {file = "frozenlist-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af2a51c8a381d76eabb76f228f565ed4c3701441ecec101dd18be70ebd483cfd"}, 658 | {file = "frozenlist-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:82d22f6e6f2916e837c91c860140ef9947e31194c82aaeda843d6551cec92f19"}, 659 | {file = "frozenlist-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cfe6fef507f8bac40f009c85c7eddfed88c1c0d38c75e72fe10476cef94e10f"}, 660 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f602e380a5132880fa245c92030abb0fc6ff34e0c5500600366cedc6adb06a"}, 661 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ad065b2ebd09f32511ff2be35c5dfafee6192978b5a1e9d279a5c6e121e3b03"}, 662 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc93f5f62df3bdc1f677066327fc81f92b83644852a31c6aa9b32c2dde86ea7d"}, 663 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:89fdfc84c6bf0bff2ff3170bb34ecba8a6911b260d318d377171429c4be18c73"}, 664 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:47b2848e464883d0bbdcd9493c67443e5e695a84694efff0476f9059b4cb6257"}, 665 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f52d0732e56906f8ddea4bd856192984650282424049c956857fed43697ea43"}, 666 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:16ef7dd5b7d17495404a2e7a49bac1bc13d6d20c16d11f4133c757dd94c4144c"}, 667 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1cf63243bc5f5c19762943b0aa9e0d3fb3723d0c514d820a18a9b9a5ef864315"}, 668 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:54a1e09ab7a69f843cd28fefd2bcaf23edb9e3a8d7680032c8968b8ac934587d"}, 669 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:954b154a4533ef28bd3e83ffdf4eadf39deeda9e38fb8feaf066d6069885e034"}, 670 | {file = "frozenlist-1.2.0-cp38-cp38-win32.whl", hash = "sha256:cb3957c39668d10e2b486acc85f94153520a23263b6401e8f59422ef65b9520d"}, 671 | {file = "frozenlist-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0a7c7cce70e41bc13d7d50f0e5dd175f14a4f1837a8549b0936ed0cbe6170bf9"}, 672 | {file = "frozenlist-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4c457220468d734e3077580a3642b7f682f5fd9507f17ddf1029452450912cdc"}, 673 | {file = "frozenlist-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e74f8b4d8677ebb4015ac01fcaf05f34e8a1f22775db1f304f497f2f88fdc697"}, 674 | {file = "frozenlist-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fbd4844ff111449f3bbe20ba24fbb906b5b1c2384d0f3287c9f7da2354ce6d23"}, 675 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0081a623c886197ff8de9e635528fd7e6a387dccef432149e25c13946cb0cd0"}, 676 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b6e21e5770df2dea06cb7b6323fbc008b13c4a4e3b52cb54685276479ee7676"}, 677 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:406aeb340613b4b559db78d86864485f68919b7141dec82aba24d1477fd2976f"}, 678 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:878ebe074839d649a1cdb03a61077d05760624f36d196884a5cafb12290e187b"}, 679 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1fef737fd1388f9b93bba8808c5f63058113c10f4e3c0763ced68431773f72f9"}, 680 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a495c3d513573b0b3f935bfa887a85d9ae09f0627cf47cad17d0cc9b9ba5c38"}, 681 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e7d0dd3e727c70c2680f5f09a0775525229809f1a35d8552b92ff10b2b14f2c2"}, 682 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:66a518731a21a55b7d3e087b430f1956a36793acc15912e2878431c7aec54210"}, 683 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:94728f97ddf603d23c8c3dd5cae2644fa12d33116e69f49b1644a71bb77b89ae"}, 684 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c1e8e9033d34c2c9e186e58279879d78c94dd365068a3607af33f2bc99357a53"}, 685 | {file = "frozenlist-1.2.0-cp39-cp39-win32.whl", hash = "sha256:83334e84a290a158c0c4cc4d22e8c7cfe0bba5b76d37f1c2509dabd22acafe15"}, 686 | {file = "frozenlist-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:735f386ec522e384f511614c01d2ef9cf799f051353876b4c6fb93ef67a6d1ee"}, 687 | {file = "frozenlist-1.2.0.tar.gz", hash = "sha256:68201be60ac56aff972dc18085800b6ee07973c49103a8aba669dee3d71079de"}, 688 | ] 689 | humanfriendly = [ 690 | {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, 691 | {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, 692 | ] 693 | idna = [ 694 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 695 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 696 | ] 697 | iniconfig = [ 698 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 699 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 700 | ] 701 | jinja2 = [ 702 | {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, 703 | {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, 704 | ] 705 | livereload = [ 706 | {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, 707 | ] 708 | markupsafe = [ 709 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, 710 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, 711 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, 712 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, 713 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, 714 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, 715 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, 716 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, 717 | {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, 718 | {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, 719 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 720 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 721 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 722 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 723 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 724 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 725 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, 726 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, 727 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, 728 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, 729 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, 730 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, 731 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 732 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 733 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 734 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 735 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 736 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 737 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 738 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 739 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, 740 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, 741 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, 742 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, 743 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, 744 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, 745 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 746 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 747 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, 748 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 749 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 750 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 751 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 752 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 753 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 754 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, 755 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, 756 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, 757 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, 758 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, 759 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, 760 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 761 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 762 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 763 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 764 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 765 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 766 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 767 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 768 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 769 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, 770 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, 771 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, 772 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, 773 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, 774 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, 775 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 776 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 777 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 778 | ] 779 | multidict = [ 780 | {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"}, 781 | {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"}, 782 | {file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"}, 783 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"}, 784 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"}, 785 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"}, 786 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"}, 787 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"}, 788 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"}, 789 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"}, 790 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"}, 791 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"}, 792 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"}, 793 | {file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"}, 794 | {file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"}, 795 | {file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"}, 796 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"}, 797 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"}, 798 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"}, 799 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"}, 800 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"}, 801 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"}, 802 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"}, 803 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"}, 804 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"}, 805 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"}, 806 | {file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"}, 807 | {file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"}, 808 | {file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"}, 809 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"}, 810 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"}, 811 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"}, 812 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"}, 813 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"}, 814 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"}, 815 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"}, 816 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"}, 817 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"}, 818 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"}, 819 | {file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"}, 820 | {file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"}, 821 | {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"}, 822 | {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"}, 823 | {file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"}, 824 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"}, 825 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"}, 826 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"}, 827 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"}, 828 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"}, 829 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"}, 830 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"}, 831 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"}, 832 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"}, 833 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"}, 834 | {file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"}, 835 | {file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"}, 836 | {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"}, 837 | {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"}, 838 | {file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"}, 839 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"}, 840 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"}, 841 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"}, 842 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"}, 843 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"}, 844 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"}, 845 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"}, 846 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"}, 847 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"}, 848 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"}, 849 | {file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"}, 850 | {file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"}, 851 | {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, 852 | ] 853 | packaging = [ 854 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 855 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 856 | ] 857 | pathtools = [ 858 | {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, 859 | ] 860 | pluggy = [ 861 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 862 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 863 | ] 864 | port-for = [ 865 | {file = "port-for-0.3.1.tar.gz", hash = "sha256:b16a84bb29c2954db44c29be38b17c659c9c27e33918dec16b90d375cc596f1c"}, 866 | ] 867 | py = [ 868 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 869 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 870 | ] 871 | pyparsing = [ 872 | {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, 873 | {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, 874 | ] 875 | pyreadline3 = [ 876 | {file = "pyreadline3-3.3-py3-none-any.whl", hash = "sha256:0003fd0079d152ecbd8111202c5a7dfa6a5569ffd65b235e45f3c2ecbee337b4"}, 877 | {file = "pyreadline3-3.3.tar.gz", hash = "sha256:ff3b5a1ac0010d0967869f723e687d42cabc7dccf33b14934c92aa5168d260b3"}, 878 | ] 879 | pytest = [ 880 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 881 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 882 | ] 883 | pytest-aiohttp = [ 884 | {file = "pytest-aiohttp-0.3.0.tar.gz", hash = "sha256:c929854339637977375838703b62fef63528598bc0a9d451639eba95f4aaa44f"}, 885 | {file = "pytest_aiohttp-0.3.0-py3-none-any.whl", hash = "sha256:0b9b660b146a65e1313e2083d0d2e1f63047797354af9a28d6b7c9f0726fa33d"}, 886 | ] 887 | pytest-asyncio = [ 888 | {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, 889 | {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, 890 | ] 891 | pytest-cov = [ 892 | {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, 893 | {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, 894 | ] 895 | pyyaml = [ 896 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 897 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 898 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 899 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 900 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 901 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 902 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 903 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 904 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 905 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 906 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 907 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 908 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 909 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 910 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 911 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 912 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 913 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 914 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 915 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 916 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 917 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 918 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 919 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 920 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 921 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 922 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 923 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 924 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 925 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 926 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 927 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 928 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 929 | ] 930 | requests = [ 931 | {file = "requests-2.27.0-py2.py3-none-any.whl", hash = "sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"}, 932 | {file = "requests-2.27.0.tar.gz", hash = "sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5"}, 933 | ] 934 | six = [ 935 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 936 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 937 | ] 938 | sphinx-autobuild = [ 939 | {file = "sphinx-autobuild-0.7.1.tar.gz", hash = "sha256:66388f81884666e3821edbe05dd53a0cfb68093873d17320d0610de8db28c74e"}, 940 | {file = "sphinx_autobuild-0.7.1-py2-none-any.whl", hash = "sha256:e60aea0789cab02fa32ee63c7acae5ef41c06f1434d9fd0a74250a61f5994692"}, 941 | ] 942 | toml = [ 943 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 944 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 945 | ] 946 | tomli = [ 947 | {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, 948 | {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, 949 | ] 950 | tornado = [ 951 | {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, 952 | {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, 953 | {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, 954 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, 955 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, 956 | {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, 957 | {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, 958 | {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, 959 | {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, 960 | {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, 961 | {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, 962 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, 963 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, 964 | {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, 965 | {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, 966 | {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, 967 | {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, 968 | {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, 969 | {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, 970 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, 971 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, 972 | {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, 973 | {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, 974 | {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, 975 | {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, 976 | {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, 977 | {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, 978 | {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, 979 | {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, 980 | {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, 981 | {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, 982 | {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, 983 | {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, 984 | {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, 985 | {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, 986 | {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, 987 | {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, 988 | {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, 989 | {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, 990 | {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, 991 | {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, 992 | ] 993 | "tus.py" = [ 994 | {file = "tus.py-1.3.4.tar.gz", hash = "sha256:b80feda87700aae629eb19dd98cec68ae520cd9b2aa24bd0bab2b777be0b4366"}, 995 | ] 996 | urllib3 = [ 997 | {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, 998 | {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, 999 | ] 1000 | watchdog = [ 1001 | {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, 1002 | {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, 1003 | {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, 1004 | {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, 1005 | {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, 1006 | {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, 1007 | {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, 1008 | {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, 1009 | {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, 1010 | {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, 1011 | {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, 1012 | {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, 1013 | {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, 1014 | {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, 1015 | {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, 1016 | {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, 1017 | {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, 1018 | {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, 1019 | {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, 1020 | {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, 1021 | {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, 1022 | {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, 1023 | {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, 1024 | ] 1025 | yarl = [ 1026 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, 1027 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, 1028 | {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, 1029 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, 1030 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, 1031 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, 1032 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, 1033 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, 1034 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, 1035 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, 1036 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, 1037 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, 1038 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, 1039 | {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, 1040 | {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, 1041 | {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, 1042 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, 1043 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, 1044 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, 1045 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, 1046 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, 1047 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, 1048 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, 1049 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, 1050 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, 1051 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, 1052 | {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, 1053 | {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, 1054 | {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, 1055 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, 1056 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, 1057 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, 1058 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, 1059 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, 1060 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, 1061 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, 1062 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, 1063 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, 1064 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, 1065 | {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, 1066 | {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, 1067 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, 1068 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, 1069 | {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, 1070 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, 1071 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, 1072 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, 1073 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, 1074 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, 1075 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, 1076 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, 1077 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, 1078 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, 1079 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, 1080 | {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, 1081 | {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, 1082 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, 1083 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, 1084 | {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, 1085 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, 1086 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, 1087 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, 1088 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, 1089 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, 1090 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, 1091 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, 1092 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, 1093 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, 1094 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, 1095 | {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, 1096 | {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, 1097 | {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, 1098 | ] 1099 | --------------------------------------------------------------------------------