GRPCAction

Description

With DSG you can declare custom gRPC actions related to your app service with the decorator grpc_action. Once your service registered, it will create the RPC and its messages in your .proto file after the proto generation.

A gRPC action is a representation of an RPC inside the service where it’s declared. It is composed of a request and a response definitions.

Note

The corresponding proto code extracted from the decorator will be automatically generated by the generateproto command. Do not do it manually.

Example of a basic RPC command of a generated .proto file:

rpc BasicList(BasicRequest) returns (BasicResponse) {}

It can also use a stream as a request/response.

Usage

Import

First of all, you need to import the grpc_action decorator:

from django_socio_grpc.decorators import grpc_action

This decorator can now be used for each action of your service.

Before looking at each argument of this decorator let see its definition:

from typing import List, Type
from django_socio_grpc.protobuf.generation_plugin import BaseGenerationPlugin
from django_socio_grpc.protobuf.message_name_constructor import MessageNameConstructor

grpc_action(
    request: RequestResponseType | None = None,
    response: RequestResponseType | None = None,
    request_name: str | None = None,
    response_name: str | None = None,
    request_stream: bool = False,
    response_stream: bool = False,
    use_request_list: bool = False,
    use_response_list: bool = False,
    message_name_constructor_class: Type[
        MessageNameConstructor
    ] = grpc_settings.DEFAULT_MESSAGE_NAME_CONSTRUCTOR
    use_generation_plugins: List[BaseGenerationPlugin] = field(
        default_factory=grpc_settings.DEFAULT_GENERATION_PLUGINS
    )
)

request response

The request and response arguments can be:
  • a list of FieldDict: the fields of the message, if the list is empty, the message will be of type google.protobuf.Empty. (See example)

  • a Serializer: the serializer describing the message. (See Proto Serializers)

  • a str: the name of the message if already defined in the proto file.

  • a Placeholder: a placeholder to use in the proto file (See Placeholders).

This 4 possibilies are typed like this (to help you understand where the different options and class come from. To see examples refer to Use Cases section):

from typing import List, Optional, TypedDict, Union
from django_socio_grpc.protobuf.typing import FieldDict
from django_socio_grpc.proto_serializers import BaseProtoSerializer
from django_socio_grpc.grpc_actions.placeholders import Placeholder

RequestResponseType = Union[List[FieldDict], Type[BaseProtoSerializer], str, Placeholder]

request_name response_name

By default, the name of the request/response message is generated from the name of the action, the name of the serializer if a serializer is used, and the service name.

Those arguments are used to override this name. Example: Overriding the request and response proto name.

If not set will use the message_name_constructor_class argument as specified in doc

request_stream response_stream

Those arguments are used to mark the RPC request/response as a stream. Example: Streaming.

use_request_list use_response_list

Warning

Both of these arguments are deprecated and will be removed in version 1.0.0. They are replaced by the GenerationPlugin mechanism combined with ListGenerationPlugin

Those arguments are used to encapsulate the message inside a List message. It is useful when returning a list of object with a serializer. Example: Usage of Request And Response List

message_name_constructor

This argument allows you to customize the proto message names generated when no request_name and/or response_name are specified. It expect a class with specific method and behavior and its instance is passed as arguments in the generation plugin mechanism. For more information, please read the specified documentation.

Defaulting to the DEFAULT_MESSAGE_NAME_CONSTRUCTOR setting

use_generation_plugins

This argument allows to customize the proto generated dynamically by DSG to match your needs. It accepts a list of instances of BaseGenerationPlugin.

For more information, please read the generation plugin documentation

Use Cases

override_default_generation_plugins

This argument allows to override the default generation plugins used in the DSG settings. By default it’s False so your services using the “use_generation_plugins” argument will also use the default generation plugins.

Basic FieldDict request and response:

This ExampleService has a Retrieve action (RPC) that takes a uuid as argument and returns a username and a list of items:

from django_socio_grpc.decorators import grpc_action
from django_socio_grpc.generics import GenericService

class ExampleService(GenericService):
    ...

    @grpc_action(
        request=[
            {
                "name": "uuid",
                "type": "string",
            }
        ],
        response=[
            {
                "name": "username",
                "type": "string",
            },
            {
                "name": "items",
                "type": "string",
                "cardinality": "repeated",
            },
        ],
    )
    async def Retrieve(self, request, context):
        ...

This results in the following proto code after the proto generation with the generateproto command:

service ExampleService {
    rpc Retrieve(RetrieveRequest) returns (RetrieveResponse) {}
}

message RetrieveRequest {
    string uuid = 1;
}

message RetrieveResponse {
    string username = 1;
    repeated string items = 2;
}

Overriding the request and response proto name

