From ae9fb7ba76b62c3f5fa91b1e1466b6b6266a1c77 Mon Sep 17 00:00:00 2001 From: Yifan Zhang Date: Sun, 4 Jul 2021 11:35:13 +0300 Subject: [PATCH] automatic documentation for application apis --- Dockerfile | 4 +- Makefile | 19 ++++ README.rst | 56 ++++++---- apihub/cli.py | 2 +- apihub/result.py | 15 +-- apihub/server.py | 115 ++++++++++++++++++-- apihub/utils.py | 6 +- images/APIHub-logo.png | Bin 0 -> 20425 bytes poetry.lock | 186 +++++++++++++++++++++++--------- pyproject.toml | 3 +- tests/fixtures/result_input.txt | 1 + 11 files changed, 315 insertions(+), 92 deletions(-) create mode 100644 images/APIHub-logo.png diff --git a/Dockerfile b/Dockerfile index 3bf5999..1d0f9b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN apt-get update && apt-get install -y git #RUN pip install apihub COPY . /code -RUN pip install /code/dist/apihub-0.1.0-py3-none-any.whl +RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - # expose port for prometheus EXPOSE 8000 @@ -14,4 +14,4 @@ EXPOSE 5000 ENV PORT 5000 -CMD ["apihub_server"] +CMD ["poetry", "apihub_server"] diff --git a/Makefile b/Makefile index ea3d8f7..cb7ac8a 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,25 @@ pdb: mypy: poetry run mypy --ignore-missing-imports apihub +.PHONY: result +result: + PYTHONPATH=../pipeline/src poetry run python apihub/result.py \ + --in-kind FILE --in-filename tests/fixtures/result_input.txt \ + --out-kind FILE --out-filename - \ + --debug + +.PHONY: redis-result +redis-result: + PYTHONPATH=../pipeline/src poetry run python apihub/result.py \ + --in-kind LREDIS --in-redis redis://localhost:6379/1 \ + --in-topic result --in-namespace apihub \ + --debug + +.PHONY: server +server: + PYTHONPATH=../pipeline/src poetry run python apihub/server.py \ + --out-kind LREDIS --debug + .PHONY: pre-commit pre-commit: pre-commit run --all-files diff --git a/README.rst b/README.rst index f5abd29..1134802 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@

- Logo + Logo

APIHub

