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

feat(grpc): adding service metadata #3278

Merged
merged 2 commits into from Dec 7, 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
64 changes: 62 additions & 2 deletions src/bentoml/_internal/server/grpc/servicer/v1/__init__.py
@@ -1,12 +1,13 @@
from __future__ import annotations

import sys
import typing as t
import asyncio
import logging
from typing import TYPE_CHECKING

import anyio

from .....utils import LazyLoader
from ......exceptions import InvalidArgument
from ......exceptions import BentoMLException
from ......grpc.utils import import_grpc
Expand All @@ -20,14 +21,15 @@
from logging import _ExcInfoType as ExcInfoType # type: ignore (private warning)

import grpc

from google.protobuf import struct_pb2
from ......grpc.v1 import service_pb2 as pb
from ......grpc.v1 import service_pb2_grpc as services
from ......grpc.types import BentoServicerContext
from .....service.service import Service
else:
grpc, _ = import_grpc()
pb, services = import_generated_stubs(version="v1")
struct_pb2 = LazyLoader("struct_pb2", globals(), "google.protobuf.struct_pb2")


def log_exception(request: pb.Request, exc_info: ExcInfoType) -> None:
Expand Down Expand Up @@ -97,4 +99,62 @@ async def Call( # type: ignore (no async types) # pylint: disable=invalid-overr
)
return response

async def ServiceMetadata( # type: ignore (no async types) # pylint: disable=invalid-overridden-method
self,
request: pb.ServiceMetadataRequest, # pylint: disable=unused-argument
context: BentoServicerContext, # pylint: disable=unused-argument
) -> pb.ServiceMetadataResponse:
return pb.ServiceMetadataResponse(
name=service.name,
docs=service.doc,
apis=[
pb.ServiceMetadataResponse.InferenceAPI(
name=api.name,
docs=api.doc,
input=make_descriptor_spec(
api.input.to_spec(), pb.ServiceMetadataResponse
),
output=make_descriptor_spec(
api.output.to_spec(), pb.ServiceMetadataResponse
),
)
for api in service.apis.values()
],
)

return BentoServiceImpl()


if TYPE_CHECKING:
NestedDictStrAny = dict[str, dict[str, t.Any] | t.Any]
TupleAny = tuple[t.Any, ...]


def _tuple_converter(d: NestedDictStrAny | None) -> NestedDictStrAny | None:
# handles case for struct_pb2.Value where nested items are tuple.
# if that is the case, then convert to list.
# This dict is only one level deep, as we don't allow nested Multipart.
if d is not None:
for key, value in d.items():
if isinstance(value, tuple):
d[key] = list(t.cast("TupleAny", value))
elif isinstance(value, dict):
d[key] = _tuple_converter(t.cast("NestedDictStrAny", value))
return d


def make_descriptor_spec(
spec: dict[str, t.Any], pb: type[pb.ServiceMetadataResponse]
) -> pb.ServiceMetadataResponse.DescriptorMetadata:
from .....io_descriptors.json import parse_dict_to_proto

descriptor_id = spec.pop("id")
return pb.DescriptorMetadata(
descriptor_id=descriptor_id,
attributes=struct_pb2.Struct(
fields={
key: parse_dict_to_proto(_tuple_converter(value), struct_pb2.Value())
for key, value in spec.items()
}
),
)
37 changes: 37 additions & 0 deletions src/bentoml/grpc/v1/service.proto
Expand Up @@ -18,6 +18,43 @@ option py_generic_services = true;
service BentoService {
// Call handles methodcaller of given API entrypoint.
rpc Call(Request) returns (Response) {}
// ServiceMetadata returns metadata of bentoml.Service.
rpc ServiceMetadata(ServiceMetadataRequest) returns (ServiceMetadataResponse) {}
}

// ServiceMetadataRequest message doesn't take any arguments.
message ServiceMetadataRequest {}

// ServiceMetadataResponse returns metadata of bentoml.Service.
// Currently it includes name, version, apis, and docs.
message ServiceMetadataResponse {
// DescriptorMetadata is a metadata of any given IODescriptor.
message DescriptorMetadata {
// descriptor_id describes the given ID of the descriptor, which matches with our OpenAPI definition.
optional string descriptor_id = 1;

// attributes is the kwargs of the given descriptor.
google.protobuf.Struct attributes = 2;
}
// InferenceAPI is bentoml._internal.service.inferece_api.InferenceAPI
// that is exposed to gRPC client.
// There is no way for reflection to get information of given @svc.api.
message InferenceAPI {
// name is the name of the API.
string name = 1;
// input is the input descriptor of the API.
DescriptorMetadata input = 2;
// output is the output descriptor of the API.
DescriptorMetadata output = 3;
// docs is the optional documentation of the API.
optional string docs = 4;
}
// name is the service name.
string name = 1;
// apis holds a list of InferenceAPI of the service.
repeated InferenceAPI apis = 2;
// docs is the documentation of the service.
string docs = 3;
}

// Request message for incoming Call.
Expand Down