This ExampleService has a Retrieve action (RPC). By default the name of the proto message will be RetrieveRequest and RetrieveResponse. It is possible to change it by using request_name and response_name arguments:

from django_socio_grpc.decorators import grpc_action
from django_socio_grpc.generics import GenericService

class ExampleService(GenericService):
    ...

    @grpc_action(
        request=[
            {
                "name": "uuid",
                "type": "string",
            }
        ],
        response=[
            {
                "name": "username",
                "type": "string",
            },
            {
                "name": "items",
                "type": "string",
                "cardinality": "repeated",
            },
        ],
        request_name= "CustomRetrieveRequest",
        response_name= "CustomRetrieveResponse"
    )
    async def Retrieve(self, request, context):
        ...

This results in the following proto code after the proto generation with the generateproto command:

service ExampleService {
    rpc Retrieve(CustomRetrieveRequest) returns (CustomRetrieveResponse) {}
}

message CustomRetrieveRequest {
    string uuid = 1;
}

message CustomRetrieveResponse {
    string username = 1;
    repeated string items = 2;
}

Serializers as messages

Serializers can be used to generate the response message as shown in the example below: Here the UserProtoSerializer is used to generate the response message.

from django_socio_grpc.decorators import grpc_action
from django_socio_grpc.proto_serializers import ModelProtoSerializer
from django_socio_grpc.generics import GenericService
from rest_framework import serializers
from rest_framework.pagination import PageNumberPagination
from django.contrib.auth.models import User
from django_socio_grpc.protobuf.generation_plugin import ListGenerationPlugin

class UserProtoSerializer(ModelProtoSerializer):
    username = serializers.CharField()

    class Meta:
        model = User
        fields = ("username",)

class ExampleService(GenericService):
    ...

    # This is used to have the `count` field in the message. Not needed if set by default in the settings
    pagination_class = PageNumberPagination

    @grpc_action(
        request=[],
        response=UserProtoSerializer,
        use_generation_plugins=[ListGenerationPlugin(response=True)],
    )
    async def List(self, request, context):
        ...

This is corresponds to the following proto code after the proto generation with the generateproto command:

service ExampleService {
    rpc List(google.protobuf.Empty) returns (UserListResponse) {}
}

message UserResponse {
    string username = 1;
}

message UserListResponse {
    repeated UserResponse results = 1;
    int32 count = 2;
}

Note

In the UserListResponse message, the results field is a UserResponse message, it is the message generated from the UserProtoSerializer. This field name can be changed using Serializer Meta attr or serializer kwargs. There is also a count field which is the total number of results, it is present only if the pagination is enabled.

Usage of Request And Response List

from rest_framework import serializers
from django_socio_grpc.decorators import grpc_action
from django_socio_grpc.proto_serializers import ModelProtoSerializer
from django_socio_grpc.protobuf.generation_plugin import ListGenerationPlugin

class UserProtoSerializer(ModelProtoSerializer):
    uuid = serializers.UUIDField(read_only=True)
    username = serializers.CharField()
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ("uuid", "username", "password")

@grpc_action(
    request=UserProtoSerializer,
    response=UserProtoSerializer,
    use_generation_plugins=[ListGenerationPlugin(request=True, response=True)]
)
async def BulkCreate(self, request, context):
    return await self._bulk_create(request, context)

This corresponds to the generated proto code:

service ExampleService {
    rpc List(UserListRequest) returns (UserListResponse) {}
}

message UserRequest {
    string username = 1;
    string password = 1;
}

message UserListRequest {
    repeated UserRequest results = 1;
    int32 count = 2;
}

message UserResponse {
    string uuid = 1;
    string username = 1;
}

message UserListResponse {
    repeated UserResponse results = 1;
    int32 count = 2;
}

Note

In the UserListResponse and UserListRequest message, the results field is a UserResponse or UserRequest message, it is the message generated from the UserProtoSerializer. This field name can be changed using Serializer Meta attr or serializer kwargs. It is not possible to change them separately for now. There is also a count field which is the total number of results, it is present only if the pagination is enabled. This field is not used for Request.

Streaming

You can use the request_stream and response_stream arguments to mark the RPC as a stream, as shown in the following example (See Streaming doc for implementation ):

from django_socio_grpc.decorators import grpc_action

@grpc_action(
    request="google.protobuf.Empty",
    response=[{"name": "str", "type": "string"}],
    response_stream=True,
)
async def Stream(self, request, context):
    ...

This is equivalent to:

rpc Stream(google.protobuf.Empty) returns (stream StreamResponse) {}

Placeholders

Placeholders are objects that will be replaced in the service registration step. They are useful when you want to use arguments that should be overwritten in subclasses (Meaning when you are coding your own Mixins).

They define a resolve method that will be called with the service instance as argument.

# service.py
from django_socio_grpc.grpc_actions.placeholders import Placeholder

