Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions dploy_kickstart/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, callble: typing.Callable) -> None:
self.evaluate_comment_args()

@staticmethod
def parse_comments(cs: str) -> None:
def parse_comments(cs: str) -> typing.List[str]:
"""Parse comments."""
args = []
if not cs:
Expand Down Expand Up @@ -105,7 +105,7 @@ def __name__(self) -> str:
return self.callble.__name__


def slice_n_dice(s: str) -> list:
def slice_n_dice(s: str) -> typing.List[str]:
"""Parse quotes within strings as separate slices."""
in_quotes = False
slice_points = []
Expand Down
43 changes: 20 additions & 23 deletions dploy_kickstart/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@

import os
import logging
import typing

import click
from waitress import serve as waitress_serve
from paste.translogger import TransLogger

from dploy_kickstart import deps as pd
from dploy_kickstart import server as ps

import uvicorn

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -40,10 +37,10 @@ def cli() -> None:
+ "'install-deps' command",
)
@click.option(
"--wsgi/--no-wsgi",
"--asgi/--no-asgi",
default=True,
help="Use Waitress as a WSGI server, defaults to True,"
+ " else launches a Flask debug server.",
help="Use ASGI server, defaults to True,"
+ " else launches a FastAPI debug server.",
)
@click.option(
"-h", "--host", help="Host to serve on, defaults to '0.0.0.0'", default="0.0.0.0"
Expand All @@ -52,8 +49,8 @@ def cli() -> None:
"-p", "--port", help="Port to serve on, defaults to '8080'", default=8080, type=int
)
def serve(
entrypoint: str, location: str, deps: str, wsgi: bool, host: str, port: int
) -> typing.Any:
entrypoint: str, location: str, deps: str, asgi: bool, host: str, port: int
) -> None:
"""CLI serve."""
if deps:
click.echo(f"Installing deps: {deps}")
Expand All @@ -62,27 +59,19 @@ def serve(
app = ps.generate_app()
app = ps.append_entrypoint(app, entrypoint, os.path.abspath(location))

if not wsgi:
click.echo("Starting Flask Development server")
app.run(
host=os.getenv("DPLOY_KICKSTART_HOST", "0.0.0.0"),
port=int(os.getenv("DPLOY_KICKSTART_PORT", 8080)),
)
else:
click.echo("Starting Waitress server")
waitress_serve(
TransLogger(app, setup_console_handler=False),
host=os.getenv("dploy_kickstart_HOST", "0.0.0.0"),
port=int(os.getenv("dploy_kickstart_PORT", 8080)),
)
uvicorn.run(
app,
host=os.getenv("DPLOY_KICKSTART_HOST", "0.0.0.0"),
port=int(os.getenv("DPLOY_KICKSTART_PORT", 8080)),
)


@cli.command(help="install dependencies")
@click.option(
"-d",
"--deps",
required=True,
help="comma separated paths to either requirements.txt or setup.py files",
help="comma separated paths to either requirements.txt" " or setup.py files",
)
@click.option(
"-l",
Expand Down Expand Up @@ -113,3 +102,11 @@ def _deps(deps: str, location: str) -> None:
"Supported formats: "
"requirements.txt, setup.py".format(r)
)


if __name__ == "__main__":
_entrypoint = "cbt_v2/deployment.py"
_location = "/Users/baturayofluoglu/Workspace/ai-email-cbt-tfidf"
_deps = False
os.environ["LANGUAGE"] = "en"
serve(_entrypoint, _location, _deps, asgi=True, host="0.0.0.0", port=80)
5 changes: 1 addition & 4 deletions dploy_kickstart/deps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Dependency installation logic."""


import sys
import os
import subprocess
Expand Down Expand Up @@ -28,8 +26,7 @@ def install_requirements_txt(requirements_txt_location: str) -> None:
)

cmd = f"{sys.executable} -m pip install -r {requirements_txt_location}"
print("##")
print(cmd)

c = execute_cmd(cmd)
if c != 0:
raise RequirementsInstallException(requirements_txt_location)
Expand Down
23 changes: 0 additions & 23 deletions dploy_kickstart/openapi.py

This file was deleted.

48 changes: 19 additions & 29 deletions dploy_kickstart/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,54 @@

import logging
import typing

from flask import Flask, jsonify
from fastapi import FastAPI, APIRouter
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse

import dploy_kickstart.wrapper as pw
import dploy_kickstart.errors as pe
import dploy_kickstart.openapi as po

log = logging.getLogger(__name__)


def append_entrypoint(
app: typing.Generic, entrypoint: str, location: str
) -> typing.Generic:
def append_entrypoint(app: FastAPI, entrypoint: str, location: str) -> FastAPI:
"""Add routes/functions defined in entrypoint."""
mod = pw.import_entrypoint(entrypoint, location)
fm = pw.get_func_annotations(mod)

if not any([e.endpoint for e in fm]):
raise Exception("no endpoints defined")

openapi_spec = po.base_spec(title=entrypoint)
api_router = APIRouter()

# iterate over annotations in usercode
for f in fm:
if f.endpoint:
log.debug(
f"adding endpoint for func: {f.__name__} (func_args: {f.comment_args})"
)

app.add_url_rule(
f.endpoint_path,
f.endpoint_path,
pw.func_wrapper(f),
api_router.add_api_route(
methods=[f.request_method.upper()],
strict_slashes=False
path=f.endpoint_path,
endpoint=pw.func_wrapper(f),
)

# add info about endpoint to api spec
po.path_spec(openapi_spec, f)

app.add_url_rule(
"/openapi.yaml", "/openapi.yaml", openapi_spec.to_yaml, methods=["GET"],
)

app.include_router(api_router)
return app


def generate_app() -> typing.Generic:
"""Generate a Flask app."""
app = Flask(__name__)
"""Generate a FastApi app."""
app = FastAPI()

@app.route("/healthz/", methods=["GET"])
def health_check() -> None:
return "healthy", 200
@app.get("/healthz/", status_code=200)
async def health_check() -> None:
return "healthy"

@app.errorhandler(pe.ServerException)
def handle_server_exception(error: Exception) -> None:
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
@app.exception_handler(pe.ServerException)
async def handle_server_exception(_, exc: pe.ServerException) -> None:
response = jsonable_encoder(exc)
return JSONResponse(status_code=exc.status_code, content=response)

return app
11 changes: 5 additions & 6 deletions dploy_kickstart/transformers.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
"""Utilities to transform requests and responses."""

import typing
from flask import jsonify, Response, Request

from fastapi import Response
import dploy_kickstart.annotations as da


def json_resp(func_result: typing.Any) -> Response:
"""Transform json response."""
return jsonify(func_result)
return func_result


def json_req(f: da.AnnotatedCallable, req: Request):
def json_req(f: da.AnnotatedCallable, body: dict):
"""Preprocess application/json request."""
if f.json_to_kwargs:
return f(**req.json)
return f(**body)
else:
return f(req.json)
return f(body)


MIME_TYPE_REQ_MAPPER = {
Expand Down
15 changes: 9 additions & 6 deletions dploy_kickstart/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import typing
import traceback

from flask import request
from fastapi import Request, Body

import dploy_kickstart.errors as pe
import dploy_kickstart.transformers as pt
import dploy_kickstart.annotations as pa
Expand Down Expand Up @@ -61,7 +62,7 @@ def get_func_annotations(mod: typing.Generic) -> typing.Dict:


def import_entrypoint(entrypoint: str, location: str) -> typing.Generic:
"""Import entryoint from user code."""
"""Import entrypoint from user code."""
# assert if entrypoint contains a path prefix and if so add it to location
if os.path.dirname(entrypoint) != "":
location = os.path.join(location, os.path.dirname(entrypoint))
Expand Down Expand Up @@ -101,9 +102,9 @@ def import_entrypoint(entrypoint: str, location: str) -> typing.Generic:
def func_wrapper(f: pa.AnnotatedCallable) -> typing.Callable:
"""Wrap functions with request logic."""

def exposed_func() -> typing.Callable:
async def exposed_func(request: Request, body=Body(...)) -> typing.Callable:
# some sanity checking
if request.content_type.lower() != f.request_content_type:
if request.headers["content-type"].lower() != f.request_content_type:
raise pe.UnsupportedMediaType(
"Please provide a valid 'Content-Type' header, valid: {}".format(
f.request_content_type
Expand All @@ -112,15 +113,17 @@ def exposed_func() -> typing.Callable:

# preprocess input for callable
try:
res = pt.MIME_TYPE_REQ_MAPPER[f.response_mime_type](f, request)
res = pt.MIME_TYPE_REQ_MAPPER[f.response_mime_type](f, body)
except Exception:
raise pe.UserApplicationError(
message=f"error in executing '{f.__name__}'",
traceback=traceback.format_exc(),
)

# determine whether or not to process response before sending it back to caller
wrapped_res = pt.MIME_TYPE_RES_MAPPER[request.content_type](res)
wrapped_res = pt.MIME_TYPE_RES_MAPPER[request.headers["content-type"].lower()](
res
)

return wrapped_res

Expand Down
Loading