@@ -89,32 +89,26 @@ About The Project `[Product Name Screen Shot][product-screenshot] `__ -Here’s a blank template to get started: **To avoid retyping too much -info. Do a search and replace with your text editor for the following:** -``yifan``, ``apihub``, ``yifan2019``, ``email``, ``APIHub``, -``project_description`` + Features & TODOs ---------------- -:: - - [X] Security - [X] authenticate - [X] admin, manager, user - [X] user management - [X] rate limiter - [ ] register - [ ] social login - [ ] Subscription - [-] subscription - [-] quota - [X] application token - [-] daily usage record in redis - [ ] Async/sync API calls - [ ] api worker reports input/output: describe - [X] generic worker deployment - [ ] auto scaler for api workers +- |check| Security + |check| authenticate + |check| admin, manager, user + |check| user management + - |check| rate limiter + - |check| register + - |uncheck_| social login +- |check| Subscription + - |check| subscription + - |check| application token + - |check| daily usage record in redis +- |uncheck| Async/sync API calls + - |check| api worker reports input/output: describe + - |check| generic worker deployment + - |uncheck| auto scaler for api workers Built With ---------- @@ -276,6 +270,22 @@ Copyright (C) 2021, Qatar Computing Research Institute, HBKU +.. |check| raw:: html + + + +.. |check_| raw:: html + + + +.. |uncheck| raw:: html + + + +.. |uncheck_| raw:: html + + + .. |Contributors| image:: https://img.shields.io/github/contributors/yifan/apihub.svg?style=for-the-badge :target: https://github.com/yifan/apihub/graphs/contributors diff --git a/apihub/cli.py b/apihub/cli.py index 275a966..4be71a9 100644 --- a/apihub/cli.py +++ b/apihub/cli.py @@ -91,7 +91,7 @@ def create_subscription(admin, limit, days, recurring, username, application): "username": username, "application": application, "starts_at": datetime.now(), - "limit": limit, + "credit": limit, "expires_at": expires_at, "recurring": recurring, } diff --git a/apihub/result.py b/apihub/result.py index 8544893..694a65e 100644 --- a/apihub/result.py +++ b/apihub/result.py @@ -1,10 +1,8 @@ -import json - import redis from prometheus_client import Counter, Histogram from dotenv import load_dotenv -from pipeline import ProcessorSettings, Processor, Command, CommandActions +from pipeline import ProcessorSettings, Processor, Command, CommandActions, Definition from apihub.utils import Result, Status, RedisSettings, DEFINITION from apihub import __worker__, __version__ @@ -44,12 +42,17 @@ def setup(self) -> None: self.redis = redis.Redis.from_url(settings.redis) def process_command(self, command: Command) -> None: + self.logger.info("Processing COMMAND") if command.action == CommandActions.Define: - for k, v in command.content.items(): - self.redis.hset(DEFINITION, k, json.dumps(v)) - self.logger.info(f"{k} definition:\n{json.dumps(v, indent=2)}") + definition = Definition.parse_obj(command.content) + self.logger.info(definition) + self.redis.hset(DEFINITION, definition.source.topic, definition.json()) + self.logger.info( + f"{definition.source.topic} definition:\n{definition.json()}" + ) def process(self, message_content, message_id): + self.logger.info("Processing MESSAGE") result = Result.parse_obj(message_content) if result.status == Status.PROCESSED: result.result = { diff --git a/apihub/server.py b/apihub/server.py index 26e3e68..2cf7c60 100644 --- a/apihub/server.py +++ b/apihub/server.py @@ -1,5 +1,6 @@ import sys import functools +import logging from typing import Dict, Any from fastapi import FastAPI, HTTPException, Request, Query, Depends @@ -7,14 +8,15 @@ from pydantic import BaseModel, Field from fastapi_jwt_auth import AuthJWT from fastapi_jwt_auth.exceptions import AuthJWTException +from fastapi.openapi.utils import get_openapi from dotenv import load_dotenv -from pipeline import Message, Settings, Command, CommandActions, Monitor +from pipeline import Message, Settings, Command, CommandActions, Monitor, Definition from apihub_users.security.depends import RateLimiter, RateLimits, require_user from apihub_users.security.router import router as security_router from apihub_users.subscription.depends import require_subscription from apihub_users.subscription.router import router as application_router -from apihub.utils import State, make_topic, make_key, Result, Status +from apihub.utils import DEFINITION, State, make_topic, make_key, Result, Status from apihub import __worker__, __version__ @@ -30,7 +32,8 @@ @functools.lru_cache(maxsize=None) def get_state(): - return State() + logging.basicConfig(level=logging.DEBUG) + return State(logger=logging) def get_redis(): @@ -60,10 +63,12 @@ def jwt_get_config(): api.include_router( security_router, # prefix='/security', - # tags=['security'], + tags=["security"], dependencies=[Depends(ip_rate_limited)], ) -api.include_router(application_router, dependencies=[Depends(ip_rate_limited)]) +api.include_router( + application_router, tags=["subscription"], dependencies=[Depends(ip_rate_limited)] +) @api.exception_handler(AuthJWTException) @@ -91,6 +96,7 @@ async def root(): @api.get( "/define/{application}", + include_in_schema=False, dependencies=[Depends(ip_rate_limited)], ) async def define_service( @@ -101,9 +107,12 @@ async def define_service( get_state().write(make_topic(application), Command(action=CommandActions.Define)) + return {"define": f"application {application}"} + @api.post( "/async/{application}", + include_in_schema=False, response_model=AsyncAPIRequestResponse, dependencies=[Depends(ip_rate_limited)], ) @@ -144,7 +153,11 @@ async def async_service( return AsyncAPIRequestResponse(success=True, key=key) -@api.get("/async/{application}", dependencies=[Depends(ip_rate_limited)]) +@api.get( + "/async/{application}", + dependencies=[Depends(ip_rate_limited)], + include_in_schema=False, +) async def async_service_result( application: str, key: str = Query( @@ -194,13 +207,101 @@ async def async_service_result( ) -@api.post("/sync/{service_name}") +@api.post( + "/sync/{service_name}", + include_in_schema=False, +) async def sync_service(service_name: str): """generic synchronised api hendler""" # TODO synchronised service, basically it will wait and return results # when it is ready. It will have a timeout of 30 seconds +def get_paths(redis=get_redis()): + paths = {} + for name, dct_str in redis.hgetall(DEFINITION).items(): + name = name.decode("utf-8") + path = {} + definition = Definition.parse_raw(dct_str) + operation = {} + operation["tags"] = ["app"] + operation["summary"] = definition.description + operation["description"] = definition.description + parameters = {} + operation["requestBody"] = { + "content": { + "application/json": { + "schema": definition.input_schema, # ["properties"], + } + }, + "required": True, + } + operation["responses"] = { + "200": { + "description": "successful request", + "status_code": 200, + "content": { + "application/json": {"schema": AsyncAPIRequestResponse.schema()} + }, + } + } + path["post"] = operation + + operation = {} + operation["tags"] = ["app"] + operation["summary"] = "obtain results from previous post requests" + operation["description"] = definition.description + parameters = [ + { + "name": "key", + "in": "query", + "description": "the unique key obtained from post request", + "required": True, + "type": "string", + "format": "string", + } + ] + # a strange way to sort parameters maybe + operation["parameters"] = list( + {param["name"]: param for param in parameters}.values() + ) + operation["responses"] = { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": definition.output_schema, + } + }, + } + } + path["get"] = operation + + paths[f"/async/{name}"] = path + return paths + + +def custom_openapi(app=api): + # if app.openapi_schema: + # return app.openapi_schema + + openapi_schema = get_openapi( + title="APIHub", + version="0.1.0", + description="API for AI", + routes=app.routes, + ) + openapi_schema["info"]["x-logo"] = { + "url": "https://raw.githubusercontent.com/yifan/apihub/master/images/APIHub-logo.png" + } + openapi_schema["paths"].update(get_paths()) + app.openapi_schema = openapi_schema + return app.openapi_schema + + +api.openapi = custom_openapi + + class ServerSettings(Settings): port: int = 5000 log_level: str = "debug" diff --git a/apihub/utils.py b/apihub/utils.py index b406510..888d722 100644 --- a/apihub/utils.py +++ b/apihub/utils.py @@ -30,8 +30,8 @@ class RedisSettings(Settings): class State: - def __init__(self): - self.pipeline = Pipeline() + def __init__(self, logger): + self.pipeline = Pipeline(logger=logger) settings = RedisSettings() settings.parse_args(args=[]) self.redis = redis.Redis.from_url(settings.redis) @@ -49,4 +49,4 @@ def make_key(): def make_topic(service_name: str): - return f"api-{service_name}" + return service_name diff --git a/images/APIHub-logo.png b/images/APIHub-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b39d7640fbd9bd06cedf554ffd82a46e89155b5a GIT binary patch literal 20425 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 literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index b49e06a..5c4577b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "apihub-users" -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.5" description = "user and subscription management for APIHub" category = "main" optional = false @@ -21,8 +21,8 @@ uvicorn = "^0.13.4" [package.source] type = "git" url = "https://github.com/yifan/apihub-users" -reference = "master" -resolved_reference = "2f0995453f1038bc3802fd04e1c8bf76a9f00145" +reference = "develop" +resolved_reference = "0e103098cf2d4fee3113c6108b07d9387e08de4e" [[package]] name = "appdirs" @@ -54,6 +54,30 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +[[package]] +name = "black" +version = "21.6b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.8.1,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] +python2 = ["typed-ast (>=1.4.2)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2021.5.30" @@ -224,7 +248,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.5.0" +version = "4.6.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -236,7 +260,8 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "more-itertools" @@ -289,9 +314,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pipeline" -version = "0.11.9" +version = "0.11.16" description = "data streaming on top of popular message queues" category = "main" optional = false @@ -314,7 +347,7 @@ pulsar = ["pulsar-client (>=2.5.0,<2.6.0)"] type = "git" url = "https://github.com/yifan/pipeline" reference = "develop" -resolved_reference = "191e3aec75ed5005b83b7da0348e4318002f3f4b" +resolved_reference = "9ec050a25a07807e03eb55189792892d4dcd4fc8" [[package]] name = "pluggy" @@ -520,6 +553,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] hiredis = ["hiredis (>=0.1.3)"] +[[package]] +name = "regex" +version = "2021.7.1" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "requests" version = "2.25.1" @@ -548,7 +589,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "sqlalchemy" -version = "1.4.19" +version = "1.4.20" description = "Database Abstraction Library" category = "main" optional = false @@ -580,7 +621,7 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "sqlalchemy-utils" -version = "0.37.7" +version = "0.37.8" description = "Various utility functions for SQLAlchemy." category = "main" optional = false @@ -600,8 +641,8 @@ intervals = ["intervals (>=0.7.1)"] password = ["passlib (>=1.6,<2.0)"] pendulum = ["pendulum (>=2.0.5)"] phone = ["phonenumbers (>=5.9.2)"] -test = ["pytest (>=2.7.1)", "Pygments (>=1.2)", "Jinja2 (>=2.3)", "docutils (>=0.10)", "flexmock (>=0.9.7)", "mock (==2.0.0)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pg8000 (>=1.12.4)", "pytz (>=2014.2)", "python-dateutil (>=2.6)", "pymysql", "flake8 (>=2.4.0)", "isort (>=4.2.2)", "pyodbc"] -test_all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "anyjson (>=0.3.3)", "arrow (>=0.3.4)", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "mock (==2.0.0)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test = ["pytest (>=2.7.1)", "Pygments (>=1.2)", "Jinja2 (>=2.3)", "docutils (>=0.10)", "flexmock (>=0.9.7)", "mock (==2.0.0)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pg8000 (>=1.12.4)", "pytz (>=2014.2)", "python-dateutil (>=2.6)", "pymysql", "flake8 (>=2.4.0)", "isort (>=4.2.2)", "pyodbc", "backports.zoneinfo"] +test_all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "anyjson (>=0.3.3)", "arrow (>=0.3.4)", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "mock (==2.0.0)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)", "backports.zoneinfo"] timezone = ["python-dateutil"] url = ["furl (>=0.4.1)"] @@ -650,7 +691,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.5" +version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -733,7 +774,7 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "443daa3b628e392109c343015bd2ec5cb1ffb0823fa4664fc16950e1b57fdf90" +content-hash = "a137b5fb25218585d068a77ea577d9def8be04e18927b2183687de18f85367af" [metadata.files] apihub-users = [] @@ -749,6 +790,10 @@ attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] +black = [ + {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, + {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, +] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, @@ -908,8 +953,8 @@ idna = [ {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, - {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, + {file = "importlib_metadata-4.6.0-py3-none-any.whl", hash = "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33"}, + {file = "importlib_metadata-4.6.0.tar.gz", hash = "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb"}, ] more-itertools = [ {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"}, @@ -951,6 +996,10 @@ packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] pipeline = [] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -1092,6 +1141,45 @@ redis = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] +regex = [ + {file = "regex-2021.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:494d0172774dc0beeea984b94c95389143db029575f7ca908edd74469321ea99"}, + {file = "regex-2021.7.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:8cf6728f89b071bd3ab37cb8a0e306f4de897553a0ed07442015ee65fbf53d62"}, + {file = "regex-2021.7.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1806370b2bef4d4193eebe8ee59a9fd7547836a34917b7badbe6561a8594d9cb"}, + {file = "regex-2021.7.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d0cf2651a8804f6325747c7e55e3be0f90ee2848e25d6b817aa2728d263f9abb"}, + {file = "regex-2021.7.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:268fe9dd1deb4a30c8593cabd63f7a241dfdc5bd9dd0233906c718db22cdd49a"}, + {file = "regex-2021.7.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:7743798dfb573d006f1143d745bf17efad39775a5190b347da5d83079646be56"}, + {file = "regex-2021.7.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0e46c1191b2eb293a6912269ed08b4512e7e241bbf591f97e527492e04c77e93"}, + {file = "regex-2021.7.1-cp36-cp36m-win32.whl", hash = "sha256:b1dbeef938281f240347d50f28ae53c4b046a23389cd1fc4acec5ea0eae646a1"}, + {file = "regex-2021.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6c72ebb72e64e9bd195cb35a9b9bbfb955fd953b295255b8ae3e4ad4a146b615"}, + {file = "regex-2021.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf819c5b77ff44accc9a24e31f1f7ceaaf6c960816913ed3ef8443b9d20d81b6"}, + {file = "regex-2021.7.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e80d2851109e56420b71f9702ad1646e2f0364528adbf6af85527bc61e49f394"}, + {file = "regex-2021.7.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a1b6a3f600d6aff97e3f28c34192c9ed93fee293bd96ef327b64adb51a74b2f6"}, + {file = "regex-2021.7.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ed77b97896312bc2deafe137ca2626e8b63808f5bedb944f73665c68093688a7"}, + {file = "regex-2021.7.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a548bb51c4476332ce4139df8e637386730f79a92652a907d12c696b6252b64d"}, + {file = "regex-2021.7.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:210c359e6ee5b83f7d8c529ba3c75ba405481d50f35a420609b0db827e2e3bb5"}, + {file = "regex-2021.7.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:1d386402ae7f3c9b107ae5863f7ecccb0167762c82a687ae6526b040feaa5ac6"}, + {file = "regex-2021.7.1-cp37-cp37m-win32.whl", hash = "sha256:5049d00dbb78f9d166d1c704e93934d42cce0570842bb1a61695123d6b01de09"}, + {file = "regex-2021.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:361be4d311ac995a8c7ad577025a3ae3a538531b1f2cf32efd8b7e5d33a13e5a"}, + {file = "regex-2021.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f32f47fb22c988c0b35756024b61d156e5c4011cb8004aa53d93b03323c45657"}, + {file = "regex-2021.7.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b024ee43ee6b310fad5acaee23e6485b21468718cb792a9d1693eecacc3f0b7e"}, + {file = "regex-2021.7.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b092754c06852e8a8b022004aff56c24b06310189186805800d09313c37ce1f8"}, + {file = "regex-2021.7.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a8a5826d8a1b64e2ff9af488cc179e1a4d0f144d11ce486a9f34ea38ccedf4ef"}, + {file = "regex-2021.7.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:444723ebaeb7fa8125f29c01a31101a3854ac3de293e317944022ae5effa53a4"}, + {file = "regex-2021.7.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:fdad3122b69cdabdb3da4c2a4107875913ac78dab0117fc73f988ad589c66b66"}, + {file = "regex-2021.7.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4b1999ef60c45357598935c12508abf56edbbb9c380df6f336de38a6c3a294ae"}, + {file = "regex-2021.7.1-cp38-cp38-win32.whl", hash = "sha256:e07e92935040c67f49571779d115ecb3e727016d42fb36ee0d8757db4ca12ee0"}, + {file = "regex-2021.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:6b8b629f93246e507287ee07e26744beaffb4c56ed520576deac8b615bd76012"}, + {file = "regex-2021.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56bef6b414949e2c9acf96cb5d78de8b529c7b99752619494e78dc76f99fd005"}, + {file = "regex-2021.7.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:78a2a885345a2d60b5e68099e877757d5ed12e46ba1e87507175f14f80892af3"}, + {file = "regex-2021.7.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3f7a92e60930f8fca2623d9e326c173b7cf2c8b7e4fdcf984b75a1d2fb08114d"}, + {file = "regex-2021.7.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4fc86b729ab88fe8ac3ec92287df253c64aa71560d76da5acd8a2e245839c629"}, + {file = "regex-2021.7.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:59845101de68fd5d3a1145df9ea022e85ecd1b49300ea68307ad4302320f6f61"}, + {file = "regex-2021.7.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ce269e903b00d1ab4746793e9c50a57eec5d5388681abef074d7b9a65748fca5"}, + {file = "regex-2021.7.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c11f2fca544b5e30a0e813023196a63b1cb9869106ef9a26e9dae28bce3e4e26"}, + {file = "regex-2021.7.1-cp39-cp39-win32.whl", hash = "sha256:1ccbd41dbee3a31e18938096510b7d4ee53aa9fce2ee3dcc8ec82ae264f6acfd"}, + {file = "regex-2021.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:18040755606b0c21281493ec309214bd61e41a170509e5014f41d6a5a586e161"}, + {file = "regex-2021.7.1.tar.gz", hash = "sha256:849802379a660206277675aa5a5c327f5c910c690649535863ddf329b0ba8c87"}, +] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, @@ -1101,40 +1189,40 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.4.19-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:ddbce8fe4d0190db21db602e38aaf4c158c540b49f1ef7475323ec682a9fbf2d"}, - {file = "SQLAlchemy-1.4.19-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:942ca49b7ec7449d2473a6587825c55ad99534ddfc4eee249dd42be3cc1aa8c9"}, - {file = "SQLAlchemy-1.4.19-cp27-cp27m-win32.whl", hash = "sha256:9c0945c79cbe507b49524e31a4bb8700060bbccb60bb553df6432e176baff3d5"}, - {file = "SQLAlchemy-1.4.19-cp27-cp27m-win_amd64.whl", hash = "sha256:6fd1b745ade2020a1a7bf1e22536d8afe86287882c81ca5d860bdf231d5854e9"}, - {file = "SQLAlchemy-1.4.19-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0fb3f73e5009f5a4c9b24469939d3d57cc3ad8099a09c0cfefc47fe45ab7ffbe"}, - {file = "SQLAlchemy-1.4.19-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:64eab458619ef759f16f0f82242813d3289e829f8557fbc7c212ca4eadf96472"}, - {file = "SQLAlchemy-1.4.19-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:311051c06f905774427b4a92dcb3924d6ee563dea3a88176da02fdfc572d0d1d"}, - {file = "SQLAlchemy-1.4.19-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a34a7fd3353ee61a1dca72fc0c3e38d4e56bdc2c343e712f60a8c70acd4ef5bf"}, - {file = "SQLAlchemy-1.4.19-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ace9ab2af9d7d7b0e2ff2178809941c56ab8921e38128278192a73a8a1c08a2"}, - {file = "SQLAlchemy-1.4.19-cp36-cp36m-win32.whl", hash = "sha256:96d3d4a7ead376d738775a1fa9786dc17a31975ec664cea284e53735c79a5686"}, - {file = "SQLAlchemy-1.4.19-cp36-cp36m-win_amd64.whl", hash = "sha256:20f4bf1459548a74aade997cb045015e4d72f0fde1789b09b3bb380be28f6511"}, - {file = "SQLAlchemy-1.4.19-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:8cba69545246d16c6d2a12ce45865947cbdd814bacddf2e532fdd4512e70728c"}, - {file = "SQLAlchemy-1.4.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ba8a96b6d058c7dcf44de8ac0955b7a787f7177a0221dd4b8016e0191268f5"}, - {file = "SQLAlchemy-1.4.19-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8f1e7f4de05c15d6b46af12f3cf0c2552f2940d201a49926703249a62402d851"}, - {file = "SQLAlchemy-1.4.19-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c92d9ebf4b38c22c0c9e4f203a80e101910a50dc555b4578816932015b97d7f"}, - {file = "SQLAlchemy-1.4.19-cp37-cp37m-win32.whl", hash = "sha256:c6efc7477551ba9ce632d5c3b448b7de0277c86005eec190a1068fcc7115fd0e"}, - {file = "SQLAlchemy-1.4.19-cp37-cp37m-win_amd64.whl", hash = "sha256:e2761b925fda550debfd5a8bc3cef9debc9a23c6a280429c4ec3a07c35c6b4b3"}, - {file = "SQLAlchemy-1.4.19-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:58d4f79d119010fdced6e7fd7e4b9f2230dbf55a8235d7c58b1c8207ef74791b"}, - {file = "SQLAlchemy-1.4.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cefd44faca7c57534503261f6fab49bd47eb9c2945ee0bab09faaa8cb047c24f"}, - {file = "SQLAlchemy-1.4.19-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9133635edcec1e7fbfc16eba5dc2b5b3b11818d25b7a57cfcbfa8d3b3e9594fd"}, - {file = "SQLAlchemy-1.4.19-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3cf5f543d048a7c8da500133068c5c90c97a2c4bf0c027928a85028a519f33d"}, - {file = "SQLAlchemy-1.4.19-cp38-cp38-win32.whl", hash = "sha256:d04160462f874eaa4d88721a0d5ecca8ebf433616801efe779f252ef87b0e216"}, - {file = "SQLAlchemy-1.4.19-cp38-cp38-win_amd64.whl", hash = "sha256:45b0f773e195d8d51e2fd67cb5b5fb32f5a1f5e7f0752016207091bed108909a"}, - {file = "SQLAlchemy-1.4.19-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:93ba458b3c279581288a10a55df2aa6ac3509882228fcbad9d9d88069f899337"}, - {file = "SQLAlchemy-1.4.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6317701c06a829b066c794545512bb70b1a10a74574cfa5658a0aaf49f31aa93"}, - {file = "SQLAlchemy-1.4.19-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:95a9fd0a11f89a80d8815418eccba034f3fec8ea1f04c41b6b8decc5c95852e9"}, - {file = "SQLAlchemy-1.4.19-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9014fd1d8aebcb4eb6bc69a382dd149200e1d5924412b1d08b4443f6c1ce526f"}, - {file = "SQLAlchemy-1.4.19-cp39-cp39-win32.whl", hash = "sha256:fa05a77662c23226c9ec031638fd90ae767009e05cd092b948740f09d10645f0"}, - {file = "SQLAlchemy-1.4.19-cp39-cp39-win_amd64.whl", hash = "sha256:d7b21a4b62921cf6dca97e8f9dea1fbe2432aebbb09895a2bd4f527105af41a4"}, - {file = "SQLAlchemy-1.4.19.tar.gz", hash = "sha256:89a5a13dcf33b7e47c7a9404a297c836965a247c7f076a0fe0910cae2bee5ce2"}, + {file = "SQLAlchemy-1.4.20-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:525dd3c2205b11a2bc6d770bf1ec63bde0253fd754b4c19c399d27ddc9dad0d3"}, + {file = "SQLAlchemy-1.4.20-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a67371752fd86d1d03a3b82d4e75404608f6f4d579b9676124079a22a40c79f"}, + {file = "SQLAlchemy-1.4.20-cp27-cp27m-win32.whl", hash = "sha256:7150e5b543b466f45f668b352f7abda27998cc8035f051d1b7e9524ca9eb2f5f"}, + {file = "SQLAlchemy-1.4.20-cp27-cp27m-win_amd64.whl", hash = "sha256:6da83225a23eaf7b3f48f3d5f53c91b2cf00fbfa48b24a7a758160112dd3e123"}, + {file = "SQLAlchemy-1.4.20-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9841762d114018c49483c089fa2d47f7e612e57666323f615913d7d7f46e9606"}, + {file = "SQLAlchemy-1.4.20-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:eaee5dd378f6f0d7c3ec49aeeb26564d55ac0ad73b9b4688bf29e66deabddf73"}, + {file = "SQLAlchemy-1.4.20-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb25bcf9161e2fcbe9eebe8e829719b2334e849183f0e496bf4b83722bcccfa"}, + {file = "SQLAlchemy-1.4.20-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8d860c62e3f51623ccd528d8fac44580501df557d4b467cc5581587fcf057719"}, + {file = "SQLAlchemy-1.4.20-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f6d467b67a7e5048f1408e8ea60d6caa70be5b386d0eebbf1185ab49cb8c7e4"}, + {file = "SQLAlchemy-1.4.20-cp36-cp36m-win32.whl", hash = "sha256:ff8bebc7a9d297dff2003460e01db2c20c63818b45fb19170f388b1a72fe5a14"}, + {file = "SQLAlchemy-1.4.20-cp36-cp36m-win_amd64.whl", hash = "sha256:46361690f1e1c5385994a4caeb6e8126063ff593a5c635700bbc1245de793c1e"}, + {file = "SQLAlchemy-1.4.20-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:c0eb2cd3ad4967fcbdd9e066e8cd91fe2c23c671dbae9952f0b4d3d42832cc5f"}, + {file = "SQLAlchemy-1.4.20-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76fbc24311a3d039d6cd147d396719f606d96d1413f3816c028a48e29367f646"}, + {file = "SQLAlchemy-1.4.20-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f14acb0fd16d404fda9370f93aace682f284340c89c3442ac747c5466ac7e2b5"}, + {file = "SQLAlchemy-1.4.20-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcd84e4d46a86291495d131a7824ba38d2e8278bda9425c50661a04633174319"}, + {file = "SQLAlchemy-1.4.20-cp37-cp37m-win32.whl", hash = "sha256:2f60a2e599cf5cf5e5327ce60f2918b897e42ad9f405d10dd01e37869c0ce6fc"}, + {file = "SQLAlchemy-1.4.20-cp37-cp37m-win_amd64.whl", hash = "sha256:f6fc526bd70898489d02bf52c8f0632ab377592ae954d0c0a5bb38d618dddaa9"}, + {file = "SQLAlchemy-1.4.20-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:25c0e0f3a7e8c19350086b3c0fe93c4def045cec053d749ef15da710c4d54c81"}, + {file = "SQLAlchemy-1.4.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d48456e1aa4f0537f9c9af7be71e1f0659ff68bc1cd538ebc785f6b007bd0d"}, + {file = "SQLAlchemy-1.4.20-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9675d5bc7e4f96a7bb2b54d14e9b269a5fb6e5d36ecc7d01f0f65bb9af3185f9"}, + {file = "SQLAlchemy-1.4.20-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b502b5e2f08500cc4b8d29bfc4f51d805adcbc00f8d149e98fda8aae85ddb644"}, + {file = "SQLAlchemy-1.4.20-cp38-cp38-win32.whl", hash = "sha256:aad3234a41340e9cf6184e621694e2a7233ba3f8aef9b1e6de8cba431b45ebd2"}, + {file = "SQLAlchemy-1.4.20-cp38-cp38-win_amd64.whl", hash = "sha256:6c8406c3d8c1c7d15da454de15d77f7bb48d14ede5db994f74226c348cf1050e"}, + {file = "SQLAlchemy-1.4.20-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:238d78b3110b7f7cffdb70bf9cda686e0d876a849bc78ba4d471aa7b1461f306"}, + {file = "SQLAlchemy-1.4.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:854a7b15750e617e16f8d65dbc004f065a7963544b253b923f16109557648777"}, + {file = "SQLAlchemy-1.4.20-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff38ecf89c69a531a7326c2dae71982edfe2f805f3c016cdc5bfd1a04ebf80cb"}, + {file = "SQLAlchemy-1.4.20-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86c079732328f1add097b0b8079cd532b5d28e207fac93e9d6ea5f487506deef"}, + {file = "SQLAlchemy-1.4.20-cp39-cp39-win32.whl", hash = "sha256:46b99eab618cdc1c871ea707b7c52edc23cfea6c750740cd242ba62b5c84de7f"}, + {file = "SQLAlchemy-1.4.20-cp39-cp39-win_amd64.whl", hash = "sha256:b86d83fefc8a8c394f3490c37e1953bc16c311a3d1d1cf91518793bfb9847fb4"}, + {file = "SQLAlchemy-1.4.20.tar.gz", hash = "sha256:38ee3a266afef2978e82824650457f70c5d74ec0cadec1b10fe5ed6f038eb5d0"}, ] sqlalchemy-utils = [ - {file = "SQLAlchemy-Utils-0.37.7.tar.gz", hash = "sha256:716d9d9592258db9651a511d03e6b2553242c2a440855ee3f7d5812bbb55d9eb"}, - {file = "SQLAlchemy_Utils-0.37.7-py3-none-any.whl", hash = "sha256:afd204ed051f53302cd8789cc29c9b15bf458f8baef14a9052bf2823f855d2cb"}, + {file = "SQLAlchemy-Utils-0.37.8.tar.gz", hash = "sha256:a6aaee154f798be4e479af0ceffaa5034d35fcf6f40707c0947d21bde64e05e5"}, + {file = "SQLAlchemy_Utils-0.37.8-py3-none-any.whl", hash = "sha256:b1bf67d904fed16b16ef1dc07f03e5e93a6b23899f920f6b41c09be45fbb85f2"}, ] starlette = [ {file = "starlette-0.14.2-py3-none-any.whl", hash = "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed"}, @@ -1186,8 +1274,8 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ - {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, - {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] uvicorn = [ {file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"}, diff --git a/pyproject.toml b/pyproject.toml index 424cdac..b869ac0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ psycopg2-binary = "^2.8.6" SQLAlchemy = "^1.4.15" SQLAlchemy-Utils = "^0.37.4" python-multipart = "^0.0.5" -apihub-users = {git = "https://github.com/yifan/apihub-users"} +apihub-users = {git = "https://github.com/yifan/apihub-users", rev = "develop"} pipeline = {git = "https://github.com/yifan/pipeline", rev = "develop", extras = ["redis"]} [tool.poetry.dev-dependencies] @@ -64,6 +64,7 @@ factory-boy = "^3.2.0" requests = "^2.25.1" mypy = "^0.812" python-dotenv = {extras = ["cli"], version = "^0.17.1"} +black = {version = "^21.6b0", allow-prereleases = true} [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/fixtures/result_input.txt b/tests/fixtures/result_input.txt index 8a67bcd..7c6e3c7 100644 --- a/tests/fixtures/result_input.txt +++ b/tests/fixtures/result_input.txt @@ -1,2 +1,3 @@ {"kind": "MESG", "id": "ab7fe542-bdf2-11eb-b401-f21898b454f0", "created": "2021-05-26T10:19:11.878161", "logs": [], "content": {"user": "user", "api": "test", "status": "accepted", "submission_time": "2021-05-26T07:19:11.926576"}} {"kind": "MESG", "id": "ab7fe542-bdf2-11eb-b401-f21898b454f0", "created": "2021-05-26T10:19:11.878161", "logs": [{"name": "mediaframeworker", "version": "0.5.0", "updated": ["generalframe"], "received": "2021-05-26T12:13:54.953091", "processed": "2021-05-26T12:13:54.954349", "elapsed": 0.0013907699999999856}], "content": {"text": "this is simple", "user": "user", "api": "test", "status": "processed", "submission_time": "2021-05-26T07:19:11.926576", "generalframe": "Cultural-identity"}} +{"kind": "COMD", "id": "2cae5cbeda8d11eb959ff21898b454f0", "created": "2021-07-01T19:55:43.980102", "logs": [{"name": "mediaframeworker", "version": "0.5.0", "updated": ["source", "name", "input_schema", "version", "output_schema", "destination", "description"], "received": "2021-07-01T19:55:44.107834", "processed": "2021-07-01T19:55:44.109527", "elapsed": 0.0018327369999999732}], "content": {"name": "mediaframeworker", "version": "0.5.0", "description": "predict the frame of reporting for news articles", "source": {"namespace": null, "topic": "in-topic", "timeout": 0, "filename": "fixtures/articles.json", "content_only": false}, "destination": {"namespace": null, "topic": "out-topic", "compress": false, "filename": "-", "overwrite": false, "content_only": false}, "input_schema": {"title": "Input", "type": "object", "properties": {"text": {"title": "article text", "description": "article text in a single string", "minLength": 1, "type": "string"}}, "required": ["text"]}, "output_schema": {"title": "Output", "type": "object", "properties": {"generalframe": {"title": "predicted frame", "type": "string"}}, "required": ["generalframe"]}}, "action": "DEFINE"}