# This placeholder always resolves to "MyRequest"
class RequestNamePlaceholder(Placeholder):
    def resolve(self, service: GenericService):
        return "MyRequest"

In a service class, you can use placeholders in any of the grpc_action arguments:

# service.py
from django_socio_grpc.generics import GenericService
from django_socio_grpc.grpc_actions.placeholders import AttrPlaceholder, SelfSerializer

class ExampleSuperService(GenericService):

    @grpc_action(
        request=AttrPlaceholder("_request"),
        request_name=RequestNamePlaceholder, # RequestNamePlaceholder comes from the doc code just above
        response=SelfSerializer,
        response_name = "MyResponse",
    )
    def Route(self, request, context):
        ...

class ExampleSubService(ExampleSuperService):

    serializer_class = MySerializer
    _request = []

    def Route(self, request, context):
        ...

This is gets transformed into the following proto code after the proto generation with the generateproto command:

service ExampleSubService {
    rpc Route(MyRequest) returns (MyResponse) {}
}

// The name of the message is "MyRequest" because of the placeholder
message MyRequest {
    // This message is empty because _request is an empty list
}

message MyResponse {
    ...
    // Defined by MySerializer
}

There are a few predefined placeholders:

FnPlaceholder

Resolves to the result of a function.

# django_socio_grpc.grpc_actions.placeholders.FnPlaceholder

def fn(service) -> str:
    return "Ok"

FnPlaceholder(fn) == "Ok"

AttrPlaceholder

Resolves to a named class attribute of the service.

# django_socio_grpc.grpc_actions.placeholders.AttrPlaceholder

AttrPlaceholder("my_attribute") == service.my_attribute

SelfSerializer

Resolves to the serializer_class of the service.

# django_socio_grpc.grpc_actions.placeholders.SelfSerializer

SelfSerializer == service.serializer_class

StrTemplatePlaceholder

Resolves to a string template with either service attributes names or functions as parameter. It uses str.format to inject the values.

# django_socio_grpc.grpc_actions.placeholders.StrTemplatePlaceholder

def fn(service) -> str:
    return "Ok"

StrTemplatePlaceholder("{}Request{}", "My", fn) == "MyRequestOk"

LookupField

Resolves to the service lookup field message. For for information about lookup_field or it’s implementation see Make a custom retrieve

from django_socio_grpc.generics import GenericService

class Serializer(BaseSerializer):
    """
    This is only for LookupField. Use a proto serializer imported from django_socio_grpc.proto_serializer in real code.
    """
    uuid = serializers.CharField()

# If declaring a service like this
class Service(GenericService):
    serializer_class = Serializer
    lookup_field = "uuid"

# Then if using LookupField placeholder in grpc_action's request or response parameter it will transform at runtime to

# django_socio_grpc.grpc_actions.placeholders.LookupField
LookupField == [{
    "name": "uuid",
    "type": "string", # This is the type of the field in the serializer
}]

Force Message for Known Method

You can use the grpc action decorator on the known method to override the default message that comes from mixins.

# service.py
from django_socio_grpc.decorators import grpc_action
from django_socio_grpc.generics import AsyncModelService
from my_app.models import MyModel # Replace by your model
from my_app.serializers import MyModelProtoSerializer # Replace by your serializer

class MyModelService(AsyncModelService):
    queryset = MyModel.objects.all().order_by("uuid")
    serializer_class = MyModelProtoSerializer

    @grpc_action(
        request=[{"name": "my_example_request", "type": "string"}],
        response=[{"name": "my_example_response", "type": "string"}],
    )
    async def Retrieve(self, request, context):
        pass

This will result in the following proto code after the proto generation with the generateproto command:

import "google/protobuf/empty.proto";

service MyModelController {
    ...
    rpc Retrieve(ExampleRetrieveRequest) returns (ExampleRetrieveResponse) {}
    ...
}

...

message ExampleRetrieveRequest {
    string my_example_request = 1;
}

message ExampleRetrieveResponse {
    string my_example_response = 1;
}

Comments

You can add comments to your request/response fields by using the comment key when using a FieldDict as shown in the following example. The comment will be added to the corresponding field in the proto file.

from django_socio_grpc.generics import GenericService
from django_socio_grpc.decorators import grpc_action

class Service(GenericService):
    ...

    @grpc_action(
        request=[],
        response=[
            {
                "name": "username",
                "type": "string",
                "comment": "This is my proto comment",
            },
        ],
    )
    async def Retrieve(self, request, context):
        ...

This will result in the following generated proto code:

service Service {
    rpc Retrieve(RetrieveRequest) returns (RetrieveResponse) {}
}

message RetrieveRequest {
}

message RetrieveResponse {
    // This is my proto comment
    string username = 1;
}