├── MANIFEST.in ├── examples ├── petstore │ ├── run.sh │ ├── petstore.py │ └── petstore.yml ├── hello │ ├── hello.yml │ └── hello.py └── async_api │ ├── async_api.yml │ └── async_api.py ├── docs └── images │ ├── sticker.png │ └── sticker_logo.png ├── tests ├── fixtures │ └── api_simple_get │ │ ├── api.py │ │ └── api.yml ├── conftest.py ├── runtimes │ ├── handlers.py │ ├── test_flask.py │ ├── test_bottle.py │ ├── test_sanic.py │ └── test_tornado.py └── test_openapi.py ├── sticker ├── __init__.py ├── runtimes │ ├── bottle.py │ ├── flask.py │ ├── sanic.py │ ├── tornado.py │ └── base.py └── openapi.py ├── AUTHORS.rst ├── Pipfile ├── .gitignore ├── CODE_OF_CONDUCT.rst ├── setup.py ├── README.rst ├── LICENSE └── Pipfile.lock /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | -------------------------------------------------------------------------------- /examples/petstore/run.sh: -------------------------------------------------------------------------------- 1 | export FLASK_APP=petstore.py 2 | python3 -m flask run 3 | -------------------------------------------------------------------------------- /docs/images/sticker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelcaricio/sticker/HEAD/docs/images/sticker.png -------------------------------------------------------------------------------- /tests/fixtures/api_simple_get/api.py: -------------------------------------------------------------------------------- 1 | 2 | def say_hello(params): 3 | return {"content": "Hello!"} 4 | -------------------------------------------------------------------------------- /docs/images/sticker_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelcaricio/sticker/HEAD/docs/images/sticker_logo.png -------------------------------------------------------------------------------- /examples/hello/hello.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | paths: 3 | /: 4 | get: 5 | operationId: hello.say_hello 6 | -------------------------------------------------------------------------------- /tests/fixtures/api_simple_get/api.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | paths: 3 | /: 4 | get: 5 | operationId: api.say_hello 6 | -------------------------------------------------------------------------------- /examples/async_api/async_api.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | paths: 3 | /: 4 | get: 5 | operationId: async_api.handle_request 6 | -------------------------------------------------------------------------------- /sticker/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .runtimes.flask import FlaskAPI 3 | from .runtimes.bottle import BottleAPI 4 | from .runtimes.tornado import TornadoAPI 5 | from .runtimes.sanic import SanicAPI 6 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Maintainers 2 | ``````````` 3 | 4 | - Rafael Caricio `@rafaelcaricio `_ 5 | 6 | Patches and Suggestions 7 | ``````````````````````` 8 | 9 | - ... 10 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | pyyaml = "*" 11 | flask = "*" 12 | bottle = "*" 13 | tornado = "*" 14 | sanic = "*" 15 | aiohttp = "*" 16 | 17 | 18 | [dev-packages] 19 | 20 | pytest = "*" 21 | ipython = "*" 22 | webtest = "*" 23 | twine = "*" 24 | pytest-sanic = "*" 25 | docutils = "*" 26 | restructuredtext-lint = "*" 27 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pytest 3 | 4 | HERE = os.path.abspath(os.path.dirname(__file__)) 5 | FIXTURES_DIR = os.path.join(HERE, 'fixtures') 6 | 7 | 8 | @pytest.fixture() 9 | def simple_api_get_spec(): 10 | return os.path.join(FIXTURES_DIR, 'api_simple_get', 'api.yml') 11 | 12 | 13 | @pytest.fixture(scope='class') 14 | def simple_api_spec_attr(request): 15 | request.cls.simple_api_get_spec = simple_api_get_spec() 16 | -------------------------------------------------------------------------------- /tests/runtimes/handlers.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def handler_set_status_code(params): 4 | return {"status": 201} 5 | 6 | 7 | def handler_set_status_and_content(params): 8 | return { 9 | "content": '{"id":"123"}', 10 | "status": 201 11 | } 12 | 13 | 14 | def handler_set_status_code_to_400(params): 15 | return {"status": 400} 16 | 17 | 18 | def handler_set_headers(params): 19 | return { 20 | "headers": { 21 | "Content-Type": "application/json" 22 | }, 23 | "content": '{"id":"123"}', 24 | "status": 201 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_openapi.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | from sticker.openapi import OpenAPISpec 3 | 4 | 5 | def test_reading_paths(): 6 | spec_text = dedent(""" 7 | openapi: 3.0.0 8 | paths: 9 | /: 10 | get: 11 | operationId: dummy_handler 12 | """) 13 | 14 | spec = OpenAPISpec(spec_text) 15 | assert len(spec.paths()) == 1 16 | 17 | path = spec.paths()[0] 18 | assert path.url_path() == '/' 19 | assert len(path.operations()) == 1 20 | 21 | operation = path.operations()[0] 22 | assert operation.function_fullpath() == 'dummy_handler' 23 | assert operation.http_method() == 'GET' 24 | -------------------------------------------------------------------------------- /examples/async_api/async_api.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | 4 | async def fetch(session, url): 5 | """ 6 | Use session object to perform 'get' request on url 7 | """ 8 | async with session.get(url) as result: 9 | return await result.json() 10 | 11 | 12 | async def handle_request(params): 13 | url = "https://api.github.com/repos/channelcat/sanic" 14 | 15 | async with aiohttp.ClientSession() as session: 16 | result = await fetch(session, url) 17 | 18 | return {'content': result} 19 | 20 | 21 | if __name__ == '__main__': 22 | from sticker import SanicAPI 23 | api = SanicAPI('async_api.yml') 24 | app = api.get_app(__name__) 25 | app.run(host="0.0.0.0", port=8000, workers=2) 26 | -------------------------------------------------------------------------------- /examples/hello/hello.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def say_hello(params): 4 | return {"content": "Hello World!"} 5 | 6 | 7 | def run_with_flask(): 8 | from sticker import FlaskAPI 9 | api = FlaskAPI('hello.yml') 10 | api.get_app(__name__).run() 11 | 12 | 13 | def run_with_bottle(): 14 | from sticker import BottleAPI 15 | api = BottleAPI('hello.yml') 16 | api.run() 17 | 18 | 19 | def run_with_tornado(): 20 | import tornado.ioloop 21 | from sticker import TornadoAPI 22 | api = TornadoAPI('hello.yml') 23 | api.get_app().listen(8888) 24 | tornado.ioloop.IOLoop.current().start() 25 | 26 | 27 | def run_with_sanic(): 28 | from sticker import SanicAPI 29 | api = SanicAPI('hello.yml') 30 | api.get_app(__name__).run() 31 | 32 | 33 | if __name__ == '__main__': 34 | run_with_sanic() 35 | -------------------------------------------------------------------------------- /sticker/runtimes/bottle.py: -------------------------------------------------------------------------------- 1 | 2 | from .base import FlaskLikeAPI 3 | from typing import Optional 4 | import bottle 5 | 6 | 7 | class BottleAPI(FlaskLikeAPI): 8 | def __init__(self, spec_filename: Optional[str]=None, spec_text: Optional[str]=None): 9 | super().__init__(spec_filename=spec_filename, spec_text=spec_text, request=bottle.request) 10 | self.app = bottle.app() 11 | 12 | def run(self, *args, **kwargs): 13 | self.register_routes() 14 | return self.app.run(*args, **kwargs) 15 | 16 | def register_route(self, rule, endpoint, view_func, methods): 17 | self.app.route(rule, methods, view_func) 18 | 19 | def back_to_framework(self, result): 20 | kwargs = { 21 | 'body': result.get('content', ''), 22 | 'status': result.get('status', 200), 23 | 'headers': result.get('headers', {}) 24 | } 25 | return bottle.HTTPResponse(**kwargs) 26 | -------------------------------------------------------------------------------- /sticker/runtimes/flask.py: -------------------------------------------------------------------------------- 1 | 2 | from .base import FlaskLikeAPI 3 | from typing import Optional 4 | import flask 5 | 6 | 7 | class FlaskAPI(FlaskLikeAPI): 8 | def __init__(self, spec_filename: Optional[str]=None, spec_text: Optional[str]=None): 9 | super().__init__(spec_filename=spec_filename, spec_text=spec_text, request=flask.request) 10 | self.app: flask.Flask = None 11 | 12 | def get_app(self, *args, **kwargs) -> flask.Flask: 13 | self.app = flask.Flask(*args, **kwargs) 14 | self.register_routes() 15 | return self.app 16 | 17 | def register_route(self, rule, endpoint, view_func, methods): 18 | self.app.add_url_rule( 19 | rule=rule, 20 | endpoint=endpoint, 21 | view_func=view_func, 22 | methods=methods 23 | ) 24 | 25 | def back_to_framework(self, result: dict): 26 | args = (result.get('content', ''), result.get('status', 200), result.get('headers', {}),) 27 | return flask.make_response(*args) 28 | -------------------------------------------------------------------------------- /examples/petstore/petstore.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | PETS_STORAGE = [] 4 | 5 | 6 | def list_pets(params): 7 | limit = min(params.get('limit', 100), 100) 8 | return {'content': json.dumps(PETS_STORAGE[:limit])} 9 | 10 | 11 | def create_pets(params): 12 | global PETS_STORAGE 13 | PETS_STORAGE.append(params['pet']) 14 | return {'status_code': 201} 15 | 16 | 17 | def show_pet_by_id(params): 18 | pet_id = params['petId'] 19 | for pet in PETS_STORAGE: 20 | if pet['id'] == pet_id: 21 | return {'content': json.dumps(pet)} 22 | return {'status_code': 404} 23 | 24 | 25 | def run_with_flask(): 26 | from sticker import FlaskAPI 27 | api = FlaskAPI('petstore.yml') 28 | api.get_app(__name__).run() 29 | 30 | 31 | def run_with_bottle(): 32 | from sticker import BottleAPI 33 | api = BottleAPI('petstore.yml') 34 | api.run() 35 | 36 | 37 | def run_with_tornado(): 38 | import tornado.ioloop 39 | from sticker import TornadoAPI 40 | api = TornadoAPI('petstore.yml') 41 | api.get_app().listen(8888) 42 | tornado.ioloop.IOLoop.current().start() 43 | 44 | 45 | if __name__ == '__main__': 46 | run_with_tornado() 47 | -------------------------------------------------------------------------------- /tests/runtimes/test_flask.py: -------------------------------------------------------------------------------- 1 | from webtest import TestApp 2 | from textwrap import dedent 3 | 4 | from sticker import FlaskAPI 5 | 6 | 7 | def test_simple(simple_api_get_spec): 8 | api = FlaskAPI(simple_api_get_spec) 9 | api_client = TestApp(api.get_app(__name__)) 10 | 11 | response = api_client.get('/') 12 | assert response.status == '200 OK' 13 | assert response.body.decode() == 'Hello!' 14 | 15 | 16 | def api_client_for(operation_id): 17 | api = FlaskAPI(spec_text=dedent(""" 18 | openapi: 3.0.0 19 | paths: 20 | /: 21 | get: 22 | operationId: handlers.{operation_id} 23 | """.format(operation_id=operation_id))) 24 | app = api.get_app(__name__) 25 | app.debug = True 26 | api_client = TestApp(app) 27 | return api_client 28 | 29 | 30 | def test_set_status_code(): 31 | response = api_client_for('handler_set_status_code').get('/') 32 | assert response.status == '201 CREATED' 33 | assert response.body.decode() == '' 34 | 35 | 36 | def test_set_status_and_content(): 37 | response = api_client_for('handler_set_status_and_content').get('/') 38 | assert response.status == '201 CREATED' 39 | assert response.body.decode() == '{"id":"123"}' 40 | 41 | 42 | def test_set_headers(): 43 | response = api_client_for('handler_set_headers').get('/') 44 | assert response.status == '201 CREATED' 45 | assert response.body.decode() == '{"id":"123"}' 46 | assert 'Content-Type' in response.headers 47 | assert response.headers['Content-Type'] == 'application/json' 48 | -------------------------------------------------------------------------------- /tests/runtimes/test_bottle.py: -------------------------------------------------------------------------------- 1 | from webtest import TestApp 2 | from textwrap import dedent 3 | 4 | from sticker import BottleAPI 5 | 6 | 7 | def test_simple(simple_api_get_spec): 8 | api = BottleAPI(simple_api_get_spec) 9 | api.register_routes() 10 | api_client = TestApp(api.app) 11 | 12 | response = api_client.get('/') 13 | assert response.status == '200 OK' 14 | assert response.body.decode() == 'Hello!' 15 | 16 | 17 | def api_client_for(operation_id): 18 | api = BottleAPI(spec_text=dedent(""" 19 | openapi: 3.0.0 20 | paths: 21 | /: 22 | get: 23 | operationId: handlers.{operation_id} 24 | """.format(operation_id=operation_id))) 25 | api.register_routes() 26 | api_client = TestApp(api.app) 27 | return api_client 28 | 29 | 30 | def test_set_status_code(): 31 | client = api_client_for('handler_set_status_code') 32 | response = client.get('/') 33 | assert response.status == '201 Created' 34 | assert response.body.decode() == '' 35 | 36 | 37 | def test_set_status_and_content(): 38 | response = api_client_for('handler_set_status_and_content').get('/') 39 | assert response.status == '201 Created' 40 | assert response.body.decode() == '{"id":"123"}' 41 | 42 | 43 | def test_set_headers(): 44 | response = api_client_for('handler_set_headers').get('/') 45 | assert response.status == '201 Created' 46 | assert response.body.decode() == '{"id":"123"}' 47 | assert 'Content-Type' in response.headers 48 | assert response.headers['Content-Type'] == 'application/json' 49 | -------------------------------------------------------------------------------- /sticker/openapi.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Any, Callable, Optional 2 | import yaml 3 | import importlib 4 | 5 | 6 | class SpecOperation: 7 | def __init__(self, method_type: str, definition: Dict[str, Any]): 8 | self.method_type = method_type 9 | self.definition = definition 10 | 11 | def http_method(self) -> str: 12 | return self.method_type.upper() 13 | 14 | def function_fullpath(self) -> Optional[str]: 15 | return self.definition.get('operationId') 16 | 17 | def resolve_function(self) -> Callable: 18 | module, func = self.function_fullpath().rsplit('.', 1) 19 | return getattr(importlib.import_module(module), func) 20 | 21 | 22 | class SpecPath: 23 | def __init__(self, path: str, definition: Dict[str, dict]): 24 | self.path = path 25 | self.definition = definition 26 | 27 | def url_path(self) -> str: 28 | return self.path 29 | 30 | def operations(self) -> List[SpecOperation]: 31 | available_operations: List[SpecOperation] = [] 32 | for method_type, op_def in self.definition.items(): 33 | available_operations.append(SpecOperation(method_type, op_def)) 34 | return available_operations 35 | 36 | 37 | class OpenAPISpec: 38 | def __init__(self, spec_text: str): 39 | self.definition = yaml.safe_load(spec_text) 40 | 41 | def paths(self) -> List[SpecPath]: 42 | available_paths: List[SpecPath] = [] 43 | for url_path, path_def in self.definition.get('paths', {}).items(): 44 | available_paths.append(SpecPath(path=url_path, definition=path_def)) 45 | return available_paths 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .vscode 103 | .DS_Store 104 | .idea 105 | .pytest_cache 106 | -------------------------------------------------------------------------------- /sticker/runtimes/sanic.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from typing import Callable, Dict, Optional 4 | from sanic import Sanic, response 5 | 6 | from .base import FlaskLikeAPI 7 | 8 | 9 | class SanicAPI(FlaskLikeAPI): 10 | def __init__(self, spec_filename: Optional[str]=None, spec_text: Optional[str]=None): 11 | super().__init__(spec_filename=spec_filename, spec_text=spec_text) 12 | self.app: Sanic = None 13 | 14 | def get_app(self, *args, **kwargs): 15 | self.app = Sanic(*args, **kwargs) 16 | self.register_routes() 17 | return self.app 18 | 19 | def register_route(self, rule, endpoint, view_func, methods): 20 | self.app.route(uri=rule, methods=methods, name=endpoint)(view_func) 21 | 22 | def wrap_handler(self, bare_function: Callable): 23 | if asyncio.iscoroutine(bare_function): 24 | async def _wrapper(request, *args, **kwargs): 25 | params = self.to_python_literals(request, *args, **kwargs) 26 | return self.back_to_framework(await bare_function(params)) 27 | else: 28 | def _wrapper(request, *args, **kwargs): 29 | params = self.to_python_literals(request, *args, **kwargs) 30 | return self.back_to_framework(bare_function(params)) 31 | return _wrapper 32 | 33 | def route_call_by_http_method(self, method_to_func: Dict[str, Callable]) -> Callable: 34 | def _route(request, *args, **kwargs): 35 | return method_to_func[request.method](request, *args, **kwargs) 36 | return _route 37 | 38 | def to_python_literals(self, request, *args, **kwargs): 39 | return {} 40 | 41 | def back_to_framework(self, result): 42 | return response.text(result.get('content', ''), 43 | status=result.get('status', 200), 44 | headers=result.get('headers', {})) 45 | -------------------------------------------------------------------------------- /tests/runtimes/test_sanic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sticker import SanicAPI 3 | from textwrap import dedent 4 | 5 | 6 | async def test_simple(test_client, simple_api_get_spec): 7 | api = SanicAPI(simple_api_get_spec) 8 | api_client = await test_client(api.get_app(__name__)) 9 | 10 | response = await api_client.get('/') 11 | assert 200 == response.status 12 | assert 'Hello!' == (await response.content.read()).decode() 13 | 14 | 15 | def api_client_for(operation_id, test_client): 16 | api = SanicAPI(spec_text=dedent(""" 17 | openapi: 3.0.0 18 | paths: 19 | /: 20 | get: 21 | operationId: handlers.{operation_id} 22 | """.format(operation_id=operation_id))) 23 | return test_client(api.get_app(__name__)) 24 | 25 | 26 | async def test_set_status_code(test_client): 27 | api_client = await api_client_for('handler_set_status_code', test_client) 28 | response = await api_client.get('/') 29 | assert 201 == response.status 30 | assert '' == (await response.content.read()).decode() 31 | 32 | 33 | async def test_set_status_code_to_400(test_client): 34 | api_client = await api_client_for('handler_set_status_code_to_400', test_client) 35 | response = await api_client.get('/') 36 | assert 400 == response.status 37 | assert (await response.content.read()).decode() == '' 38 | 39 | 40 | async def test_set_status_and_content(test_client): 41 | api_client = await api_client_for('handler_set_status_and_content', test_client) 42 | response = await api_client.get('/') 43 | assert 201 == response.status 44 | assert '{"id":"123"}' == (await response.content.read()).decode() 45 | 46 | 47 | async def test_set_headers(test_client): 48 | api_client = await api_client_for('handler_set_headers', test_client) 49 | response = await api_client.get('/') 50 | assert 201 == response.status 51 | assert '{"id":"123"}' == (await response.content.read()).decode() 52 | assert 'Content-Type' in response.headers 53 | assert 'application/json' == response.headers['Content-Type'] 54 | 55 | -------------------------------------------------------------------------------- /tests/runtimes/test_tornado.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from textwrap import dedent 3 | 4 | from sticker import TornadoAPI 5 | from tornado.testing import AsyncHTTPTestCase 6 | 7 | 8 | @pytest.mark.usefixtures("simple_api_spec_attr") 9 | class TestHelloApp(AsyncHTTPTestCase): 10 | def get_app(self): 11 | return TornadoAPI(self.simple_api_get_spec).get_app() 12 | 13 | def test_homepage(self): 14 | response = self.fetch('/') 15 | self.assertEqual(response.code, 200) 16 | self.assertEqual(response.body.decode(), 'Hello!') 17 | 18 | 19 | class APIClientForTestCase(AsyncHTTPTestCase): 20 | operation_id = NotImplemented 21 | 22 | def get_app(self): 23 | return self.api_client_for().get_app() 24 | 25 | def api_client_for(self): 26 | return TornadoAPI(spec_text=dedent(""" 27 | openapi: 3.0.0 28 | paths: 29 | /: 30 | get: 31 | operationId: handlers.{operation_id} 32 | """.format(operation_id=self.operation_id))) 33 | 34 | 35 | class TestSetStatusCode(APIClientForTestCase): 36 | operation_id = 'handler_set_status_code' 37 | 38 | def test_set_status_code(self): 39 | response = self.fetch('/') 40 | self.assertEqual(201, response.code) 41 | self.assertEqual('', response.body.decode()) 42 | 43 | 44 | class TestSetStatusAndContent(APIClientForTestCase): 45 | operation_id = 'handler_set_status_and_content' 46 | 47 | def test_set_status_and_content(self): 48 | response = self.fetch('/') 49 | self.assertEqual(201, response.code) 50 | self.assertEqual('{"id":"123"}', response.body.decode()) 51 | 52 | 53 | class TestSetHeaders(APIClientForTestCase): 54 | operation_id = 'handler_set_headers' 55 | 56 | def test_set_headers(self): 57 | response = self.fetch('/') 58 | self.assertEqual(201, response.code) 59 | self.assertEqual('{"id":"123"}', response.body.decode()) 60 | self.assertTrue('Content-Type' in response.headers) 61 | self.assertEquals('application/json', response.headers['Content-Type']) 62 | -------------------------------------------------------------------------------- /examples/petstore/petstore.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: MIT 7 | servers: 8 | - url: http://petstore.swagger.io/v1 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | operationId: petstore.list_pets 14 | tags: 15 | - pets 16 | parameters: 17 | - name: limit 18 | in: query 19 | description: How many items to return at one time (max 100) 20 | required: false 21 | schema: 22 | type: integer 23 | format: int32 24 | responses: 25 | '200': 26 | description: An paged array of pets 27 | headers: 28 | x-next: 29 | description: A link to the next page of responses 30 | schema: 31 | type: string 32 | content: 33 | application/json: 34 | schema: 35 | $ref: "#/components/schemas/Pets" 36 | default: 37 | description: unexpected error 38 | content: 39 | application/json: 40 | schema: 41 | $ref: "#/components/schemas/Error" 42 | post: 43 | summary: Create a pet 44 | operationId: petstore.create_pets 45 | tags: 46 | - pets 47 | responses: 48 | '201': 49 | description: Null response 50 | default: 51 | description: unexpected error 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "#/components/schemas/Error" 56 | /pets/{petId}: 57 | get: 58 | summary: Info for a specific pet 59 | operationId: petstore.show_pet_by_id 60 | tags: 61 | - pets 62 | parameters: 63 | - name: petId 64 | in: path 65 | required: true 66 | description: The id of the pet to retrieve 67 | schema: 68 | type: string 69 | responses: 70 | '200': 71 | description: Expected response to a valid request 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/Pets" 76 | default: 77 | description: unexpected error 78 | content: 79 | application/json: 80 | schema: 81 | $ref: "#/components/schemas/Error" 82 | components: 83 | schemas: 84 | Pet: 85 | required: 86 | - id 87 | - name 88 | properties: 89 | id: 90 | type: integer 91 | format: int64 92 | name: 93 | type: string 94 | tag: 95 | type: string 96 | Pets: 97 | type: array 98 | items: 99 | $ref: "#/components/schemas/Pet" 100 | Error: 101 | required: 102 | - code 103 | - message 104 | properties: 105 | code: 106 | type: integer 107 | format: int32 108 | message: 109 | type: string 110 | -------------------------------------------------------------------------------- /sticker/runtimes/tornado.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Callable, Optional 2 | import re 3 | 4 | import tornado.web 5 | 6 | from .base import BaseAPI 7 | from ..openapi import SpecPath, SpecOperation 8 | 9 | 10 | class TornadoAPI(BaseAPI): 11 | def __init__(self, spec_filename: Optional[str]=None, spec_text: Optional[str]=None): 12 | super().__init__(spec_filename=spec_filename, spec_text=spec_text) 13 | self.routes: List = None 14 | 15 | def get_app(self, *args, **kwargs): 16 | self.routes = [] 17 | self.register_routes() 18 | return tornado.web.Application(self.routes, *args, **kwargs) 19 | 20 | def register_path(self, path: SpecPath) -> None: 21 | kwargs = { 22 | 'api': self, 23 | 'operations': path.operations() 24 | } 25 | path_regex = self.to_regex(path.url_path()) 26 | self.routes.append((path_regex, GenericHandler, kwargs)) 27 | 28 | @staticmethod 29 | def to_regex(path: str) -> str: 30 | return re.sub(r'{([^}]+)}', r'(?P<\1>[^/]+)', path) 31 | 32 | def wrap_handler(self, 33 | handler: tornado.web.RequestHandler, 34 | operation: SpecOperation, 35 | bare_function: Callable): 36 | def _wrapper(*args, **kwargs): 37 | params = self.to_python_literals(handler, operation, *args, **kwargs) 38 | return self.back_to_framework(handler, operation, bare_function(params)) 39 | return _wrapper 40 | 41 | def to_python_literals( 42 | self, 43 | handler: tornado.web.RequestHandler, 44 | operation: SpecOperation, 45 | *args, 46 | **kwargs 47 | ): 48 | return {} 49 | 50 | def back_to_framework( 51 | self, 52 | handler: tornado.web.RequestHandler, 53 | operation: SpecOperation, 54 | result 55 | ): 56 | handler.write(result.get('content', '')) 57 | handler.set_status(result.get('status', 200)) 58 | for header, value in result.get('headers', {}).items(): 59 | handler.set_header(header, value) 60 | handler.finish() 61 | 62 | 63 | class GenericHandler(tornado.web.RequestHandler): 64 | api: TornadoAPI = None 65 | operations_hash: Dict[str, Callable] = None 66 | 67 | def initialize(self, api: TornadoAPI, operations: List[SpecOperation]): 68 | self.api = api 69 | self.operations_hash = {} 70 | for operation in operations: 71 | handler = api.wrap_handler( 72 | handler=self, 73 | operation=operation, 74 | bare_function=operation.resolve_function() 75 | ) 76 | self.operations_hash[operation.http_method()] = handler 77 | 78 | def call_method_or_super(self, method: str, *args, **kwargs): 79 | if method.upper() not in self.operations_hash: 80 | return getattr(super(), method.lower())(*args, **kwargs) 81 | return self.operations_hash[method.upper()](*args, **kwargs) 82 | 83 | def get(self, *args, **kwargs): 84 | return self.call_method_or_super('get', *args, **kwargs) 85 | 86 | def post(self, *args, **kwargs): 87 | return self.call_method_or_super('post', *args, **kwargs) 88 | 89 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | ==================================== 3 | 4 | Our Pledge 5 | ---------- 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to making participation in our 9 | project and our community a harassment-free experience for everyone, 10 | regardless of age, body size, disability, ethnicity, gender identity and 11 | expression, level of experience, nationality, personal appearance, race, 12 | religion, or sexual identity and orientation. 13 | 14 | Our Standards 15 | ------------- 16 | 17 | Examples of behavior that contributes to creating a positive environment 18 | include: 19 | 20 | - Using welcoming and inclusive language 21 | - Being respectful of differing viewpoints and experiences 22 | - Gracefully accepting constructive criticism 23 | - Focusing on what is best for the community 24 | - Showing empathy towards other community members 25 | 26 | Examples of unacceptable behavior by participants include: 27 | 28 | - The use of sexualized language or imagery and unwelcome sexual 29 | attention or advances 30 | - Trolling, insulting/derogatory comments, and personal or political 31 | attacks 32 | - Public or private harassment 33 | - Publishing others’ private information, such as a physical or 34 | electronic address, without explicit permission 35 | - Other conduct which could reasonably be considered inappropriate in a 36 | professional setting 37 | 38 | Our Responsibilities 39 | -------------------- 40 | 41 | Project maintainers are responsible for clarifying the standards of 42 | acceptable behavior and are expected to take appropriate and fair 43 | corrective action in response to any instances of unacceptable behavior. 44 | 45 | Project maintainers have the right and responsibility to remove, edit, 46 | or reject comments, commits, code, wiki edits, issues, and other 47 | contributions that are not aligned to this Code of Conduct, or to ban 48 | temporarily or permanently any contributor for other behaviors that they 49 | deem inappropriate, threatening, offensive, or harmful. 50 | 51 | Scope 52 | ----- 53 | 54 | This Code of Conduct applies both within project spaces and in public 55 | spaces when an individual is representing the project or its community. 56 | Examples of representing a project or community include using an 57 | official project e-mail address, posting via an official social media 58 | account, or acting as an appointed representative at an online or 59 | offline event. Representation of a project may be further defined and 60 | clarified by project maintainers. 61 | 62 | Enforcement 63 | ----------- 64 | 65 | Instances of abusive, harassing, or otherwise unacceptable behavior may 66 | be reported by contacting the project team at sticker@caric.io. The 67 | project team will review and investigate all complaints, and will 68 | respond in a way that it deems appropriate to the circumstances. The 69 | project team is obligated to maintain confidentiality with regard to the 70 | reporter of an incident. Further details of specific enforcement 71 | policies may be posted separately. 72 | 73 | Project maintainers who do not follow or enforce the Code of Conduct in 74 | good faith may face temporary or permanent repercussions as determined 75 | by other members of the project’s leadership. 76 | 77 | Attribution 78 | ----------- 79 | 80 | This Code of Conduct is adapted from the `Contributor 81 | Covenant `__, version 1.4, available at 82 | `http://contributor-covenant.org/version/1/4 `__ 83 | -------------------------------------------------------------------------------- /sticker/runtimes/base.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from os import path 3 | import sys 4 | from typing import Dict, Callable, Optional 5 | 6 | from ..openapi import OpenAPISpec, SpecPath 7 | 8 | 9 | class BaseAPI: 10 | def __init__(self, spec_filename: Optional[str]=None, spec_text: Optional[str]=None): 11 | if spec_text: 12 | self.contents = spec_text 13 | else: 14 | self.setup_path(spec_filename) 15 | self.contents = self.read_file_contents(spec_filename) 16 | self.spec = OpenAPISpec(self.contents) 17 | 18 | @staticmethod 19 | def setup_path(spec_filename): 20 | handlers_base_path = path.dirname(path.abspath(spec_filename)) 21 | sys.path.insert(1, path.abspath(handlers_base_path)) 22 | 23 | @staticmethod 24 | def read_file_contents(filename: str): 25 | with open(filename, encoding='utf-8') as file: 26 | return file.read() 27 | 28 | def register_routes(self) -> None: 29 | for path in self.spec.paths(): 30 | self.register_path(path) 31 | 32 | def register_path(self, path: SpecPath) -> None: 33 | raise NotImplementedError 34 | 35 | def wrap_handler(self, bare_function: Callable): 36 | def _wrapper(*args, **kwargs): 37 | params = self.to_python_literals(*args, **kwargs) 38 | return self.back_to_framework(bare_function(params)) 39 | return _wrapper 40 | 41 | @abstractmethod 42 | def to_python_literals(self, *args, **kwargs): 43 | """ 44 | Get flask parameters and build Python literals. 45 | 46 | :return: 47 | """ 48 | raise NotImplementedError 49 | 50 | @abstractmethod 51 | def back_to_framework(self, result): 52 | """ 53 | Returns response values that Flask understand. 54 | 55 | :return: 56 | """ 57 | raise NotImplementedError 58 | 59 | 60 | class FlaskLikeAPI(BaseAPI): 61 | def __init__(self, spec_filename: Optional[str]=None, spec_text: Optional[str]=None, request=None): 62 | self.request = request 63 | super().__init__(spec_filename=spec_filename, spec_text=spec_text) 64 | 65 | def register_routes(self) -> None: 66 | for path in self.spec.paths(): 67 | self.register_path(path) 68 | 69 | def register_path(self, path: SpecPath) -> None: 70 | method_to_func: Dict[str, Callable] = {} 71 | for operation in path.operations(): 72 | method_to_func[operation.http_method()] = self.wrap_handler( 73 | operation.resolve_function()) 74 | 75 | route_path = self.translate_route_format(path) 76 | view_func = self.route_call_by_http_method(method_to_func) 77 | self.register_route( 78 | rule=route_path, 79 | endpoint=path.url_path(), 80 | view_func=view_func, 81 | methods=list(method_to_func.keys()) 82 | ) 83 | 84 | @staticmethod 85 | def translate_route_format(path) -> str: 86 | return path.url_path().replace('{', '<').replace('}', '>') 87 | 88 | def route_call_by_http_method(self, method_to_func: Dict[str, Callable]) -> Callable: 89 | def _route(*args, **kwargs): 90 | return method_to_func[self.request.method](*args, **kwargs) 91 | return _route 92 | 93 | def to_python_literals(self, *args, **kwargs): 94 | """ 95 | Get flask parameters and build Python literals. 96 | 97 | :return: 98 | """ 99 | return {} 100 | 101 | def back_to_framework(self, result): 102 | """ 103 | Returns response values that Flask understand. 104 | 105 | :return: 106 | """ 107 | return result.get('content', '') 108 | 109 | @abstractmethod 110 | def register_route(self, rule, endpoint, view_func, methods): 111 | raise NotImplementedError 112 | 113 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import find_packages, setup, Command 13 | 14 | # Package meta-data. 15 | NAME = 'sticker' 16 | DESCRIPTION = 'Sticker is a powerful yet boilerplate-free alternative to writing your web API.' 17 | URL = 'https://github.com/rafaelcaricio/sticker' 18 | EMAIL = 'sticker-dev@caric.io' 19 | AUTHOR = 'Rafael Caricio' 20 | REQUIRES_PYTHON = '>=3.6.0' 21 | VERSION = '0.0.5' 22 | 23 | # What packages are required for this module to be executed? 24 | REQUIRED = [ 25 | 'pyyaml' 26 | ] 27 | 28 | # The rest you shouldn't have to touch too much :) 29 | # ------------------------------------------------ 30 | # Except, perhaps the License and Trove Classifiers! 31 | # If you do change the License, remember to change the Trove Classifier for that! 32 | 33 | here = os.path.abspath(os.path.dirname(__file__)) 34 | 35 | # Import the README and use it as the long-description. 36 | # Note: this will only work if 'README.rst' is present in your MANIFEST.in file! 37 | with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 38 | long_description = '\n' + f.read() 39 | 40 | # Load the package's __version__.py module as a dictionary. 41 | about = {} 42 | if not VERSION: 43 | with open(os.path.join(here, NAME, '__version__.py')) as f: 44 | exec(f.read(), about) 45 | else: 46 | about['__version__'] = VERSION 47 | 48 | 49 | class UploadCommand(Command): 50 | """Support setup.py upload.""" 51 | 52 | description = 'Build and publish the package.' 53 | user_options = [] 54 | 55 | @staticmethod 56 | def status(s): 57 | """Prints things in bold.""" 58 | print('\033[1m{0}\033[0m'.format(s)) 59 | 60 | def initialize_options(self): 61 | pass 62 | 63 | def finalize_options(self): 64 | pass 65 | 66 | def run(self): 67 | try: 68 | self.status('Removing previous builds…') 69 | rmtree(os.path.join(here, 'dist')) 70 | except OSError: 71 | pass 72 | 73 | self.status('Building Source and Wheel (universal) distribution…') 74 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 75 | 76 | self.status('Uploading the package to PyPi via Twine…') 77 | os.system('twine upload dist/*') 78 | 79 | self.status('Pushing git tags…') 80 | os.system('git tag {0}'.format(about['__version__'])) 81 | os.system('git push --tags') 82 | 83 | sys.exit() 84 | 85 | 86 | # Where the magic happens: 87 | setup( 88 | name=NAME, 89 | version=about['__version__'], 90 | description=DESCRIPTION, 91 | long_description=long_description, 92 | author=AUTHOR, 93 | author_email=EMAIL, 94 | python_requires=REQUIRES_PYTHON, 95 | url=URL, 96 | packages=find_packages(exclude=('tests',)), 97 | # If your package is a single module, use this instead of 'packages': 98 | # py_modules=['mypackage'], 99 | 100 | # entry_points={ 101 | # 'console_scripts': ['mycli=mymodule:cli'], 102 | # }, 103 | install_requires=REQUIRED, 104 | extras_require={ 105 | 'tornado': ['tornado>=5.0.1'], 106 | 'bottle': ['bottle>=0.12.13'], 107 | 'flask': ['flask>=0.12.2'], 108 | 'sanic': ['sanic>=0.7.0'], 109 | }, 110 | include_package_data=True, 111 | license='Apache 2.0', 112 | classifiers=[ 113 | # Trove classifiers 114 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 115 | 'Development Status :: 2 - Pre-Alpha', 116 | 'License :: OSI Approved :: Apache Software License', 117 | 'Programming Language :: Python', 118 | 'Programming Language :: Python :: 3', 119 | 'Programming Language :: Python :: 3.6', 120 | 'Programming Language :: Python :: 3.7', 121 | 'Programming Language :: Python :: 3 :: Only', 122 | 'Programming Language :: Python :: Implementation :: CPython', 123 | 'Programming Language :: Python :: Implementation :: PyPy', 124 | 'Framework :: Bottle', 125 | 'Framework :: AsyncIO', 126 | 'Framework :: Flask', 127 | 'Topic :: Internet :: WWW/HTTP', 128 | 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', 129 | 'Topic :: Internet :: WWW/HTTP :: WSGI', 130 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 131 | 'Topic :: Software Development :: Libraries :: Python Modules', 132 | ], 133 | # $ setup.py publish support. 134 | cmdclass={ 135 | 'upload': UploadCommand, 136 | }, 137 | ) 138 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/rafaelcaricio/sticker/raw/master/docs/images/sticker_logo.png 2 | :align: center 3 | :alt: Sticker 4 | :target: https://github.com/rafaelcaricio/sticker 5 | 6 | | 7 | 8 | .. image:: https://img.shields.io/pypi/v/sticker.svg 9 | :target: https://pypi.python.org/pypi/sticker 10 | 11 | .. image:: https://img.shields.io/pypi/l/sticker.svg 12 | :target: https://pypi.python.org/pypi/sticker 13 | 14 | .. image:: https://img.shields.io/pypi/pyversions/sticker.svg 15 | :target: https://pypi.python.org/pypi/sticker 16 | 17 | .. image:: https://img.shields.io/github/contributors/rafaelcaricio/sticker.svg 18 | :target: https://github.com/rafaelcaricio/sticker/graphs/contributors 19 | 20 | Write boilerplate-free Python functions and use them as your API handlers. 21 | Sticker allows you to choose Flask, bottle.py, Sanic, or Tornado as your 22 | application runtime. 23 | 24 | **Highlights**: 25 | 26 | * Community created and maintained 27 | * Support for `OpenAPI 3.0 `_ 28 | * Multi-framework support: `Flask `_, `bottle.py `_, `Sanic `_, and `Tornado `_ 29 | * Support for **pure Python handlers** (no boilerplate code) 30 | 31 | It's Easy to Write 32 | ================== 33 | 34 | You need a little bit of Python. 35 | 36 | .. code-block:: python 37 | 38 | def say_hello(params): 39 | return {"contents": "Hello World!"} 40 | 41 | Plus bits of your API description. 42 | 43 | .. code-block:: YAML 44 | 45 | openapi: "3.0.0" 46 | paths: 47 | /: 48 | get: 49 | operationId: hello.say_hello 50 | 51 | Now the fun part, you choose which web framework you want to use. 52 | 53 | Run with Flask: 54 | 55 | .. code-block:: python 56 | 57 | from sticker import FlaskAPI 58 | api = FlaskAPI('hello.yml') 59 | api.get_app(__name__).run() 60 | 61 | Run with Bottle.py: 62 | 63 | .. code-block:: python 64 | 65 | from sticker import BottleAPI 66 | api = BottleAPI('hello.yml') 67 | api.run() 68 | 69 | Run with Sanic: 70 | 71 | .. code-block:: python 72 | 73 | from sticker import SanicAPI 74 | api = SanicAPI('hello.yml') 75 | api.get_app(__name__).run() 76 | 77 | Run with Tornado: 78 | 79 | .. code-block:: python 80 | 81 | from sticker import TornadoAPI 82 | import tornado.ioloop 83 | api = TornadoAPI('hello.yml') 84 | api.get_app().listen(8888) 85 | tornado.ioloop.IOLoop.current().start() 86 | 87 | The framework setup, validation, types conversion, and mocking is handled at runtime by Sticker. 88 | 89 | ✨ 90 | 91 | Installation 92 | ============ 93 | 94 | Sticker is published at PyPI, so you can use ``pip`` to install: 95 | 96 | .. code-block:: bash 97 | 98 | $ pip install sticker 99 | 100 | Requirements 101 | ============ 102 | 103 | Sticker was developed for **Python >=3.6** and **OpenAPI 3.0**. Support for Python 2.7 is not present nor planned for this project. 104 | 105 | Documentation 106 | ============= 107 | 108 | Sticker is a flexible metaframework for Web API development and execution. The OpenAPI 3.0 standard is used as 109 | description format for Sticker powered APIs. You provide the API specification and choose one of the 110 | Sticker's runtimes to have a webserver up and running. 111 | 112 | In this document we will describe a few different ways to write code that works well with Sticker. 113 | 114 | Pure Python Handlers 115 | -------------------- 116 | 117 | Sticker supports the use of pure Python functions as handlers. Your code will be free of any framework 118 | specific boilerplate code, including Sticker's itself. This allows you to swap between different frameworks 119 | as you wish. Sticker will take care of putting together your code, your API, and the framework you choose. 120 | 121 | .. code-block:: python 122 | 123 | def myhandler(params): 124 | return { 125 | "content": f"Hello {params.get("name", "World")}!", 126 | "status": 200 127 | } 128 | 129 | Writing tests for pure Python handles is easy and also 130 | free of boilerplate code. 131 | 132 | .. code-block:: python 133 | 134 | def test_myhandler(): 135 | params = { 136 | "name": "John Doe" 137 | } 138 | response = myhandler(params) 139 | assert response["content"] == "Hello John Doe!" 140 | 141 | As you could see in the example above, no imports from Sticker were necessary to define the API handler function. 142 | This is only possible because Sticker expects your handlers to follow a code convention. 143 | 144 | Anatomy Of An API Handler Function 145 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 146 | 147 | Write this part. 148 | 149 | Responses 150 | ^^^^^^^^^ 151 | 152 | API handlers are expected to return a Python dictionary (``dict``) object. The returned dictionary defines how a response 153 | will look like. All keys in the dictionary are optional. The expected keys are described in the table bellow. 154 | 155 | =========== ======================== =========== 156 | Key Type Description 157 | =========== ======================== =========== 158 | content str Body of HTTP request. No treatment/parsing of this value is done. The value is passed directly to the chosen framework. 159 | json Union[dict, List[dict]] JSON value to be used in the body of the request. This is a shortcut to having the header "Content-Type: application/json" and serializing this value using the most common way done by the chosen framework. 160 | file Union[IO[AnyStr], str] Data to be returned as byte stream. This is a shortcut for having the header "Content-Type: application/octet-stream". Uses the most common way to stream files with the chosen framework. 161 | redirect str The path or full URL to be redirected. This is a shortcut for having the header "Location:" with HTTP status `301`. 162 | status int The HTTP status code to be used in the response. This value overrides any shortcut default status code. 163 | headers Dict[str, str] The HTTP headers to be used in the response. This value is merged with the shortcut values with priority. 164 | =========== ======================== =========== 165 | 166 | 167 | We have exposed here some examples of using different configurations of the ``dict`` we've defined above to describe the 168 | HTTP response of API handlers. The actual HTTP response value generated will vary depending on the framework chosen as 169 | runtime. The examples are a minimal illustration of what to expect to be the HTTP response. 170 | 171 | The "content" key can be used when it's desired to return a "Hello world!" string with status ``200``. 172 | 173 | .. code-block:: python 174 | 175 | def say_hello(params): 176 | return {"content": "Hello world!"} 177 | 178 | Results in the HTTP response similar to: 179 | 180 | .. code-block:: 181 | 182 | HTTP/1.1 200 OK 183 | Content-Type: text/plain 184 | 185 | Hello world! 186 | 187 | The "json" key can be used when desired to return an JSON response with status ``201``. 188 | 189 | .. code-block:: python 190 | 191 | def create(params): 192 | data = { 193 | "id": "uhHuehuE", 194 | "value": "something" 195 | } 196 | return {"json": data, "status": 201} 197 | 198 | The HTTP response generated will be similar to: 199 | 200 | .. code-block:: 201 | 202 | HTTP/1.1 201 Created 203 | Content-Type: application/json 204 | 205 | {"id":"uhHuehuE","value":"something"} 206 | 207 | The "file" key is used to return file contents. 208 | 209 | .. code-block:: python 210 | 211 | def homepage(params): 212 | return { 213 | "file": open('templates/home.html', 'r'), 214 | "headers": { 215 | "Content-Type": "text/html" 216 | } 217 | } 218 | 219 | The HTTP response will be similar to: 220 | 221 | .. code-block:: 222 | 223 | HTTP/1.1 200 OK 224 | Content-Type: text/html 225 | 226 | My homepage

Welcome!

227 | 228 | When necessary to redirect request, the "redirect" key can be used. 229 | 230 | .. code-block:: python 231 | 232 | def old_endpoint(params): 233 | return {'redirect': '/new-path'} 234 | 235 | The HTTP response will be similar to: 236 | 237 | .. code-block:: 238 | 239 | HTTP/1.1 301 Moved Permanently 240 | Location: https://example.com/new-path 241 | 242 | The usage of keys "status" and "headers" were shown in the previous examples. The "status" and "headers" keys, when set, 243 | override the values set by default when using the shortcut keys ("json", "file", and "redirect"). 244 | 245 | Error Handling 246 | -------------- 247 | 248 | Sticker expects you to define the error format to be returned by your API. A error handler is configurable, 249 | and called every time validation for the endpoint fails. 250 | 251 | .. code-block:: python 252 | 253 | def error_handler(error): 254 | return { 255 | "content": { 256 | "error": error["message"] 257 | }, 258 | "headers": { 259 | "Content-Type": "application/json" 260 | }, 261 | "status_code": 400 262 | } 263 | 264 | Developing 265 | ========== 266 | 267 | We follow `Semantic Versioning `_. 268 | 269 | Contributing 270 | ============ 271 | 272 | Sticker is developed under the `Apache 2.0 license `_ 273 | and is publicly available to everyone. We are happy to accept contributions. 274 | 275 | How to Contribute 276 | ----------------- 277 | 278 | #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. There is a `Good First Issue`_ tag for issues that should be ideal for people who are not very familiar with the codebase yet. 279 | #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). 280 | #. Write a test which shows that the bug was fixed or that the feature works as expected. 281 | #. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS_. 282 | 283 | .. _`the repository`: https://github.com/rafaelcaricio/sticker 284 | .. _AUTHORS: https://github.com/rafaelcaricio/sticker/blob/master/AUTHORS.rst 285 | .. _Good First Issue: https://github.com/rafaelcaricio/sticker/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b7b23b58ec4ce3a72c00d3e9354dd53a34e68cced4cdff37443a726e4097dc34" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.python.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "aiofiles": { 18 | "hashes": [ 19 | "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27", 20 | "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092" 21 | ], 22 | "version": "==0.6.0" 23 | }, 24 | "aiohttp": { 25 | "hashes": [ 26 | "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", 27 | "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", 28 | "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", 29 | "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", 30 | "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", 31 | "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", 32 | "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", 33 | "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", 34 | "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", 35 | "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", 36 | "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", 37 | "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", 38 | "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", 39 | "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", 40 | "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", 41 | "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", 42 | "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", 43 | "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", 44 | "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", 45 | "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", 46 | "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", 47 | "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", 48 | "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", 49 | "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", 50 | "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", 51 | "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", 52 | "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", 53 | "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", 54 | "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", 55 | "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", 56 | "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", 57 | "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", 58 | "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", 59 | "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", 60 | "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", 61 | "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", 62 | "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" 63 | ], 64 | "index": "pypi", 65 | "version": "==3.7.4" 66 | }, 67 | "async-timeout": { 68 | "hashes": [ 69 | "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", 70 | "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" 71 | ], 72 | "version": "==3.0.1" 73 | }, 74 | "attrs": { 75 | "hashes": [ 76 | "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", 77 | "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" 78 | ], 79 | "version": "==20.3.0" 80 | }, 81 | "bottle": { 82 | "hashes": [ 83 | "sha256:39b751aee0b167be8dffb63ca81b735bbf1dd0905b3bc42761efedee8f123355" 84 | ], 85 | "index": "pypi", 86 | "version": "==0.12.13" 87 | }, 88 | "chardet": { 89 | "hashes": [ 90 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 91 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 92 | ], 93 | "version": "==3.0.4" 94 | }, 95 | "click": { 96 | "hashes": [ 97 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 98 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 99 | ], 100 | "version": "==7.1.2" 101 | }, 102 | "flask": { 103 | "hashes": [ 104 | "sha256:7fab1062d11dd0038434e790d18c5b9133fd9e6b7257d707c4578ccc1e38b67c", 105 | "sha256:b1883637bbee4dc7bc98d900792d0a304d609fce0f5bd9ca91d1b6457e5918dd" 106 | ], 107 | "index": "pypi", 108 | "version": "==1.0" 109 | }, 110 | "httptools": { 111 | "hashes": [ 112 | "sha256:01b392a166adcc8bc2f526a939a8aabf89fe079243e1543fd0e7dc1b58d737cb", 113 | "sha256:200fc1cdf733a9ff554c0bb97a4047785cfaad9875307d6087001db3eb2b417f", 114 | "sha256:3ab1f390d8867f74b3b5ee2a7ecc9b8d7f53750bd45714bf1cb72a953d7dfa77", 115 | "sha256:78d03dd39b09c99ec917d50189e6743adbfd18c15d5944392d2eabda688bf149", 116 | "sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5", 117 | "sha256:80ffa04fe8c8dfacf6e4cef8277347d35b0442c581f5814f3b0cf41b65c43c6e", 118 | "sha256:813871f961edea6cb2fe312f2d9b27d12a51ba92545380126f80d0de1917ea15", 119 | "sha256:94505026be56652d7a530ab03d89474dc6021019d6b8682281977163b3471ea0", 120 | "sha256:a23166e5ae2775709cf4f7ad4c2048755ebfb272767d244e1a96d55ac775cca7", 121 | "sha256:a289c27ccae399a70eacf32df9a44059ca2ba4ac444604b00a19a6c1f0809943", 122 | "sha256:a7594f9a010cdf1e16a58b3bf26c9da39bbf663e3b8d46d39176999d71816658", 123 | "sha256:b08d00d889a118f68f37f3c43e359aab24ee29eb2e3fe96d64c6a2ba8b9d6557", 124 | "sha256:cc9be041e428c10f8b6ab358c6b393648f9457094e1dcc11b4906026d43cd380", 125 | "sha256:d5682eeb10cca0606c4a8286a3391d4c3c5a36f0c448e71b8bd05be4e1694bfb", 126 | "sha256:fd3b8905e21431ad306eeaf56644a68fdd621bf8f3097eff54d0f6bdf7262065" 127 | ], 128 | "version": "==0.2.0" 129 | }, 130 | "idna": { 131 | "hashes": [ 132 | "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", 133 | "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" 134 | ], 135 | "version": "==3.1" 136 | }, 137 | "itsdangerous": { 138 | "hashes": [ 139 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 140 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 141 | ], 142 | "version": "==1.1.0" 143 | }, 144 | "jinja2": { 145 | "hashes": [ 146 | "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", 147 | "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" 148 | ], 149 | "version": "==2.11.3" 150 | }, 151 | "markupsafe": { 152 | "hashes": [ 153 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 154 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 155 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 156 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 157 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 158 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 159 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 160 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 161 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 162 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 163 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 164 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 165 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 166 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 167 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 168 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 169 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 170 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 171 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 172 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 173 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 174 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 175 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 176 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 177 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 178 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 179 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 180 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 181 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 182 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 183 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 184 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 185 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 186 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 187 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 188 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 189 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 190 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 191 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 192 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 193 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 194 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 195 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 196 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 197 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 198 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 199 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 200 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 201 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 202 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 203 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 204 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 205 | ], 206 | "version": "==1.1.1" 207 | }, 208 | "multidict": { 209 | "hashes": [ 210 | "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", 211 | "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", 212 | "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", 213 | "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", 214 | "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", 215 | "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", 216 | "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", 217 | "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", 218 | "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", 219 | "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", 220 | "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", 221 | "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", 222 | "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", 223 | "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", 224 | "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", 225 | "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", 226 | "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", 227 | "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", 228 | "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", 229 | "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", 230 | "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", 231 | "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", 232 | "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", 233 | "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", 234 | "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", 235 | "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", 236 | "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", 237 | "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", 238 | "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", 239 | "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", 240 | "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", 241 | "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", 242 | "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", 243 | "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", 244 | "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", 245 | "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", 246 | "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" 247 | ], 248 | "version": "==5.1.0" 249 | }, 250 | "pyyaml": { 251 | "hashes": [ 252 | "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", 253 | "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", 254 | "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", 255 | "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", 256 | "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", 257 | "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", 258 | "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", 259 | "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", 260 | "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", 261 | "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", 262 | "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" 263 | ], 264 | "index": "pypi", 265 | "version": "==5.1" 266 | }, 267 | "sanic": { 268 | "hashes": [ 269 | "sha256:18a3bd729093ac93a245849c44045c505a11e6d36da5bf231cb986bfb1e3c14c", 270 | "sha256:22b1a6f1dc55db8a136335cb0961afa95040ca78aa8c78425a40d91e8618e60e" 271 | ], 272 | "index": "pypi", 273 | "version": "==0.7.0" 274 | }, 275 | "tornado": { 276 | "hashes": [ 277 | "sha256:186ba4f280429a24120f329c7c08ea91818ff6bf47ed2ccb66f8f460698fc4ed", 278 | "sha256:3e9a2333362d3dad7876d902595b64aea1a2f91d0df13191ea1f8bca5a447771", 279 | "sha256:4d192236a9ffee54cb0032f22a8a0cfa64258872f1d83d71f3356681f69a37be", 280 | "sha256:69194436190b777abf0b631a692b0b29ba4157d18eeee07327b486e033b944dc", 281 | "sha256:b5bf7407f88327b80e666dabf91a1e7beb11236855a5c65ba5cf0e9e25ae296b" 282 | ], 283 | "index": "pypi", 284 | "version": "==5.0.1" 285 | }, 286 | "typing-extensions": { 287 | "hashes": [ 288 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 289 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 290 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 291 | ], 292 | "version": "==3.7.4.3" 293 | }, 294 | "ujson": { 295 | "hashes": [ 296 | "sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967", 297 | "sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1", 298 | "sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47", 299 | "sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720", 300 | "sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a", 301 | "sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108", 302 | "sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737", 303 | "sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394", 304 | "sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c", 305 | "sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c", 306 | "sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb", 307 | "sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939", 308 | "sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d", 309 | "sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99", 310 | "sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5", 311 | "sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa", 312 | "sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361", 313 | "sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e", 314 | "sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec", 315 | "sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c", 316 | "sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb" 317 | ], 318 | "version": "==4.0.2" 319 | }, 320 | "uvloop": { 321 | "hashes": [ 322 | "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc", 323 | "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69", 324 | "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01", 325 | "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d", 326 | "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760", 327 | "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c", 328 | "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47", 329 | "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c", 330 | "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c", 331 | "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7" 332 | ], 333 | "version": "==0.15.2" 334 | }, 335 | "websockets": { 336 | "hashes": [ 337 | "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", 338 | "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", 339 | "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", 340 | "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", 341 | "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", 342 | "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", 343 | "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", 344 | "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", 345 | "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", 346 | "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", 347 | "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", 348 | "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", 349 | "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", 350 | "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", 351 | "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", 352 | "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", 353 | "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", 354 | "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", 355 | "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", 356 | "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", 357 | "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", 358 | "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" 359 | ], 360 | "version": "==8.1" 361 | }, 362 | "werkzeug": { 363 | "hashes": [ 364 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 365 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 366 | ], 367 | "version": "==1.0.1" 368 | }, 369 | "yarl": { 370 | "hashes": [ 371 | "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", 372 | "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", 373 | "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", 374 | "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", 375 | "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", 376 | "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", 377 | "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", 378 | "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", 379 | "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", 380 | "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", 381 | "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", 382 | "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", 383 | "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", 384 | "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", 385 | "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", 386 | "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", 387 | "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", 388 | "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", 389 | "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", 390 | "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", 391 | "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", 392 | "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", 393 | "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", 394 | "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", 395 | "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", 396 | "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", 397 | "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", 398 | "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", 399 | "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", 400 | "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", 401 | "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", 402 | "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", 403 | "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", 404 | "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", 405 | "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", 406 | "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", 407 | "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" 408 | ], 409 | "version": "==1.6.3" 410 | } 411 | }, 412 | "develop": { 413 | "aiofiles": { 414 | "hashes": [ 415 | "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27", 416 | "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092" 417 | ], 418 | "version": "==0.6.0" 419 | }, 420 | "aiohttp": { 421 | "hashes": [ 422 | "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", 423 | "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", 424 | "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", 425 | "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", 426 | "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", 427 | "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", 428 | "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", 429 | "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", 430 | "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", 431 | "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", 432 | "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", 433 | "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", 434 | "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", 435 | "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", 436 | "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", 437 | "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", 438 | "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", 439 | "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", 440 | "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", 441 | "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", 442 | "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", 443 | "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", 444 | "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", 445 | "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", 446 | "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", 447 | "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", 448 | "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", 449 | "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", 450 | "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", 451 | "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", 452 | "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", 453 | "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", 454 | "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", 455 | "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", 456 | "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", 457 | "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", 458 | "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" 459 | ], 460 | "index": "pypi", 461 | "version": "==3.7.4" 462 | }, 463 | "async-timeout": { 464 | "hashes": [ 465 | "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", 466 | "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" 467 | ], 468 | "version": "==3.0.1" 469 | }, 470 | "attrs": { 471 | "hashes": [ 472 | "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", 473 | "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" 474 | ], 475 | "version": "==20.3.0" 476 | }, 477 | "beautifulsoup4": { 478 | "hashes": [ 479 | "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35", 480 | "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25", 481 | "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666" 482 | ], 483 | "version": "==4.9.3" 484 | }, 485 | "certifi": { 486 | "hashes": [ 487 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 488 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 489 | ], 490 | "version": "==2020.12.5" 491 | }, 492 | "chardet": { 493 | "hashes": [ 494 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 495 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 496 | ], 497 | "version": "==3.0.4" 498 | }, 499 | "decorator": { 500 | "hashes": [ 501 | "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060", 502 | "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98" 503 | ], 504 | "version": "==5.0.7" 505 | }, 506 | "docutils": { 507 | "hashes": [ 508 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 509 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", 510 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" 511 | ], 512 | "index": "pypi", 513 | "version": "==0.14" 514 | }, 515 | "httptools": { 516 | "hashes": [ 517 | "sha256:01b392a166adcc8bc2f526a939a8aabf89fe079243e1543fd0e7dc1b58d737cb", 518 | "sha256:200fc1cdf733a9ff554c0bb97a4047785cfaad9875307d6087001db3eb2b417f", 519 | "sha256:3ab1f390d8867f74b3b5ee2a7ecc9b8d7f53750bd45714bf1cb72a953d7dfa77", 520 | "sha256:78d03dd39b09c99ec917d50189e6743adbfd18c15d5944392d2eabda688bf149", 521 | "sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5", 522 | "sha256:80ffa04fe8c8dfacf6e4cef8277347d35b0442c581f5814f3b0cf41b65c43c6e", 523 | "sha256:813871f961edea6cb2fe312f2d9b27d12a51ba92545380126f80d0de1917ea15", 524 | "sha256:94505026be56652d7a530ab03d89474dc6021019d6b8682281977163b3471ea0", 525 | "sha256:a23166e5ae2775709cf4f7ad4c2048755ebfb272767d244e1a96d55ac775cca7", 526 | "sha256:a289c27ccae399a70eacf32df9a44059ca2ba4ac444604b00a19a6c1f0809943", 527 | "sha256:a7594f9a010cdf1e16a58b3bf26c9da39bbf663e3b8d46d39176999d71816658", 528 | "sha256:b08d00d889a118f68f37f3c43e359aab24ee29eb2e3fe96d64c6a2ba8b9d6557", 529 | "sha256:cc9be041e428c10f8b6ab358c6b393648f9457094e1dcc11b4906026d43cd380", 530 | "sha256:d5682eeb10cca0606c4a8286a3391d4c3c5a36f0c448e71b8bd05be4e1694bfb", 531 | "sha256:fd3b8905e21431ad306eeaf56644a68fdd621bf8f3097eff54d0f6bdf7262065" 532 | ], 533 | "version": "==0.2.0" 534 | }, 535 | "idna": { 536 | "hashes": [ 537 | "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", 538 | "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" 539 | ], 540 | "version": "==3.1" 541 | }, 542 | "ipython": { 543 | "hashes": [ 544 | "sha256:51c158a6c8b899898d1c91c6b51a34110196815cc905f9be0fa5878e19355608", 545 | "sha256:fcc6d46f08c3c4de7b15ae1c426e15be1b7932bcda9d83ce1a4304e8c1129df3" 546 | ], 547 | "index": "pypi", 548 | "version": "==6.2.1" 549 | }, 550 | "ipython-genutils": { 551 | "hashes": [ 552 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 553 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 554 | ], 555 | "version": "==0.2.0" 556 | }, 557 | "jedi": { 558 | "hashes": [ 559 | "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93", 560 | "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707" 561 | ], 562 | "version": "==0.18.0" 563 | }, 564 | "more-itertools": { 565 | "hashes": [ 566 | "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", 567 | "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" 568 | ], 569 | "version": "==8.7.0" 570 | }, 571 | "multidict": { 572 | "hashes": [ 573 | "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", 574 | "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", 575 | "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", 576 | "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", 577 | "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", 578 | "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", 579 | "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", 580 | "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", 581 | "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", 582 | "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", 583 | "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", 584 | "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", 585 | "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", 586 | "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", 587 | "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", 588 | "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", 589 | "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", 590 | "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", 591 | "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", 592 | "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", 593 | "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", 594 | "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", 595 | "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", 596 | "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", 597 | "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", 598 | "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", 599 | "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", 600 | "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", 601 | "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", 602 | "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", 603 | "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", 604 | "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", 605 | "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", 606 | "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", 607 | "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", 608 | "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", 609 | "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" 610 | ], 611 | "version": "==5.1.0" 612 | }, 613 | "parso": { 614 | "hashes": [ 615 | "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", 616 | "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22" 617 | ], 618 | "version": "==0.8.2" 619 | }, 620 | "pexpect": { 621 | "hashes": [ 622 | "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", 623 | "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" 624 | ], 625 | "markers": "sys_platform != 'win32'", 626 | "version": "==4.8.0" 627 | }, 628 | "pickleshare": { 629 | "hashes": [ 630 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 631 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 632 | ], 633 | "version": "==0.7.5" 634 | }, 635 | "pkginfo": { 636 | "hashes": [ 637 | "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", 638 | "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" 639 | ], 640 | "version": "==1.7.0" 641 | }, 642 | "pluggy": { 643 | "hashes": [ 644 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", 645 | "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", 646 | "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" 647 | ], 648 | "version": "==0.6.0" 649 | }, 650 | "prompt-toolkit": { 651 | "hashes": [ 652 | "sha256:37925b37a4af1f6448c76b7606e0285f79f434ad246dda007a27411cca730c6d", 653 | "sha256:dd4fca02c8069497ad931a2d09914c6b0d1b50151ce876bc15bde4c747090126", 654 | "sha256:f7eec66105baf40eda9ab026cd8b2e251337eea8d111196695d82e0c5f0af852" 655 | ], 656 | "version": "==1.0.18" 657 | }, 658 | "ptyprocess": { 659 | "hashes": [ 660 | "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", 661 | "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" 662 | ], 663 | "version": "==0.7.0" 664 | }, 665 | "py": { 666 | "hashes": [ 667 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 668 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 669 | ], 670 | "version": "==1.10.0" 671 | }, 672 | "pygments": { 673 | "hashes": [ 674 | "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", 675 | "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" 676 | ], 677 | "version": "==2.8.1" 678 | }, 679 | "pytest": { 680 | "hashes": [ 681 | "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", 682 | "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1" 683 | ], 684 | "index": "pypi", 685 | "version": "==3.5.0" 686 | }, 687 | "pytest-sanic": { 688 | "hashes": [ 689 | "sha256:1bcd85fe5d7f9107dbab374a818a92035ee00e0fa60aca0d841253928dd7c4d4", 690 | "sha256:f3667d4377f856780630fa5e23be29efd9df3fbff5a07d3467529dc02c8f93c3" 691 | ], 692 | "index": "pypi", 693 | "version": "==0.1.9" 694 | }, 695 | "requests": { 696 | "hashes": [ 697 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 698 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 699 | ], 700 | "version": "==2.25.1" 701 | }, 702 | "requests-toolbelt": { 703 | "hashes": [ 704 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 705 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 706 | ], 707 | "version": "==0.9.1" 708 | }, 709 | "restructuredtext-lint": { 710 | "hashes": [ 711 | "sha256:c48ca9a84c312b262809f041fe47dcfaedc9ee4879b3e1f9532f745c182b4037" 712 | ], 713 | "index": "pypi", 714 | "version": "==1.1.3" 715 | }, 716 | "sanic": { 717 | "hashes": [ 718 | "sha256:18a3bd729093ac93a245849c44045c505a11e6d36da5bf231cb986bfb1e3c14c", 719 | "sha256:22b1a6f1dc55db8a136335cb0961afa95040ca78aa8c78425a40d91e8618e60e" 720 | ], 721 | "index": "pypi", 722 | "version": "==0.7.0" 723 | }, 724 | "sanic-routing": { 725 | "hashes": [ 726 | "sha256:73110d30c5d33566abd2b05774c2f862499b64c392b8c34b2907af7b12c62baa", 727 | "sha256:a96bdb5592cca3d6b2e07f325c0bc50b2467f9519a6e60ce6cf21f967ee00970" 728 | ], 729 | "version": "==0.6.2" 730 | }, 731 | "simplegeneric": { 732 | "hashes": [ 733 | "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" 734 | ], 735 | "version": "==0.8.1" 736 | }, 737 | "six": { 738 | "hashes": [ 739 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 740 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 741 | ], 742 | "version": "==1.15.0" 743 | }, 744 | "soupsieve": { 745 | "hashes": [ 746 | "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc", 747 | "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b" 748 | ], 749 | "markers": "python_version >= '3.0'", 750 | "version": "==2.2.1" 751 | }, 752 | "tqdm": { 753 | "hashes": [ 754 | "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", 755 | "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" 756 | ], 757 | "version": "==4.60.0" 758 | }, 759 | "traitlets": { 760 | "hashes": [ 761 | "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", 762 | "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" 763 | ], 764 | "version": "==5.0.5" 765 | }, 766 | "twine": { 767 | "hashes": [ 768 | "sha256:08eb132bbaec40c6d25b358f546ec1dc96ebd2638a86eea68769d9e67fe2b129", 769 | "sha256:2fd9a4d9ff0bcacf41fdc40c8cb0cfaef1f1859457c9653fd1b92237cc4e9f25" 770 | ], 771 | "index": "pypi", 772 | "version": "==1.11.0" 773 | }, 774 | "typing-extensions": { 775 | "hashes": [ 776 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 777 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 778 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 779 | ], 780 | "version": "==3.7.4.3" 781 | }, 782 | "ujson": { 783 | "hashes": [ 784 | "sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967", 785 | "sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1", 786 | "sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47", 787 | "sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720", 788 | "sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a", 789 | "sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108", 790 | "sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737", 791 | "sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394", 792 | "sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c", 793 | "sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c", 794 | "sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb", 795 | "sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939", 796 | "sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d", 797 | "sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99", 798 | "sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5", 799 | "sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa", 800 | "sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361", 801 | "sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e", 802 | "sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec", 803 | "sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c", 804 | "sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb" 805 | ], 806 | "version": "==4.0.2" 807 | }, 808 | "urllib3": { 809 | "hashes": [ 810 | "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", 811 | "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" 812 | ], 813 | "version": "==1.26.4" 814 | }, 815 | "uvloop": { 816 | "hashes": [ 817 | "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc", 818 | "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69", 819 | "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01", 820 | "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d", 821 | "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760", 822 | "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c", 823 | "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47", 824 | "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c", 825 | "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c", 826 | "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7" 827 | ], 828 | "version": "==0.15.2" 829 | }, 830 | "waitress": { 831 | "hashes": [ 832 | "sha256:29af5a53e9fb4e158f525367678b50053808ca6c21ba585754c77d790008c746", 833 | "sha256:69e1f242c7f80273490d3403c3976f3ac3b26e289856936d1f620ed48f321897" 834 | ], 835 | "version": "==2.0.0" 836 | }, 837 | "wcwidth": { 838 | "hashes": [ 839 | "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", 840 | "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" 841 | ], 842 | "version": "==0.2.5" 843 | }, 844 | "webob": { 845 | "hashes": [ 846 | "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b", 847 | "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323" 848 | ], 849 | "version": "==1.8.7" 850 | }, 851 | "websockets": { 852 | "hashes": [ 853 | "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", 854 | "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", 855 | "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", 856 | "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", 857 | "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", 858 | "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", 859 | "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", 860 | "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", 861 | "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", 862 | "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", 863 | "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", 864 | "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", 865 | "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", 866 | "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", 867 | "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", 868 | "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", 869 | "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", 870 | "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", 871 | "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", 872 | "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", 873 | "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", 874 | "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" 875 | ], 876 | "version": "==8.1" 877 | }, 878 | "webtest": { 879 | "hashes": [ 880 | "sha256:9136514159a2e76a21751bf4ab5d3371e539c8ada8b950fcf68e307d9e584a07", 881 | "sha256:dbbccc15ac2465066c95dc3a7de0d30cde3791e886ccbd7e91d5d2a2580c922d" 882 | ], 883 | "index": "pypi", 884 | "version": "==2.0.29" 885 | }, 886 | "yarl": { 887 | "hashes": [ 888 | "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", 889 | "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", 890 | "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", 891 | "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", 892 | "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", 893 | "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", 894 | "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", 895 | "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", 896 | "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", 897 | "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", 898 | "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", 899 | "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", 900 | "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", 901 | "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", 902 | "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", 903 | "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", 904 | "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", 905 | "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", 906 | "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", 907 | "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", 908 | "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", 909 | "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", 910 | "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", 911 | "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", 912 | "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", 913 | "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", 914 | "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", 915 | "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", 916 | "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", 917 | "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", 918 | "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", 919 | "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", 920 | "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", 921 | "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", 922 | "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", 923 | "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", 924 | "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" 925 | ], 926 | "version": "==1.6.3" 927 | } 928 | } 929 | } 930 | --------------------------------------------------------------------------------