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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: install poetry
run: |
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python;
echo "::add-path::$HOME/.poetry/bin/"
echo "$HOME/.poetry/bin/" >> $GITHUB_PATH
- name: install deps
run: poetry install -v
- name: flake8
Expand Down
5 changes: 1 addition & 4 deletions dploy_kickstart/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ def __init__(self, callble: typing.Callable) -> None:
self.request_method = "post"
self.accepts_json = True
self.returns_json = True
self.response_mime_type = (
"application/json" # functionality deprecated, to be removed `
)
self.response_mime_type = None
self.request_content_type = (
"application/json" # functionality deprecated, to be removed `
)
Expand Down Expand Up @@ -69,7 +67,6 @@ def evaluate_comment_args(self) -> None:
self.endpoint = True
self.endpoint_path = p

# functionality deprecated, to be removed
if c[0] == "response_mime_type":
self.response_mime_type = c[1].lower()

Expand Down
2 changes: 0 additions & 2 deletions dploy_kickstart/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class ScriptImportError(ServerException):
status_code = 500

def __init__(self, message: str):
super().__init__(self)
self.message = message

def to_dict(self) -> dict:
Expand All @@ -27,7 +26,6 @@ class UnsupportedEntrypoint(ServerException):
status_code = 500

def __init__(self, entrypoint: str):
super().__init__(self)
self.message = f"entrypoint '{entrypoint}' not supported"

def to_dict(self) -> dict:
Expand Down
5 changes: 1 addition & 4 deletions dploy_kickstart/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ def append_entrypoint(app: Flask, entrypoint: str, location: str) -> Flask:
po.path_spec(openapi_spec, f)

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

return app
Expand Down
83 changes: 74 additions & 9 deletions dploy_kickstart/transformers.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,73 @@
"""Utilities to transform requests and responses."""

import io
import typing
from flask import jsonify, Response, Request
import dploy_kickstart.annotations as da


def bytes_resp(func_result: typing.Any) -> Response:
return Response(func_result, mimetype="application/octet-stream")
def bytes_resp(func_result: typing.Any, mimetype=None) -> Response:
if mimetype is None:
return Response(func_result, mimetype="application/octet-stream")
else:
return Response(func_result, mimetype=mimetype)


def bytes_io_resp(func_result: typing.Any) -> Response:
return Response(func_result.getvalue(), mimetype="application/octet-stream")
def bytes_io_resp(func_result: typing.Any, mimetype=None) -> Response:
if mimetype is None:
return Response(func_result.getvalue(), mimetype="application/octet-stream")
else:
return Response(func_result.getvalue(), mimetype=mimetype)


def default_req(f: da.AnnotatedCallable, req: Request) -> typing.Any:
return f(req.data)
def pil_image_resp(func_result: typing.Any, mimetype=None) -> Response:
# create file-object in memory
file_object = io.BytesIO()
img_format = func_result.format

# write image to a file-object
# Don't change quality and subsampling since save decrease the quality by default
func_result.save(file_object, img_format, quality=100, subsampling=0)
auto_mimetype = f"image/{img_format.lower()}"

# move to beginning of file so `send_file()` it will read from start
file_object.seek(0)

if mimetype is None:
return Response(file_object, mimetype=auto_mimetype)
else:
return Response(file_object, mimetype=mimetype)


def np_tolist_resp(func_result: typing.Any, mimetype=None) -> Response:
response = jsonify(func_result.tolist())
if mimetype is None:
return response
else:
response.mimetype = mimetype
return response


def np_item_resp(func_result: typing.Any, mimetype=None) -> Response:
response = jsonify(func_result.item())
if mimetype is None:
return response
else:
response.mimetype = mimetype
return response


def json_resp(func_result: typing.Any) -> Response:
def json_resp(func_result: typing.Any, mimetype=None) -> Response:
"""Transform json response."""
return jsonify(func_result)
response = jsonify(func_result)
if mimetype is None:
return response
else:
response.mimetype = mimetype
return response


def default_req(f: da.AnnotatedCallable, req: Request) -> typing.Any:
return f(req.data)


def json_req(f: da.AnnotatedCallable, req: Request) -> typing.Any:
Expand All @@ -39,5 +87,22 @@ def json_req(f: da.AnnotatedCallable, req: Request) -> typing.Any:
"list": json_resp,
"dict": json_resp,
"bytes": bytes_resp,
# io.BytesIO return type
"BytesIO": bytes_io_resp,
# Pillow Image Return dtype
"Image": pil_image_resp,
# Numpy Return dtypes
"ndarray": np_tolist_resp,
"matrix": np_tolist_resp,
"int8": np_item_resp,
"uint8": np_item_resp,
"int16": np_item_resp,
"uint16": np_item_resp,
"int32": np_item_resp,
"uint32": np_item_resp,
"int64": np_item_resp,
"uint64": np_item_resp,
"float16": np_item_resp,
"float32": np_item_resp,
"float64": np_item_resp,
}
4 changes: 3 additions & 1 deletion dploy_kickstart/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ def exposed_func() -> typing.Callable:

