Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V3 fastapi #173

Draft
wants to merge 25 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4aaeef9
wip fastapi
alvarolopez Jun 13, 2024
df8cd0f
fix: remove handling of multiple models
alvarolopez Jun 14, 2024
78d6c71
fix: do not run models in complex process pool
alvarolopez Jun 14, 2024
630c96a
feat+wip: move debug endpoint to FastAPI
alvarolopez Jun 21, 2024
d31fada
fix: global variables should be used FIXME
alvarolopez Jun 21, 2024
5c2c171
fix: use get_router() function to get the router
alvarolopez Jun 21, 2024
84784d0
feat+wip: move models endpoint to FastAPI
alvarolopez Jun 21, 2024
5822c83
feat+wip: move predict method to FastAPI
alvarolopez Jun 24, 2024
13446c2
feat: bump version to v3 to clarify when developing
alvarolopez Aug 8, 2024
f7bf7b4
fix: remove old commented aiohttp code
alvarolopez Aug 8, 2024
0ba6856
fix: remove old testing dependency
alvarolopez Aug 12, 2024
b79995f
fix: dependencies in tox are not used anymore
alvarolopez Aug 12, 2024
5cca19e
fix: use .get_router() function to setup router at API level
alvarolopez Sep 29, 2024
dfb0b77
feat: move version habdling to app root router
alvarolopez Sep 29, 2024
52d2c74
fix: warm model according to configuration
alvarolopez Sep 29, 2024
8628f5a
fix: honour base_path
alvarolopez Sep 29, 2024
227e03f
fix: allow enabling/disabing docs
alvarolopez Sep 29, 2024
9639821
fix: allow disabling training endpoint
alvarolopez Sep 29, 2024
6292377
feat: add tags to routes
alvarolopez Sep 29, 2024
2c14122
fix: remove commented code
alvarolopez Sep 29, 2024
babd11e
fix: include version responses
alvarolopez Sep 29, 2024
837d6fa
fix: comment training responses
alvarolopez Sep 29, 2024
35ef6f7
fix: update some old links to point to AI4EOSC
alvarolopez Sep 29, 2024
035f7e3
fix: remove wrong comment
alvarolopez Sep 29, 2024
4a4d4a7
style: make black happy
alvarolopez Sep 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deepaas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import importlib.metadata
from pathlib import Path

__version__ = "2.4.0"
__version__ = "3.0.0"


def extract_version() -> str:
Expand Down
151 changes: 80 additions & 71 deletions deepaas/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,114 +14,123 @@
# License for the specific language governing permissions and limitations
# under the License.

import pathlib
import json

from aiohttp import web
import aiohttp_apispec
import fastapi
import fastapi.responses
from oslo_config import cfg

import deepaas
from deepaas.api import v2
from deepaas.api import versions
from deepaas.api.v2 import responses
from deepaas import log
from deepaas import model

LOG = log.getLogger(__name__)

APP = None
VERSIONS = {}

CONF = cfg.CONF

LINKS = """
- [Project website](https://deep-hybrid.datacloud.eu).
- [Project documentation](https://docs.deep-hybrid.datacloud.eu).
- [Model marketplace](https://marketplace.deep-hybrid.datacloud.eu).
- [AI4EOSC Project website](https://ai4eosc.eu).
- [Project documentation](https://docs.ai4eosc.eu).
- [API documentation](https://docs.ai4os.eu/deepaas).
- [AI4EOSC Model marketplace](https://dashboard.cloud.ai4eosc.eu/marketplace).
"""

API_DESCRIPTION = (
"<img"
" src='https://marketplace.deep-hybrid-datacloud.eu/images/logo-deep.png'"
" src='https://raw.githubusercontent.com/ai4os/.github/ai4os/profile/"
"horizontal-transparent.png'"
" width=200 alt='' />"
"\n\nThis is a REST API that is focused on providing access "
"to machine learning models. By using the DEEPaaS API "
"users can easily run a REST API in front of their model, "
"thus accessing its functionality via HTTP calls. "
"to machine learning models. "
"\n\nCurrently you are browsing the "
"[Swagger UI](https://swagger.io/tools/swagger-ui/) "
"for this API, a tool that allows you to visualize and interact with the "
"API and the underlying model."
) + LINKS


