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

fix: IO OpenAPI spec for bentoml.client #3144

Merged
merged 4 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/bentoml/_internal/bento/build_dev_bentoml_whl.py
Expand Up @@ -12,7 +12,6 @@
logger = logging.getLogger(__name__)

BENTOML_DEV_BUILD = "BENTOML_BUNDLE_LOCAL_BUILD"
_exc_message = f"'{BENTOML_DEV_BUILD}=True', which requires the 'pypa/build' package. Install development dependencies with 'pip install -r requirements/dev-requirements.txt' and try again."


def build_bentoml_editable_wheel(target_path: str) -> None:
Expand All @@ -33,7 +32,9 @@ def build_bentoml_editable_wheel(target_path: str) -> None:

from build import ProjectBuilder
except ModuleNotFoundError as e:
raise MissingDependencyException(_exc_message) from e
raise MissingDependencyException(
f"Environment variable '{BENTOML_DEV_BUILD}=True', which requires the 'pypa/build' package ({e}). Install development dependencies with 'pip install -r requirements/dev-requirements.txt' and try again."
) from None

# Find bentoml module path
# This will be $GIT_ROOT/src/bentoml
Expand Down
6 changes: 2 additions & 4 deletions src/bentoml/_internal/io_descriptors/base.py
Expand Up @@ -19,9 +19,7 @@
from ..types import LazyType
from ..context import InferenceApiContext as Context
from ..service.openapi.specification import Schema
from ..service.openapi.specification import Response as OpenAPIResponse
from ..service.openapi.specification import Reference
from ..service.openapi.specification import RequestBody