# determine whether or not to process response before sending it back to caller
try:
return pt.MIME_TYPE_RES_MAPPER[res.__class__.__name__](res)
return pt.MIME_TYPE_RES_MAPPER[res.__class__.__name__](
res, f.response_mime_type
)
except Exception:
raise pe.UserApplicationError(
message=f"error in executing '{f.__name__()}' method, the return type "
Expand Down
46 changes: 45 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pytest-cov = "^2.8.1"
requests = "^2.23.0"
flynt = "^0.46.1"
pillow = "^8.0.1"
numpy = "^1.19.4"

[tool.black]
line-length = 88
Expand Down
Binary file added tests/assets/cropped_golf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/assets/deps_tests/fake_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some_random_package_kickstart==4.0.0
7 changes: 1 addition & 6 deletions tests/assets/deps_tests/my_pkg/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,5 @@
from distutils.core import setup

setup(
name="dummy",
version="1.0",
packages=[],
install_requires=[
"dummy_test==0.1.3",
],
name="dummy", version="1.0", packages=[], install_requires=["dummy_test==0.1.3",],
)
7 changes: 7 additions & 0 deletions tests/assets/deps_tests/my_pkg/stp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env python

from distutils.core import setup

setup(
name="dummy", version="1.0", packages=[], install_requires=["dummy_test==0.1.3",],
)
Binary file added tests/assets/golf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions tests/assets/server_default.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import numpy as np
from PIL import Image
import io

Expand Down Expand Up @@ -97,3 +98,34 @@ def f7(some_string):
def f8(raw_image):
image = Image.open(io.BytesIO(raw_image))
return image


# @dploy endpoint f9
def f9(raw_image):
image = Image.open(io.BytesIO(raw_image))

# Size of the image in pixels (size of original image)
# (This is not mandatory)
width, height = image.size

# Setting the points for cropped image
left = 5
top = height / 4
right = 164
bottom = 3 * height / 4

# Cropped image of above dimension
# (It will not change orginal image)
im1 = image.crop((left, top, right, bottom))
im1.format = image.format # original image extension
return im1


# @dploy endpoint f10
def f10(input):
return np.uint(input)


# @dploy endpoint f11
def f11(input):
return np.array(input)
22 changes: 10 additions & 12 deletions tests/test_callable_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ def t11():
return img1


# @dploy endpoint get_model_name
# @dploy response_method get
def t12():
return "kickstart"


@pytest.mark.parametrize(
"callable,endpoint,endpoint_path,has_args,output, error",
[
Expand All @@ -87,6 +93,7 @@ def t11():
(t9, True, "/get_image2/", True, img1, True),
(t10, True, "/get_image3/", True, img1, True),
(t11, True, "/get_image4/", True, img1, True),
(t12, True, "/get_model_name/", True, "kickstart", False),
],
)
def test_callable_annotation(
Expand All @@ -111,17 +118,10 @@ def test_callable_annotation(
@pytest.mark.parametrize(
"py_file,expected",
[
(
"c1.py",
[["endpoint", "predict"], ["endpoint", "train2"]],
),
("c1.py", [["endpoint", "predict"], ["endpoint", "train2"]],),
(
"nb_with_comments.ipynb",
[
["endpoint", "predict"],
["endpoint", "train"],
["trigger", "train"],
],
[["endpoint", "predict"], ["endpoint", "train"], ["trigger", "train"],],
),
],
)
Expand Down Expand Up @@ -180,9 +180,7 @@ def test_annotated_scripts(py_file, expected):
#
# irrelevant other stuff
""",
[
["arg", "foo bar", "arg2", "bar the foos"],
],
[["arg", "foo bar", "arg2", "bar the foos"],],
],
[
"""
Expand Down
2 changes: 2 additions & 0 deletions tests/test_dep_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@pytest.mark.parametrize(
"req_file, error_expected",
[
("fake_requirements.txt", True),
("req1.txt", True),
("non_existing.txt", True),
("requirements.txt", False),
Expand All @@ -28,6 +29,7 @@ def test_req_install(req_file, error_expected):
"setup_py, error_expected",
[
("my_pkg/setup.py", False),
("my_pkg/stp.py", True),
("non_existing_pkg/setup.py", True),
],
)
Expand Down
Loading