async def get_app(
swagger=True,
enable_doc=True,
doc="/api",
prefix="",
static_path="/static/swagger",
base_path="",
enable_train=True,
enable_predict=True,
):
"""Get the main app."""
def get_fastapi_app(
enable_doc: bool = True,
enable_train: bool = True, # FIXME(aloga): not handled yet
enable_predict: bool = True,
base_path: str = "",
) -> fastapi.FastAPI:
"""Get the main app, based on FastAPI."""
global APP
global VERSIONS

if APP:
return APP

APP = web.Application(debug=CONF.debug, client_max_size=CONF.client_max_size)
APP = fastapi.FastAPI(
title="Model serving API endpoint",
description=API_DESCRIPTION,
version=deepaas.extract_version(),
docs_url=f"{base_path}/docs" if enable_doc else None, # NOTE(aloga): changed
redoc_url=f"{base_path}/redoc" if enable_doc else None, # NOTE(aloga): new
openapi_url=f"{base_path}/openapi.json", # NOTE(aloga): changed
)

APP.middlewares.append(web.normalize_path_middleware())
model.load_v2_model()
LOG.info("Serving loaded V2 model: %s", model.V2_MODEL_NAME)

model.register_v2_models(APP)
if CONF.warm:
LOG.debug("Warming models...")
model.V2_MODEL.warm()

v2app = v2.get_app(
# FIXME(aloga): these have no effect now, remove.
enable_train=enable_train,
enable_predict=enable_predict,
)

APP.include_router(v2app, prefix=f"{base_path}/v2", tags=["v2"])
VERSIONS["v2"] = v2.get_v2_version

APP.add_api_route(
f"{base_path}/",
get_root,
methods=["GET"],
summary="Get API version information",
tags=["version"],
response_model=responses.VersionsAndLinks,
)

v2app = v2.get_app(enable_train=enable_train, enable_predict=enable_predict)
if base_path:
path = str(pathlib.Path(base_path) / "v2")
else:
path = "/v2"
APP.add_subapp(path, v2app)
versions.register_version("stable", v2.get_version)
return APP

if base_path:
# Get versions.routes, and transform them to have the base_path, as we cannot
# directly modify the routes already created and stored in the RouteTableDef
for route in versions.routes:
APP.router.add_route(
route.method, str(pathlib.Path(base_path + route.path)), route.handler
)
else:
APP.add_routes(versions.routes)

LOG.info("Serving loaded V2 models: %s", list(model.V2_MODELS.keys()))
async def get_root(request: fastapi.Request) -> fastapi.responses.JSONResponse:
versions = []
for _ver, info in VERSIONS.items():
resp = await info(request)
versions.append(json.loads(resp.body))

if CONF.warm:
for _, m in model.V2_MODELS.items():
LOG.debug("Warming models...")
await m.warm()

if swagger:
doc = str(pathlib.Path(base_path + doc))
swagger = str(pathlib.Path(base_path + "/swagger.json"))
static_path = str(pathlib.Path(base_path + static_path))

# init docs with all parameters, usual for ApiSpec
aiohttp_apispec.setup_aiohttp_apispec(
app=APP,
title="DEEP as a Service API endpoint",
info={
"description": API_DESCRIPTION,
},
externalDocs={
"description": "API documentation",
"url": "https://deepaas.readthedocs.org/",
},
version=deepaas.extract_version(),
url=swagger,
swagger_path=doc if enable_doc else None,
prefix=prefix,
static_path=static_path,
in_place=True,
)
root = str(request.url_for("get_root"))

return APP
response = {"versions": versions, "links": []}

doc = APP.docs_url.strip("/")
if doc:
doc = {"rel": "help", "type": "text/html", "href": f"{root}{doc}"}
response["links"].append(doc)

redoc = APP.redoc_url.strip("/")
if redoc:
redoc = {"rel": "help", "type": "text/html", "href": f"{root}{redoc}"}
response["links"].append(redoc)

spec = APP.openapi_url.strip("/")
if spec:
spec = {
"rel": "describedby",
"type": "application/json",
"href": f"{root}{spec}",
}
response["links"].append(spec)

return fastapi.responses.JSONResponse(content=response)
52 changes: 29 additions & 23 deletions deepaas/api/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,66 @@
# License for the specific language governing permissions and limitations
# under the License.

from aiohttp import web
import aiohttp_apispec
import fastapi
import fastapi.responses
from oslo_config import cfg

from deepaas.api.v2 import debug as v2_debug
from deepaas.api.v2 import models as v2_model
from deepaas.api.v2 import predict as v2_predict
from deepaas.api.v2 import responses
from deepaas.api.v2 import train as v2_train

# from deepaas.api.v2 import train as v2_train
from deepaas import log

