diff --git a/docs/source/build-with-bentoml/iotypes.rst b/docs/source/build-with-bentoml/iotypes.rst index a5a33cfbc98..c6fcdedea86 100644 --- a/docs/source/build-with-bentoml/iotypes.rst +++ b/docs/source/build-with-bentoml/iotypes.rst @@ -83,12 +83,12 @@ To handle nullable input, you can use ``Optional``: ) -> Generator[str, None, None]: ... -In the ``LanguageModel`` class, the ``temperature`` and ``max_tokens`` fields are marked as ``Optional``. This means they can be ``None``. Note that when using ``Optional`` types in BentoML, you must provide a default value (here, ``default=None``). General union types are not supported. +In the ``LanguageModel`` class, the ``temperature`` and ``max_tokens`` fields are marked as ``Optional``, which means they can be ``None``. When using ``Optional`` types in BentoML, you must provide a default value (here, ``default=None``). General union types are not supported. Pydantic ^^^^^^^^ -Pydantic models allow for more structured data with validation. They are particularly useful when your Service needs to handle complex data structures with rigorous validation requirements. Here is an example: +Pydantic models support more structured data with validation. They are particularly useful when your Service needs to handle complex data structures with rigorous validation requirements. Here is an example: .. code-block:: python @@ -206,6 +206,85 @@ To output a file with a path, you can use ``context.temp_dir`` to provide a uniq When the method returns a ``Path`` object pointing to the generated file, BentoML serializes this file and includes it in the response to the client. +More practical examples to handle files: + +.. tab-set:: + + .. tab-item:: Add a string to a file + + .. code-block:: python + + from pathlib import Path + from bentoml.validators import ContentType + from typing import Annotated # Python 3.9 or above + from typing_extensions import Annotated # Older than 3.9 + import bentoml + + @bentoml.service + class AppendStringToFile: + + @bentoml.api() + def append_string_to_eof( + self, + context: bentoml.Context, + txt_file: Annotated[Path, ContentType("text/plain")], + input_string: str, + ) -> Annotated[Path, ContentType("text/plain")]: + with open(txt_file, "a") as file: + file.write(input_string) + return txt_file + + .. tab-item:: Convert a PDF's first page to an image + + .. code-block:: python + + from bentoml.validators import ContentType + from typing import Annotated # Python 3.9 or above + from typing_extensions import Annotated # Older than 3.9 + from PIL import Image as im + import bentoml + + @bentoml.service + class PDFtoImage: + @bentoml.api + def pdf_first_page_as_image( + self, + pdf: Annotated[Path, ContentType("application/pdf")], + ) -> Image: + from pdf2image import convert_from_path + + pages = convert_from_path(pdf) + return pages[0].resize(pages[0].size, im.ANTIALIAS) + + .. tab-item:: Speed up an audio file + + .. code-block:: python + + from pathlib import Path + from bentoml.validators import ContentType + from typing import Annotated # Python 3.9 or above + from typing_extensions import Annotated # Older than 3.9 + import bentoml + + @bentoml.service + class AudioSpeedUp: + @bentoml.api + def speed_up_audio( + self, + context: bentoml.Context, + audio: Annotated[Path, ContentType("audio/mpeg")], + velocity: float, + ) -> Annotated[Path, ContentType("audio/mp3")]: + + import os + from pydub import AudioSegment + + output_path = os.path.join(context.temp_dir, "output.mp3") + sound = AudioSegment.from_file(audio) + sound = sound.speedup(velocity) + sound.export(output_path, format="mp3") + return Path(output_path) + If you don't want to save temporary files to disk, you can return the data as ``bytes`` instead of ``pathlib.Path`` with properly annotated ``ContentType``. This is efficient for Services that generate data on the fly. Tensors @@ -279,39 +358,45 @@ The ``DataframeSchema`` validator supports the following two orientations, which Images ^^^^^^ -BentoML Services can work with images through the PIL library or ``pathlib.Path``. +BentoML Services can handle images through ``PIL.Image.Image`` and ``pathlib.Path``. -Here is an example of using PIL: +.. tab-set:: -.. code-block:: python + .. tab-item:: PIL.Image.Image - from PIL.Image import Image as PILImage - import bentoml + You can directly pass image objects through ``PIL.Image.Image``. - @bentoml.service - class MnistPredictor: - @bentoml.api - def infer(self, input: PILImage) -> int: - # Image processing and inference logic - ... + .. code-block:: python -Alternatively, you can use ``pathlib.Path`` with a ``ContentType`` validator to handle image files: + from PIL import Image as im + from PIL.Image import Image + import bentoml -.. code-block:: python + @bentoml.service + class ImageResize: - from pathlib import Path - from typing import Annotated # Python 3.9 or above - from typing_extensions import Annotated # Older than 3.9 - from bentoml.validators import ContentType - import bentoml + @bentoml.api + def generate(self, image: Image, height: int = 64, width: int = 64) -> Image: + size = height, width + return image.resize(size, im.LANCZOS) - @bentoml.service - class MnistPredictor: - @bentoml.api - def infer(self, input: Annotated[Path, ContentType('image/jpeg')) -> int: - ... + .. tab-item:: pathlib.Path + + You can use ``pathlib.Path`` with a ``ContentType`` validator to handle image files: + + .. code-block:: python + + from pathlib import Path + from typing import Annotated # Python 3.9 or above + from typing_extensions import Annotated # Older than 3.9 + from bentoml.validators import ContentType + import bentoml -This is particularly useful when dealing with image uploads in web applications or similar scenarios. + @bentoml.service + class MnistPredictor: + @bentoml.api + def infer(self, input: Annotated[Path, ContentType('image/jpeg')]) -> int: + ... Compound ^^^^^^^^ diff --git a/examples/io-descriptors/README.md b/examples/io-descriptors/README.md deleted file mode 100644 index 54e63fa4817..00000000000 --- a/examples/io-descriptors/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# BentoML Input/Output Types Tutorial - -BentoML supports a wide range of data types when creating a Service API. The data types can be catagorized as follows: -- Python Standards: `str`, `int`, `float`, `list`, `dict` etc. -- Pydantic field types: see [Pydantic types documentation](https://field-idempotency--pydantic-docs.netlify.app/usage/types/). -- ML specific types: `nummpy.ndarray`, `torch.Tensor` , `tf.Tensor` for tensor data, `pd.DataFrame` for tabular data, `PIL.Image.Image` for -Image data, and `pathlib.Path` for files such as audios, images, and pdfs. - -When creating a Bentoml Service, you should use Python's type annotations to define the expected input and output types for each API endpoint. This -step can not only help validate the data against the specified schema, but also enhances the clarity and readability of your code. Type annotations play -an important role in generating the BentoML API, client, and Service UI components, ensuring a consitent and predictable interaction with the Service. - -You can also use `pydantic.Field` to set additional information about service parameters, such as default values and descriptions. This improves the API's -usability and provides basic documentation. - -In this tutorial, you will learn how to set different input and output types for BentoML Services. - -## Installing Dependencies - -Let's start with the environment. We recommend using virtual environment for better package handling. - -```bash -python -m venv io-descriptors-example -source io-descriptors-example/bin/activate -pip install -r requirements.txt -``` - -## Running a Service -7 different API Services are implemented in `service.py`, with diversed input/output types. When running, you should specified the class name of the Service -you'd like to run inside `bentofile.yaml`. - -```yaml -service: "service.py:AudioSpeedUp" -include: - - "service.py" -``` - -In the above configuration through `bentofile.yaml`, we're running the `AudioSpeedUp` Service, which you can find on line 62 of `service.py`. When running a different -Service, simply replace `AudioSpeedUp` with the class name of the Service. - -For example, if you want to run the first Service `ImageResize`, you can configure the `bentofile.yaml` as follows: - -```yaml -service: "service.py:ImageResize" -include: - - "service.py" -``` - -After you finished configuring `bentofile.yaml`, run `bentoml serve .` to deploy the Service locally. You can then interact with the auto-generated swagger UI to play -around with each different API endpoints. - -## Different data types - -### Standard Python types - -The following demonstrates a simple addtion Service, with both inputs and output as float parameters. You can -obviously change the type annotation to `int`, `str` etc. to get familiar with the interaction between type -annotaions and the auto-generated Swagger UI when deploying locally.\ - -```python -@bentoml.service() -class AdditionService: - - @bentoml.api() - def add(self, num1: float, num2: float) -> float: - return num1 + num2 -``` - -### Files - -Files are handled through `pathlib.Path` in BentoML (which means you should handle the file as a file path in your API implementation as well as on the client side). -Most file types can be specified through `bentoml.validators.Contentype()`. The input of this function follows the standard of the -request format (such as `text/plain`, `application/pdf`, `audio/mp3` etc.). - -##### Appending Strings to File example -```python -@bentoml.service() -class AppendStringToFile: - - @bentoml.api() - def append_string_to_eof( - self, - txt_file: t.Annotated[Path, bentoml.validators.ContentType("text/plain")], input_string: str - ) -> t.Annotated[Path, bentoml.validators.ContentType("text/plain")]: - with open(txt_file, "a") as file: - file.write(input_string) - return txt_file -``` - -Within `service.py`, example API Services with 4 different file types are implemented (audio, image, text file, and pdf file). The functionality of each Service -is quite simple and self-explanatory. - -Notice that for class `ImageResize`, two different API endpoints are implemented. This is because BentoML can support images parameters directly through -`PIL.Image.Image`, which means that image objects can be directly passed through clients, instead of a file object. - -The last two Services are examples of having `numpy.ndarray` or `pandas.DataFrame` as input parameters. Since they all work quite similarly with the above examples, -we will not specifically explain them in this tutorial. You can try to write revise the Service with `torch.Tensor` as input to check your understanding. - -To serve the these examples locally, run `bentoml serve .` - -```bash -$ bentoml serve . - -2024-03-22T19:25:24+0000 [INFO] [cli] Starting production HTTP BentoServer from "service:ImageResize" listening on http://localhost:3000 (Press CTRL+C to quit) -``` - -Open your web browser at http://0.0.0.0:3000 to view the Swagger UI for sending test requests. - -You may also send request with `curl` command or any HTTP client, e.g.: - -```bash -curl -X 'POST' \ - 'http://localhost:3000/transpose' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "tensor": [ - [0, 1, 2, 3], - [4, 5, 6, 7] - ] -}' -``` - -## Deploy to BentoCloud -Run the following command to deploy this example to BentoCloud for better management and scalability. [Sign up](https://www.bentoml.com/) if you haven't got a BentoCloud account. -```bash -bentoml deploy . -``` -For more information, see [Create Deployments](https://docs.bentoml.com/en/latest/scale-with-bentocloud/deployment/create-deployments.html). diff --git a/examples/io-descriptors/bentofile.yaml b/examples/io-descriptors/bentofile.yaml deleted file mode 100644 index fb451b42d60..00000000000 --- a/examples/io-descriptors/bentofile.yaml +++ /dev/null @@ -1,3 +0,0 @@ -service: "service.py:AudioSpeedUp" -include: - - "service.py" diff --git a/examples/io-descriptors/requirements.txt b/examples/io-descriptors/requirements.txt deleted file mode 100644 index 3f2b5da9ae4..00000000000 --- a/examples/io-descriptors/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -bentoml -torch -pydub -pdf2img -pandas diff --git a/examples/io-descriptors/service.py b/examples/io-descriptors/service.py deleted file mode 100644 index 5fe2b430bf4..00000000000 --- a/examples/io-descriptors/service.py +++ /dev/null @@ -1,111 +0,0 @@ -import typing as t -from pathlib import Path - -import numpy as np -import pandas as pd -import torch -from PIL import Image as im -from PIL.Image import Image -from pydantic import Field - -import bentoml -from bentoml.validators import DataframeSchema -from bentoml.validators import DType - - -@bentoml.service() -class ImageResize: - @bentoml.api() - def generate(self, image: Image, height: int = 64, width: int = 64) -> Image: - size = height, width - return image.resize(size, im.LANCZOS) - - @bentoml.api() - def generate_with_path( - self, - image: t.Annotated[Path, bentoml.validators.ContentType("image/jpeg")], - height: int = 64, - width: int = 64, - ) -> Image: - size = height, width - image = im.open(image) - return image.resize(size, im.LANCZOS) - - -@bentoml.service() -class AdditionService: - @bentoml.api() - def add(self, num1: float, num2: float) -> float: - return num1 + num2 - - -@bentoml.service() -class AppendStringToFile: - @bentoml.api() - def append_string_to_eof( - self, - context: bentoml.Context, - txt_file: t.Annotated[Path, bentoml.validators.ContentType("text/plain")], - input_string: str, - ) -> t.Annotated[Path, bentoml.validators.ContentType("text/plain")]: - with open(txt_file, "a") as file: - file.write(input_string) - return txt_file - - -@bentoml.service() -class PDFtoImage: - @bentoml.api() - def pdf_first_page_as_image( - self, - pdf: t.Annotated[Path, bentoml.validators.ContentType("application/pdf")], - ) -> Image: - from pdf2image import convert_from_path - - pages = convert_from_path(pdf) - return pages[0].resize(pages[0].size, im.ANTIALIAS) - - -@bentoml.service() -class AudioSpeedUp: - @bentoml.api() - def speed_up_audio( - self, - context: bentoml.Context, - audio: t.Annotated[Path, bentoml.validators.ContentType("audio/mpeg")], - velocity: float, - ) -> t.Annotated[Path, bentoml.validators.ContentType("audio/mp3")]: - import os - - from pydub import AudioSegment - - output_path = os.path.join(context.temp_dir, "output.mp3") - sound = AudioSegment.from_file(audio) - sound = sound.speedup(velocity) - sound.export(output_path, format="mp3") - return Path(output_path) - - -@bentoml.service() -class TransposeTensor: - @bentoml.api() - def transpose( - self, - tensor: t.Annotated[torch.Tensor, DType("float32")] = Field( - description="A 2x4 tensor with float32 dtype" - ), - ) -> np.ndarray: - return torch.transpose(tensor, 0, 1).numpy() - - -@bentoml.service() -class CountRowsDF: - @bentoml.api() - def count_rows( - self, - input: t.Annotated[ - pd.DataFrame, - DataframeSchema(orient="records", columns=["dummy1", "dummy2"]), - ], - ) -> int: - return len(input)