Skip to content

Commit

Permalink
Merge pull request #266 from socotecio/feat/filterInRequest
Browse files Browse the repository at this point in the history
Feat/filter in request
  • Loading branch information
AMontagu authored Mar 11, 2024
2 parents fba0dbe + 7c0c48a commit 47e146c
Show file tree
Hide file tree
Showing 34 changed files with 2,354 additions and 330 deletions.
16 changes: 16 additions & 0 deletions django_socio_grpc/decorators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
from typing import TYPE_CHECKING, List, Type

from django_socio_grpc.protobuf.message_name_constructor import MessageNameConstructor
from django_socio_grpc.settings import grpc_settings

from .grpc_actions.actions import GRPCAction

if TYPE_CHECKING:
from django_socio_grpc.protobuf.generation_plugin import BaseGenerationPlugin


def grpc_action(
request=None,
Expand All @@ -10,6 +18,8 @@ def grpc_action(
response_stream=False,
use_request_list=False,
use_response_list=False,
message_name_constructor_class: Type[MessageNameConstructor] = None,
use_generation_plugins: List["BaseGenerationPlugin"] = None,
):
"""
Easily register a grpc action into the registry to generate it into the proto file.
Expand All @@ -22,6 +32,8 @@ def grpc_action(
:param response_stream: If true the response message is marqued as stream. Default to false
:param use_request_list: If true the response message is encapsuled in a list message. Default to false
:param use_response_list: If true the response message is encapsuled in a list message. Default to false
:param message_name_constructor_class: The class used to generate the name of the model. Inherit from MessageNameConstructor and chnage logic to have highly customizable name generation.
:param use_generation_plugins: List of generation plugin to use to customize the message.
"""

def wrapper(function):
Expand All @@ -35,6 +47,10 @@ def wrapper(function):
response_stream,
use_request_list,
use_response_list,
message_name_constructor_class=message_name_constructor_class
or grpc_settings.DEFAULT_MESSAGE_NAME_CONSTRUCTOR,
use_generation_plugins=use_generation_plugins
or grpc_settings.DEFAULT_GENERATION_PLUGINS.copy(),
)

return wrapper
195 changes: 87 additions & 108 deletions django_socio_grpc/grpc_actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import abc
import asyncio
import functools
import logging
from asyncio.coroutines import _is_coroutine
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union
Expand All @@ -47,10 +48,15 @@
from rest_framework.serializers import BaseSerializer

from django_socio_grpc.protobuf.exceptions import ProtoRegistrationError
from django_socio_grpc.protobuf.generation_plugin import (
BaseGenerationPlugin,
RequestAsListGenerationPlugin,
ResponseAsListGenerationPlugin,
)
from django_socio_grpc.protobuf.message_name_constructor import MessageNameConstructor
from django_socio_grpc.protobuf.proto_classes import (
FieldCardinality,
EmptyMessage,
FieldDict,
ProtoField,
ProtoMessage,
ProtoRpc,
ProtoService,
Expand All @@ -60,11 +66,12 @@
from django_socio_grpc.request_transformer.grpc_internal_proxy import GRPCInternalProxyContext
from django_socio_grpc.settings import grpc_settings
from django_socio_grpc.utils.debug import ProtoGeneratorPrintHelper
from django_socio_grpc.utils.tools import rreplace
from django_socio_grpc.utils.utils import _is_generator, isgeneratorfunction

from .placeholders import Placeholder

logger = logging.getLogger("django_scoio_grpc.generation")

if TYPE_CHECKING:
from django_socio_grpc.services import Service

Expand All @@ -85,10 +92,19 @@ class GRPCAction:
use_response_list: bool = False
request_message_list_attr: Optional[str] = None
response_message_list_attr: Optional[str] = None
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
)

proto_rpc: Optional[ProtoRpc] = field(init=False, default=None)

def __post_init__(self):
assert issubclass(
self.message_name_constructor_class, MessageNameConstructor
), "message_name_constructor_class need to be a subclass of MessageNameConstructor"
if isinstance(self.function, SyncToAsync):
base_function = self.function

Expand All @@ -103,6 +119,35 @@ async def func(*args, **kwargs):
if isgeneratorfunction(self.function):
self._is_generator = _is_generator