CONF = cfg.CONF
LOG = log.getLogger("deepaas.api.v2")

# XXX
APP = None


def get_app(enable_train=True, enable_predict=True):
global APP

APP = web.Application()
# FIXME(aloga): check we cat get rid of global variables
APP = fastapi.APIRouter()

v2_debug.setup_debug()

APP.router.add_get("/", get_version, name="v2", allow_head=False)
v2_debug.setup_routes(APP)
v2_model.setup_routes(APP)
v2_train.setup_routes(APP, enable=enable_train)
v2_predict.setup_routes(APP, enable=enable_predict)
APP.include_router(v2_debug.get_router(), tags=["debug"])
APP.include_router(v2_model.get_router(), tags=["models"])
if enable_predict:
APP.include_router(v2_predict.get_router(), tags=["predict"])

# APP.router.add_get("/", get_version, name="v2", allow_head=False)
# v2_debug.setup_routes(APP)
# v2_model.setup_routes(APP)
# v2_train.setup_routes(APP, enable=enable_train)
# v2_predict.setup_routes(APP, enable=enable_predict)

APP.add_api_route(
"/",
get_v2_version,
methods=["GET"],
tags=["version"],
response_model=responses.Versions,
)

return APP


@aiohttp_apispec.docs(
tags=["versions"],
summary="Get V2 API version information",
)
@aiohttp_apispec.response_schema(responses.Version(), 200)
@aiohttp_apispec.response_schema(responses.Failure(), 400)
async def get_version(request):
# NOTE(aloga): we use the router table from this application (i.e. the
# global APP in this module) to be able to build the correct url, as it can
# be prefixed outside of this module (in an add_subapp() call)
root = APP.router["v2"].url_for()
async def get_v2_version(request: fastapi.Request) -> fastapi.responses.JSONResponse:
root = str(request.url_for("get_v2_version"))
version = {
"version": "stable",
"id": "v2",
"links": [
{
"rel": "self",
"type": "application/json",
"href": "%s" % root,
"href": f"{root}",
}
],
}

return web.json_response(version)
return fastapi.responses.JSONResponse(content=version)
44 changes: 28 additions & 16 deletions deepaas/api/v2/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,15 @@
import sys
import warnings

from aiohttp import web
import aiohttp_apispec
import fastapi
from oslo_config import cfg

from deepaas import log

CONF = cfg.CONF

app = web.Application()
routes = web.RouteTableDef()
router = fastapi.APIRouter(prefix="/debug")


# Ugly global variable to provide a string stream to read the DEBUG output
# if it is enabled
Expand All @@ -52,14 +51,17 @@ def close(self):
for f in self.handles:
f.close()

def isatty(self):
return all(f.isatty() for f in self.handles)


def setup_debug():
global DEBUG_STREAM

if CONF.debug_endpoint:
DEBUG_STREAM = io.StringIO()

logger = log.getLogger("deepaas").logger
logger = log.getLogger("deepaas")
hdlr = logging.StreamHandler(DEBUG_STREAM)
hdlr.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
Expand All @@ -78,23 +80,33 @@ def setup_debug():
sys.stderr = MultiOut(DEBUG_STREAM, sys.stderr)


@aiohttp_apispec.docs(
@router.get(
"/",
summary="Return debug information if enabled by API.",
description="Return debug information if enabled by API.",
tags=["debug"],
summary="""Return debug information if enabled by API.""",
description="""Return debug information if enabled by API.""",
produces=["text/plain"],
response_class=fastapi.responses.PlainTextResponse,
responses={
200: {"description": "Debug information if debug endpoint is enabled"},
204: {"description": "Debug endpoint not enabled"},
"200": {
"content": {"text/plain": {}},
"description": "Debug information if debug endpoint is enabled",
},
"204": {"description": "Debug endpoint not enabled"},
},
)
async def get(request):
async def get():
if DEBUG_STREAM is not None:
print("--- DEBUG MARKER %s ---" % datetime.datetime.now())
resp = DEBUG_STREAM.getvalue()
return web.Response(text=resp)
return web.HTTPNoContent()
return fastapi.responses.PlainTextResponse(resp)
else:
return fastapi.responses.Response(status_code=204)


def get_router() -> fastapi.APIRouter:
"""Auxiliary function to get the router.

def setup_routes(app):
app.router.add_get("/debug/", get, allow_head=False)
We use this function to be able to include the router in the main
application and do things before it gets included.
"""
return router
Loading
Loading