Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5836fd3
feat!: start migrating code to fastapi
alvarolopez Jun 13, 2024
187d756
fix: remove handling of multiple models
alvarolopez Jun 14, 2024
dd627f3
fix: do not run models in complex process pool
alvarolopez Jun 14, 2024
ebc8204
feat!: move debug endpoint to FastAPI
alvarolopez Jun 21, 2024
0fbb791
fix: global variables should be used FIXME
alvarolopez Jun 21, 2024
fb683ad
fix: use get_router() function to get the router
alvarolopez Jun 21, 2024
b402754
feat!: move models endpoint to FastAPI
alvarolopez Jun 21, 2024
549485b
feat!: move predict method to FastAPI
alvarolopez Jun 24, 2024
cb4acea
feat: bump version to v3 to clarify when developing
alvarolopez Aug 8, 2024
be68c9c
fix: remove old commented aiohttp code
alvarolopez Aug 8, 2024
3ea867f
fix: remove old testing dependency
alvarolopez Aug 12, 2024
cf33f7a
fix: dependencies in tox are not used anymore
alvarolopez Aug 12, 2024
3824b43
fix: use .get_router() function to setup router at API level
alvarolopez Sep 29, 2024
6891fb6
feat: move version habdling to app root router
alvarolopez Sep 29, 2024
d3acbee
fix: warm model according to configuration
alvarolopez Sep 29, 2024
f296afe
fix: honour base_path
alvarolopez Sep 29, 2024
513444d
fix: allow enabling/disabing docs
alvarolopez Sep 29, 2024
5ebe1de
fix: allow disabling training endpoint
alvarolopez Sep 29, 2024
6004585
feat: add tags to routes
alvarolopez Sep 29, 2024
3aa9af6
fix: remove commented code
alvarolopez Sep 29, 2024
276a5e2
fix: include version responses
alvarolopez Sep 29, 2024
b4dc3db
fix: comment training responses
alvarolopez Sep 29, 2024
340330c
fix: update some old links to point to AI4EOSC
alvarolopez Sep 29, 2024
11a94aa
fix: remove wrong comment
alvarolopez Sep 29, 2024
692ae77
style: make black happy
alvarolopez Sep 29, 2024
174e8d6
feat!: remove /train endpoints for all models
alvarolopez Mar 21, 2025
221ed3f
fix: remove unused assignment
alvarolopez Mar 21, 2025
d6ec939
fix: use correct variable name
alvarolopez Mar 21, 2025
5f20c66
style: change code style and fix linters
alvarolopez Mar 21, 2025
1b5c231
fix: improve unit testing
alvarolopez Mar 21, 2025
2b4d03f
fix: remove aiohttp dependencies
alvarolopez Mar 21, 2025
5c8e609
feat: add utility to check V2 and V3 compatibility
alvarolopez Mar 21, 2025
42d14f1
build: update poetry commands and Python versions
alvarolopez Sep 26, 2025
014ccfa
fix: VERSIONS is not a global, as it is not assigned in scope
alvarolopez Sep 26, 2025
6f9ca6b
Remove enable_predict functionality and predict-endpoint configuration
Copilot Sep 26, 2025
0212253
Clean up old commented code references
Copilot Sep 26, 2025
efbf786
Fix CLI exit code issue - return None instead of result objects
Copilot Sep 26, 2025
471eb1a
fix: remove FIXME
alvarolopez Sep 26, 2025
5f14e30
fix: remove commented code
alvarolopez Sep 26, 2025
298bf18
fix: remove commented code
alvarolopez Sep 26, 2025
7e9be33
fix: remove TODO note, as this is not a TODO
alvarolopez Sep 26, 2025
d5b9578
Remove async keyword from tests that don't require async features
Copilot Sep 26, 2025
d8cd9c5
fix: remove commented code
alvarolopez Sep 29, 2025
97c5d19
fix: make async calls actually async
alvarolopez Sep 29, 2025
5eb555e
fix: remove wrong fixme
alvarolopez Sep 29, 2025
efc6142
fix: raise exception on model loading error
alvarolopez Sep 29, 2025
cb0398d
style: fix pep8 errors
alvarolopez Sep 29, 2025
c0dddc2
fix: use set literal
alvarolopez Sep 29, 2025
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<img src="https://marketplace.deep-hybrid-datacloud.eu/images/logo-deep.png" width=200 alt="DEEP-Hybrid-DataCloud logo"/>

DEEP as a Service API (DEEPaaS API) is a REST API built on
[aiohttp](https://docs.aiohttp.org/) that allows to provide easy access to
[FastAPI](https://fastapi.tiangolo.com/) that allows to provide easy access to
machine learning, deep learning and artificial intelligence 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. DEEPaaS API leverages the [OpenAPI
Expand Down
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
153 changes: 82 additions & 71 deletions deepaas/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,114 +14,125 @@
# 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,
base_path: str = "",
) -> fastapi.FastAPI:
"""Get the main app, based on FastAPI."""
global APP

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()

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,
)

# Add a redirect from the old swagger.json to the new openapi.json
APP.add_api_route(
f"{base_path}/swagger.json",
APP.openapi,
methods=["GET"],
summary="Get OpenAPI schema",
tags=["version"],
)

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)
45 changes: 21 additions & 24 deletions deepaas/api/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,57 @@
# 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 import log

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

# NOTE(aloga): singleton pattern for the FastAPI app
APP = None


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

APP = web.Application()
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"])
APP.include_router(v2_predict.get_router(), tags=["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()
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