def _maintain_compat(self):
"""
Transform old arguments to the correct plugins
"""
# copy_plugin = self.use_generation_plugins.copy
warning_message = "You are using {0} argument in grpc_action. This argument is deprecated and has been remplaced by a specific GenerationPlugin. Please update following the documentation: https://django-socio-grpc.readthedocs.io/en/stable/features/proto-generation.html#proto-generation-plugins"
if self.use_request_list:
logger.warning(warning_message.format("use_request_list"))

if self.request_message_list_attr:
logger.warning(warning_message.format("request_message_list_attr"))
self.use_generation_plugins.insert(
0,
RequestAsListGenerationPlugin(
list_field_name=self.request_message_list_attr or "results"
),
)

if self.use_response_list:
logger.warning(warning_message.format("use_response_list"))
if self.response_message_list_attr:
logger.warning(warning_message.format("response_message_list_attr"))
self.use_generation_plugins.insert(
0,
ResponseAsListGenerationPlugin(
list_field_name=self.response_message_list_attr or "results"
),
)

def __set_name__(self, owner, name):
"""
set name function is called automatically by python and allow us
Expand Down Expand Up @@ -131,6 +176,8 @@ def get_action_params(self):
"response_name": self.response_name,
"response_stream": self.response_stream,
"use_response_list": self.use_response_list,
"message_name_constructor_class": self.message_name_constructor_class,
"use_generation_plugins": self.use_generation_plugins,
}

@property
Expand All @@ -147,119 +194,51 @@ def response_message_name(self) -> Optional[str]:
except AttributeError:
return None

def create_proto_message(
self,
message: Optional[Union[str, Type[BaseSerializer], List[FieldDict]]],
message_name: Optional[str],
message_class: Type[ProtoMessage],
action_name: str,
service: Type["Service"],
as_list: bool,
list_field_name: Optional[str],
):
assert not isinstance(message, Placeholder)
try:
prefix = service.get_service_name()

# INFO - AM - 29/12/2023 - (PROTO_DEBUG, step: 30, method: create_proto_message) allow to print the message brut before generation only for what we need
ProtoGeneratorPrintHelper.set_info_proto_message(
prefix=prefix, message_class=message_class
)
ProtoGeneratorPrintHelper.print(
"going to transform ", message, " to proto_message"
)
# INFO - AM - 29/12/2023 - this is the next method that introspect serializer to register data to then generate proto
proto_message = message_class.create(
message,
base_name=message_name or action_name,
appendable_name=not message_name,
prefix=prefix,
)
# INFO - AM - 29/12/2023 - (PROTO_DEBUG, step:90, method: create_proto_message)
ProtoGeneratorPrintHelper.print(
f"proto_message {proto_message} generated as string"
if isinstance(proto_message, str)
else f"proto_message {proto_message.name} with {len(proto_message.fields)} field generated:",
proto_message,
)
if as_list:
try:
list_field_name = proto_message.serializer.Meta.message_list_attr
except AttributeError:
list_field_name = list_field_name or "results"

fields = [
ProtoField(
name=list_field_name,
field_type=proto_message,
cardinality=FieldCardinality.REPEATED,
)
]
if getattr(service, "pagination_class", None):
fields.append(
ProtoField(
name="count",
field_type="int32",
)
),

base_list_name = action_name
prefixable = True
if isinstance(proto_message, str):
proto_message_name = proto_message
elif not proto_message.imported_from:
proto_message_name = proto_message.base_name
prefixable = proto_message.prefixable
else:
proto_message_name = action_name

base_list_name = rreplace(proto_message_name, message_class.suffix, "", 1)

list_message = message_class(
base_name=f"{base_list_name}List",
fields=fields,
prefix=prefix,
prefixable=prefixable,
)

if not proto_message.serializer:
list_message.comments = proto_message.comments
proto_message.comments = None

return list_message

return proto_message

except TypeError:
raise ProtoRegistrationError(
f"GRPCAction {action_name} has an invalid message type: {type(message)}"
)

def make_proto_rpc(self, action_name: str, service: Type["Service"]) -> ProtoRpc:
assert not isinstance(self.request, Placeholder)
assert not isinstance(self.response, Placeholder)

req_class = res_class = ProtoMessage
if grpc_settings.SEPARATE_READ_WRITE_MODEL:
req_class = RequestProtoMessage
res_class = ResponseProtoMessage

request = self.create_proto_message(
self.request,
self.request_name,
req_class,
action_name,
service,
self.use_request_list,
self.request_message_list_attr,
)
response = self.create_proto_message(
self.response,
self.response_name,
res_class,
action_name,
service,
self.use_response_list,
self.response_message_list_attr,
# INFO - AM - 22/02/2024 - Get the instance of the class that will be used to construct the names of the request and response message. Defautl class is MessageNameConstructor
message_name_constructor = self.message_name_constructor_class(
action_name=action_name,
service=service,
action_request=self.request,
request_name=self.request_name,
action_response=self.response,
response_name=self.response_name,
)

# INFO - AM - 22/02/2024 - Get the actual request name
request_name: str = message_name_constructor.construct_request_name()
request: ProtoMessage = req_class.create(value=self.request, name=request_name)

response_name: str = message_name_constructor.construct_response_name()
response: ProtoMessage = res_class.create(value=self.response, name=response_name)

# DEPRECATED - AM - 22/02/2024 - Used to keep compat before plugin
self._maintain_compat()

for generation_plugin in self.use_generation_plugins:
request, response = generation_plugin.run_validation_and_transform(
service=service,
request_message=request,
response_message=response,
message_name_constructor=message_name_constructor,
)

# DEPRECATED - AM - 22/02/2024 - In version 1.0.0 all empty message will be Empty message
if not request.fields and not request.serializer and not self.request_name:
request = EmptyMessage

# DEPRECATED - AM - 22/02/2024 - In version 1.0.0 all empty message will be Empty message
if not response.fields and not response.serializer and not self.response_name:
response = EmptyMessage

return ProtoRpc(
name=action_name,
request=request,
Expand Down
5 changes: 3 additions & 2 deletions django_socio_grpc/grpc_actions/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING, Optional

from django_socio_grpc.protobuf.proto_classes import ProtoMessage, get_proto_type
from django_socio_grpc.protobuf.message_name_constructor import MessageNameConstructor
from django_socio_grpc.protobuf.proto_classes import get_proto_type

if TYPE_CHECKING:
from django_socio_grpc.generics import GenericService
Expand All @@ -15,7 +16,7 @@ def get_serializer_class(service: "GenericService", action: Optional[str] = None

def get_serializer_base_name(service: "GenericService", action: Optional[str] = None):
serializer = get_serializer_class(service, action)
return ProtoMessage.get_base_name_from_serializer(serializer)
return MessageNameConstructor.get_base_name_from_serializer(serializer)


def get_lookup_field_from_serializer(serializer_instance, service_instance):
Expand Down
17 changes: 16 additions & 1 deletion django_socio_grpc/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
from google.protobuf import empty_pb2
from rest_framework import serializers

from django_socio_grpc.protobuf.generation_plugin import (
FilterGenerationPlugin,
PaginationGenerationPlugin,
ResponseAsListGenerationPlugin,
)

from .decorators import grpc_action
from .grpc_actions.actions import GRPCActionMixin
from .grpc_actions.placeholders import (
Expand Down Expand Up @@ -57,11 +63,16 @@ def get_default_message(model_name, fields="__all__"):
class ListModelMixin(GRPCActionMixin):
@grpc_action(
request=[],
# DEPRECATED - AM - 23/02/2024 - request_name only keept because will generate emptyMessage. Need to be removed in version 1.0.0
request_name=StrTemplatePlaceholder(
f"{{}}List{REQUEST_SUFFIX}", get_serializer_base_name
),
response=SelfSerializer,
use_response_list=True,
use_generation_plugins=[
ResponseAsListGenerationPlugin(),
FilterGenerationPlugin(display_warning_message=False),
PaginationGenerationPlugin(display_warning_message=False),
],
)
def List(self, request, context):
"""
Expand Down Expand Up @@ -119,6 +130,10 @@ class StreamModelMixin(GRPCActionMixin):
),
response=SelfSerializer,
response_stream=True,
use_generation_plugins=[
FilterGenerationPlugin(display_warning_message=False),
PaginationGenerationPlugin(display_warning_message=False),
],
)
def Stream(self, request, context):
"""
Expand Down
Loading

0 comments on commit 47e146c

Please sign in to comment.