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(ci): fixing tests [WIP] #4183

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion src/bentoml/_internal/client/__init__.py
Expand Up @@ -30,7 +30,7 @@ def __init__(self, svc: Service, server_url: str):
self._svc = svc
self.server_url = server_url

if svc is not None and len(svc.apis) == 0:
if len(svc.apis) == 0:
raise BentoMLException("No APIs were found when constructing client.")

self.endpoints = []
Expand Down
12 changes: 12 additions & 0 deletions src/bentoml/exceptions.py
Expand Up @@ -119,3 +119,15 @@ class ImportServiceError(BentoMLException):
"""Raised when BentoML failed to import the user's service file."""

pass


class UnservableException(StateException):
"""Raised when a service is not servable."""

pass


class ServerStateException(StateException):
"""Raised when a server API requiring the BentoML server to be running is executed when the server is not running."""

pass
96 changes: 54 additions & 42 deletions src/bentoml/server.py
Expand Up @@ -10,24 +10,26 @@
from abc import ABC
from abc import abstractmethod
from typing import TYPE_CHECKING
from warnings import warn

from simple_di import Provide
from simple_di import inject

from ._internal.bento import Bento
from ._internal.client import Client
from ._internal.client import GrpcClient
from ._internal.client import HTTPClient
from ._internal.configuration.containers import BentoMLContainer
from ._internal.service import Service
from ._internal.tag import Tag
from ._internal.utils.analytics.usage_stats import BENTOML_SERVE_FROM_SERVER_API
from .exceptions import BentoMLException
from .exceptions import InvalidArgument
from .exceptions import ServerStateException
from .exceptions import UnservableException

if TYPE_CHECKING:
from types import TracebackType

from ._internal.client import Client
from ._internal.client import GrpcClient
from ._internal.client import HTTPClient

_FILE: t.TypeAlias = None | int | t.IO[t.Any]


Expand All @@ -37,7 +39,10 @@
__all__ = ["Server", "GrpcServer", "HTTPServer"]


class Server(ABC):
ClientType = t.TypeVar("ClientType", bound=Client)