InputType = (
UnionType
Expand Down Expand Up @@ -90,11 +88,11 @@ def openapi_components(self) -> dict[str, t.Any] | None:
raise NotImplementedError

@abstractmethod
def openapi_request_body(self) -> RequestBody:
def openapi_request_body(self) -> dict[str, t.Any]:
raise NotImplementedError

@abstractmethod
def openapi_responses(self) -> OpenAPIResponse:
def openapi_responses(self) -> dict[str, t.Any]:
raise NotImplementedError

@abstractmethod
Expand Down
8 changes: 5 additions & 3 deletions src/bentoml/_internal/io_descriptors/file.py
Expand Up @@ -14,18 +14,20 @@
from ..types import FileLike
from ..utils.http import set_cookies
from ...exceptions import BadInput
from ...exceptions import InvalidArgument
from ...exceptions import BentoMLException
from ..service.openapi import SUCCESS_DESCRIPTION
from ..service.openapi.specification import Schema
from ..service.openapi.specification import Response as OpenAPIResponse
from ..service.openapi.specification import MediaType
from ..service.openapi.specification import RequestBody

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from typing_extensions import Self

from bentoml.grpc.v1alpha1 import service_pb2 as pb

from .base import OpenAPIResponse
from ..context import InferenceApiContext as Context

FileKind: t.TypeAlias = t.Literal["binaryio", "textio"]
Expand Down Expand Up @@ -121,7 +123,7 @@ def __new__( # pylint: disable=arguments-differ # returning subclass from new
res._mime_type = mime_type
return res

def to_spec(self):
def to_spec(self) -> dict[str, t.Any]:
raise NotImplementedError

@classmethod
Expand Down
26 changes: 14 additions & 12 deletions src/bentoml/_internal/io_descriptors/image.py
Expand Up @@ -20,19 +20,19 @@
from ...exceptions import InternalServerError
from ..service.openapi import SUCCESS_DESCRIPTION
from ..service.openapi.specification import Schema
from ..service.openapi.specification import Response as OpenAPIResponse
from ..service.openapi.specification import MediaType
from ..service.openapi.specification import RequestBody

if TYPE_CHECKING:
from types import UnionType

import PIL
import PIL.Image
from typing_extensions import Self

from bentoml.grpc.v1alpha1 import service_pb2 as pb

from .. import external_typing as ext
from .base import OpenAPIResponse
from ..context import InferenceApiContext as Context

_Mode = t.Literal[
Expand Down Expand Up @@ -224,7 +224,7 @@ def to_spec(self) -> dict[str, t.Any]:
}

@classmethod
def from_spec(cls) -> Self:
def from_spec(cls, spec: dict[str, t.Any]) -> Self:
if "args" not in spec:
raise InvalidArgument(f"Missing args key in Image spec: {spec}")

Expand All @@ -239,20 +239,22 @@ def openapi_schema(self) -> Schema:
def openapi_components(self) -> dict[str, t.Any] | None:
pass

def openapi_request_body(self) -> RequestBody:
return RequestBody(
content={
def openapi_request_body(self) -> dict[str, t.Any]:
return {
"content": {
mtype: MediaType(schema=self.openapi_schema())
for mtype in self._allowed_mimes
},
required=True,
)
"required": True,
"x-bentoml-io-descriptor": self.to_spec(),
}

def openapi_responses(self) -> OpenAPIResponse:
return OpenAPIResponse(
description=SUCCESS_DESCRIPTION,
content={self._mime_type: MediaType(schema=self.openapi_schema())},
)
return {
"description": SUCCESS_DESCRIPTION,
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"x-bentoml-io-descriptor": self.to_spec(),
}

async def from_http_request(self, request: Request) -> ImageType:
content_type, _ = parse_options_header(request.headers["content-type"])
Expand Down
10 changes: 5 additions & 5 deletions src/bentoml/_internal/io_descriptors/json.py
Expand Up @@ -10,29 +10,29 @@
from starlette.requests import Request
from starlette.responses import Response

from bentoml.exceptions import BadInput

from .base import IODescriptor
from ..types import LazyType
from ..utils import LazyLoader
from ..utils import bentoml_cattr
from ..utils.pkg import pkg_version_info
from ..utils.http import set_cookies
from ...exceptions import BadInput
from ...exceptions import InvalidArgument
from ..service.openapi import REF_PREFIX
from ..service.openapi import SUCCESS_DESCRIPTION
from ..service.openapi.specification import Schema
from ..service.openapi.specification import Response as OpenAPIResponse
from ..service.openapi.specification import MediaType
from ..service.openapi.specification import RequestBody

if TYPE_CHECKING:
from types import UnionType

import pydantic
import pydantic.schema as schema
from google.protobuf import struct_pb2
from typing_extensions import Self

from .. import external_typing as ext
from .base import OpenAPIResponse
from ..context import InferenceApiContext as Context

else:
Expand Down Expand Up @@ -250,7 +250,7 @@ def openapi_components(self) -> dict[str, t.Any] | None:

return {"schemas": pydantic_components_schema(self._pydantic_model)}

def openapi_request_body(self) -> RequestBody:
def openapi_request_body(self) -> dict[str, t.Any]:
return {
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"required": True,
Expand Down
4 changes: 2 additions & 2 deletions src/bentoml/_internal/io_descriptors/multipart.py
Expand Up @@ -221,14 +221,14 @@ def openapi_request_body(self) -> RequestBody:
return {
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"required": True,
"x-bentoml-descriptor": self.to_spec(),
"x-bentoml-io-descriptor": self.to_spec(),
}

def openapi_responses(self) -> OpenAPIResponse:
return {
"description": SUCCESS_DESCRIPTION,
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"x-bentoml-descriptor": self.to_spec(),
"x-bentoml-io-descriptor": self.to_spec(),
}

async def from_http_request(self, request: Request) -> dict[str, t.Any]:
Expand Down
10 changes: 5 additions & 5 deletions src/bentoml/_internal/io_descriptors/numpy.py
Expand Up @@ -19,16 +19,16 @@
from ...exceptions import UnprocessableEntity
from ..service.openapi import SUCCESS_DESCRIPTION
from ..service.openapi.specification import Schema
from ..service.openapi.specification import Response as OpenAPIResponse
from ..service.openapi.specification import MediaType
from ..service.openapi.specification import RequestBody

if TYPE_CHECKING:
import numpy as np
from typing_extensions import Self

from bentoml.grpc.v1alpha1 import service_pb2 as pb

from .. import external_typing as ext
from .base import OpenAPIResponse
from ..context import InferenceApiContext as Context
else:
from bentoml.grpc.utils import import_generated_stubs
Expand Down Expand Up @@ -298,15 +298,15 @@ def openapi_example(self) -> t.Any:
return self.sample_input.tolist()
return

def openapi_request_body(self) -> RequestBody:
def openapi_request_body(self) -> dict[str, t.Any]:
return {
"content": {
self._mime_type: MediaType(
schema=self.openapi_schema(), example=self.openapi_example()
)
},
"required": True,
"x-bentoml-descriptor": self.to_spec(),
"x-bentoml-io-descriptor": self.to_spec(),
}

def openapi_responses(self) -> OpenAPIResponse:
Expand All @@ -317,7 +317,7 @@ def openapi_responses(self) -> OpenAPIResponse:
schema=self.openapi_schema(), example=self.openapi_example()
)
},
"x-bentoml-descriptor": self.to_spec(),
"x-bentoml-io-descriptor": self.to_spec(),
}

def validate_array(
Expand Down
60 changes: 43 additions & 17 deletions src/bentoml/_internal/io_descriptors/pandas.py
Expand Up @@ -22,12 +22,11 @@
from ..service.openapi import SUCCESS_DESCRIPTION
from ..utils.lazy_loader import LazyLoader
from ..service.openapi.specification import Schema
from ..service.openapi.specification import Response as OpenAPIResponse
from ..service.openapi.specification import MediaType
from ..service.openapi.specification import RequestBody

if TYPE_CHECKING:
import pandas as pd
from typing_extensions import Self

from bentoml.grpc.v1alpha1 import service_pb2 as pb

Expand All @@ -42,7 +41,7 @@
"pd",
globals(),
"pandas",
exc_msg='pandas" is required to use PandasDataFrame or PandasSeries. Install with "pip install -U pandas"',
exc_msg='pandas" is required to use PandasDataFrame or PandasSeries. Install with "pip install bentoml[io-pandas]"',
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -325,6 +324,7 @@ def __init__(
self._orient = orient
self._columns = columns
self._apply_column_names = apply_column_names
# TODO: convert dtype to numpy dtype
self._dtype = dtype
self._enforce_dtype = enforce_dtype
self._shape = shape
Expand All @@ -344,14 +344,31 @@ def sample_input(self) -> ext.PdDataFrame | None:
def sample_input(self, value: ext.PdDataFrame) -> None:
self._sample_input = value

def _convert_dtype(self, value: ext.PdDType) -> str | None:
if LazyType["ext.NpNDArray"]("numpy", "ndarray").isinstance(value):
return str(value.dtype)
logger.warning(f"{type(value)} is not yet supported.")
return None

def to_spec(self) -> dict[str, t.Any]:
# TODO: support extension dtypes
dtype: bool | str | dict[str, t.Any] | None = None
if self._dtype is not None:
if isinstance(self._dtype, bool):
dtype = self._dtype
elif isinstance(self._dtype, dict):
dtype = {str(k): self._convert_dtype(v) for k, v in self._dtype.items()}
elif LazyType("numpy", "ndarray").isinstance(self._dtype):
dtype = self._dtype.name
else:
raise NotImplementedError

return {
"id": self.descriptor_id,
"args": {
"orient": self._orient,
"columns": self._columns,
"dtype": None if self._dtype is None else self._dtype.name,
"dtype": dtype,
"shape": self._shape,
"enforce_dtype": self._enforce_dtype,
"enforce_shape": self._enforce_shape,
Expand All @@ -375,17 +392,19 @@ def openapi_schema(self) -> Schema:
def openapi_components(self) -> dict[str, t.Any] | None:
pass

def openapi_request_body(self) -> RequestBody:
return RequestBody(
content={self._mime_type: MediaType(schema=self.openapi_schema())},
required=True,
)
def openapi_request_body(self) -> dict[str, t.Any]:
return {
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"required": True,
"x-bentoml-io-descriptor": self.to_spec(),
}

def openapi_responses(self) -> OpenAPIResponse:
return OpenAPIResponse(
description=SUCCESS_DESCRIPTION,
content={self._mime_type: MediaType(schema=self.openapi_schema())},
)
return {
"description": SUCCESS_DESCRIPTION,
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"x-bentoml-io-descriptor": self.to_spec(),
}

async def from_http_request(self, request: Request) -> ext.PdDataFrame:
"""
Expand Down Expand Up @@ -787,11 +806,18 @@ def input_type(self) -> LazyType[ext.PdSeries]:

def to_spec(self) -> dict[str, t.Any]:
# TODO: support extension dtypes
dtype = None
if self._dtype is not None:
if isinstance(self._dtype, (dict, bool)):
dtype = self._dtype
else:
dtype = self._dtype.name

return {
"id": self.descriptor_id,
"args": {
"orient": self._orient,
"dtype": None if self._dtype is None else self._dtype.name,
"dtype": dtype,
"shape": self._shape,
"enforce_dtype": self._enforce_dtype,
"enforce_shape": self._enforce_shape,
Expand All @@ -811,18 +837,18 @@ def openapi_schema(self) -> Schema:
def openapi_components(self) -> dict[str, t.Any] | None:
pass

def openapi_request_body(self) -> RequestBody:
def openapi_request_body(self) -> dict[str, t.Any]:
return {
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"required": True,
"x-bentoml-descriptor": self.to_spec(),
"x-bentoml-io-descriptor": self.to_spec(),
}

def openapi_responses(self) -> OpenAPIResponse:
return {
"description": SUCCESS_DESCRIPTION,
"content": {self._mime_type: MediaType(schema=self.openapi_schema())},
"x-bentoml-descriptor": self.to_spec(),
"x-bentoml-io-descriptor": self.to_spec(),
}

async def from_http_request(self, request: Request) -> ext.PdSeries:
Expand Down