class Server(ABC, t.Generic[ClientType]):
servable: str | Bento | Tag | Service
host: str
port: int
Expand All @@ -63,15 +68,16 @@ def __init__(
timeout: float = 10,
):
if bento is not None:
if not servable:
logger.warning(
"'bento' is deprecated, either remove it as a kwargs or pass '%s' as the first positional argument",
bento,
if servable is None: # type: ignore # dealing with backwards compatibility, where a user has set bento argument manually.
warn(
f"serving using the 'bento' argument is deprecated, either remove it as a kwarg or pass '{bento}' as the first positional argument",
DeprecationWarning,
stacklevel=2,
)
servable = bento
else:
raise BentoMLException(
"Cannot use both 'bento' and 'servable' as kwargs as 'bento' is deprecated."
raise InvalidArgument(
"Cannot use both 'bento' and 'servable' arguments; as 'bento' is deprecated, set 'servable' instead."
)

self.servable = servable
Expand All @@ -84,7 +90,7 @@ def __init__(
bento_str = str(servable)
elif isinstance(servable, Service):
if not servable.is_service_importable():
raise BentoMLException(
raise UnservableException(
"Cannot use 'bentoml.Service' as a server if it is defined in interactive session or Jupyter Notebooks."
)
bento_str, working_dir = servable.get_service_import_origin()
Expand Down Expand Up @@ -130,7 +136,7 @@ def start(
stdout: _FILE = None,
stderr: _FILE = None,
text: bool | None = None,
):
) -> t.ContextManager[ClientType]:
"""Start the server programmatically.

To get the client, use the context manager.
Expand Down Expand Up @@ -158,9 +164,9 @@ def __init__(__inner_self):
logger.debug(f"Starting server with arguments: {self.args}")
default_io_descriptor = None if blocking else subprocess.PIPE
if text is None:
logger.warning(
"Setting text to True will be the default behaviour for bentoml 2.x. Please set it explicitly to avoid breaking changes.\n"
+ ' Example: "server.start(text=False, ...)"',
warn(
"Setting text to True will be the default behavior for bentoml 2.x. Set it explicitly to avoid breaking changes.\n"
+ "For Example: 'server.start(text=False, ...)'"
)
self.process = subprocess.Popen(
self.args,
Expand All @@ -178,57 +184,54 @@ def __init__(__inner_self):
except KeyboardInterrupt:
pass

def __enter__(__inner_self):
def __enter__(__inner_self) -> ClientType:
return self.get_client()

def __exit__(
__inner_self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
_exc_type: type[BaseException] | None,
_exc_value: BaseException | None,
_traceback: TracebackType | None,
):
self.stop()

return _Manager()

def get_client(self) -> Client | None:
def get_client(self) -> ClientType:
if self.process is None:
# NOTE: if the process is None, we reset this envvar
del os.environ[BENTOML_SERVE_FROM_SERVER_API]
logger.warning(
raise ServerStateException(
"Attempted to get a client for a BentoML server that was not running! Try running 'bentoml.*Server.start()' first."
)
return
assert self.process is not None
out_code = self.process.poll()
if out_code == 0:
# NOTE: if the process is None, we reset this envvar
del os.environ[BENTOML_SERVE_FROM_SERVER_API]
logger.warning(
raise ServerStateException(
"Attempted to get a client from a BentoML server that has already exited! You can run '.start()' again to restart it."
)
return
elif out_code is not None:
# NOTE: if the process is None, we reset this envvar
del os.environ[BENTOML_SERVE_FROM_SERVER_API]
logs = "Attempted to get a client from a BentoML server that has already exited with an error!\nServer Output:\n"
if self.process.stdout is not None and not self.process.stdout.closed:
s = self.process.stdout.read()
logs += textwrap.indent(
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4 # type: ignore # may be string
)
if self.process.stderr is not None and not self.process.stderr.closed:
logs += "\nServer Error:\n"
s = self.process.stderr.read()
logs += textwrap.indent(
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4 # type: ignore # may be string
)
logger.warning(logs)
return
raise ServerStateException(logs)
return self._get_client()

@abstractmethod
def _get_client(self) -> Client | None:
def _get_client(self) -> ClientType:
pass

def stop(self) -> None:
Expand All @@ -244,20 +247,22 @@ def stop(self) -> None:
logger.warning(
"Attempted to stop a BentoML server that has already exited!"
)
return
elif out_code is not None:
logs = "Attempted to stop a BentoML server that has already exited with an error!\nServer Output:\n"
if self.process.stdout is not None and not self.process.stdout.closed:
s = self.process.stdout.read()
logs += textwrap.indent(
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4 # type: ignore # may be string
)
if self.process.stderr is not None and not self.process.stderr.closed:
logs += "\nServer Error:\n"
s = self.process.stderr.read()
logs += textwrap.indent(
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4 # type: ignore # may be string
)
logger.warning(logs)
return

if sys.platform == "win32":
os.kill(self.process.pid, signal.CTRL_C_EVENT)
Expand All @@ -273,8 +278,10 @@ def stop(self) -> None:
self.process.wait()

def __enter__(self):
logger.warning(
"Using bentoml.Server as a context manager is deprecated, use bentoml.Server.start instead."
warn(
"Using bentoml.Server as a context manager is deprecated, use bentoml.Server.start instead.",
DeprecationWarning,
stacklevel=2,
)

return self
Expand All @@ -291,7 +298,7 @@ def __exit__(
logger.error(f"Error stopping server: {e}", exc_info=e)


class HTTPServer(Server):
class HTTPServer(Server[HTTPClient]):
_client: HTTPClient | None = None

@inject
Expand Down Expand Up @@ -347,13 +354,18 @@ def __init__(

self.args.extend(construct_ssl_args(**ssl_args))

def get_client(self) -> HTTPClient:
return super().get_client()

def client(self) -> HTTPClient | None:
logger.warning(
"'Server.client()' is deprecated, use 'Server.get_client()' instead."
warn(
"'Server.client()' is deprecated, use 'Server.get_client()' instead.",
DeprecationWarning,
stacklevel=2,
)
return t.cast("HTTPClient | None", self.get_client())
return self._get_client()

def _get_client(self) -> HTTPClient | None:
def _get_client(self) -> HTTPClient:
if self._client is None:
from .client import HTTPClient

Expand All @@ -364,7 +376,7 @@ def _get_client(self) -> HTTPClient | None:
return self._client


class GrpcServer(Server):
class GrpcServer(Server[GrpcClient]):
_client: GrpcClient | None = None

@inject
Expand Down Expand Up @@ -423,7 +435,7 @@ def __init__(
if grpc_protocol_version is not None:
self.args.extend(["--protocol-version", str(grpc_protocol_version)])

def _get_client(self) -> GrpcClient | None:
def _get_client(self) -> GrpcClient:
if self._client is None:
from .client import GrpcClient

Expand Down