├── .deepsource.toml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── index.md └── quickstart.md ├── grpc_django ├── __init__.py ├── apps.py ├── exceptions.py ├── interfaces.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── generate_grpc_stubs.py │ │ └── run_grpc_server.py ├── migrations │ └── __init__.py ├── models.py ├── protobuf_to_dict.py ├── rpcs.py ├── server.py ├── service.py ├── settings.py ├── utils │ ├── __init__.py │ └── interceptors │ │ ├── __init__.py │ │ ├── _interceptor.py │ │ └── bases.py └── views.py ├── manage.py ├── mkdocs.yml ├── requirements.txt ├── runtests.py ├── setup.py └── tests ├── __init__.py ├── grpc.py ├── grpc_codegen ├── __init__.py ├── test_pb2.py └── test_pb2_grpc.py ├── protos └── test.proto ├── rpcs.py ├── settings.py ├── test_server.py ├── urls.py └── wsgi.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = [ 4 | "runtests.py" 5 | ] 6 | 7 | exclude_patterns = [ 8 | "docs/*" 9 | ] 10 | 11 | [[analyzers]] 12 | name = "python" 13 | enabled = true 14 | 15 | [analyzers.meta] 16 | runtime_version = "3.x.x" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | - "3.7" 6 | # command to run tests 7 | script: 8 | - coverage run --source=grpc_django manage.py test 9 | before_install: 10 | - pip install -r requirements.txt 11 | - pip install coveralls 12 | after_success: 13 | coveralls 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sohel Tarir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GRPC Django 2 | [![PyPI version shields.io](https://img.shields.io/pypi/v/grpc-django.svg?style=for-the-badge)](https://pypi.python.org/pypi/grpc-django/) 3 | [![Travis (.org)](https://img.shields.io/travis/soheltarir/grpc-django?style=for-the-badge)](https://travis-ci.org/soheltarir/grpc-django) 4 | [![PyPI - License](https://img.shields.io/pypi/l/grpc-django?style=for-the-badge)](https://github.com/soheltarir/grpc-django/edit/master/LICENSE) 5 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grpc-django?style=for-the-badge)](https://www.python.org/) 6 | 7 | **GRPC Framework written on top of Django and Django REST Framework** 8 | 9 | Documentation for the project is available at https://soheltarir.github.io/grpc-django/ 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # GRPC Django Framework 2 | 3 | Python Django gRPC microservice framework 4 | 5 | ## Requirements 6 | GRPC Django requires the following 7 | * Python (>=3.5) 8 | * Django (1.11, >=2.0) 9 | * Django REST framework 10 | 11 | ## Installation 12 | Install using `pip`, 13 | ``` 14 | pip install grpc-django 15 | ``` 16 | Add `'grpc_django'` to your `INSTALLED_APPS` setting. 17 | ``` 18 | INSTALLED_APPS = ( 19 | ... 20 | 'grpc_django', 21 | ) 22 | ``` 23 | 24 | ## Quickstart 25 | The [quickstart guide](quickstart.md) is the fastest way to get up and running, and exposing RPCs with GRPC Django framework. 26 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | We're going to create a simple service to view the users in the system. 3 | 4 | ## Project Setup 5 | Create a new Django project named grpc_tutorial 6 | ``` 7 | # Create the project directory 8 | $ mkdir grpc_tutorial 9 | $ cd grpc_tutorial 10 | 11 | # Create a virtualenv to isolate our package dependencies locally 12 | $ virtualenv env 13 | $ source env/bin/activate 14 | 15 | # Install the packages into the virtualenv 16 | $ pip install django 17 | $ pip install djangorestframework 18 | $ pip install grpc-django 19 | 20 | # Set up a new project with a single application 21 | $ django-admin startproject config . # Note the trailing '.' character 22 | $ python manage.py startapp users 23 | ``` 24 | 25 | The project layout should look like: 26 | ``` 27 | $ pwd 28 | /grpc_tutorial 29 | $ find . 30 | . 31 | ./grpc_tutorial 32 | ./grpc_tutorial/__init__.py 33 | ./grpc_tutorial/settings.py 34 | ./grpc_tutorial/urls.py 35 | ./grpc_tutorial/wsgi.py 36 | ./users 37 | ./users/migrations 38 | ./users/migrations/__init__.py 39 | ./users/models.py 40 | ./users/__init__.py 41 | ./users/apps.py 42 | ./users/admin.py 43 | ./users/tests.py 44 | ./users/views.py 45 | ./manage.py 46 | ``` 47 | Now sync your database for the first time: 48 | ``` 49 | python manage.py migrate 50 | ``` 51 | Create the Protocol Buffer file `users/user.proto` and define the GRPC service: 52 | ```proto 53 | syntax = "proto3"; 54 | 55 | package user; 56 | 57 | message User { 58 | int64 id = 1; 59 | string username = 2; 60 | string first_name = 3; 61 | string last_name = 4; 62 | } 63 | 64 | message GetPayload { 65 | int64 id = 1; 66 | } 67 | 68 | message Empty {} 69 | 70 | service UserService { 71 | rpc GetUser (GetPayload) returns (User); 72 | rpc ListUsers (Empty) returns (stream User); 73 | } 74 | ``` 75 | 76 | ## Settings 77 | Add `'grpc_django'` to `INSTALLED_APPS`. The settings module will be in `grpc_tutorial/settings.py`. 78 | ``` 79 | INSTALLED_APPS = ( 80 | ... 81 | 'grpc_django', 82 | ) 83 | ``` 84 | Add `GRPC_SETTINGS` in the settings module 85 | ```python 86 | from grpc_django import GRPCSettings, GRPCService 87 | 88 | 89 | GRPC_SETTINGS = GRPCSettings( 90 | services=[GRPCService( 91 | # Name of the service as defined in .proto definition 92 | name='UserService', 93 | # The package name as defined in .proto definition (in our case it should look like `package user;` 94 | package_name='user', 95 | # The path (relative to `manage.py`) to the .proto definition 96 | proto_path='users/user.proto', 97 | # This will be the list of RPCs similar to `urls.py` definition in Django 98 | rpc_conf='users.rpc' 99 | )] 100 | ) 101 | 102 | ``` 103 | 104 | ## Generating the client and server code 105 | Next you need to generate the gRPC client and server interfaces from your `users/user.proto` service 106 | definition. 107 | 108 | First, install the `grpcio-tools` package: 109 | ``` 110 | $ pip install grpcio-tools 111 | ``` 112 | 113 | Use the following Django management command to generate the Python code: 114 | ``` 115 | $ python manage.py generate_grpc_stubs 116 | ``` 117 | 118 | The above command will generate a python package `grpc_codegen` containing the following modules: 119 | ``` 120 | ./manage.py 121 | ... 122 | ./grpc_codegen 123 | ./grpc_codegen/__init__.py 124 | ./grpc_codegen/user_pb2.py 125 | ./grpc_codegen/user_pb2_grpc.py 126 | ... 127 | ``` 128 | 129 | ## Serializers 130 | Now we're going to define some serializers using Django REST framework. Let's create a new module 131 | named `users/serializers.py` that we'll be used for data representations. 132 | ```python 133 | from django.contrib.auth.models import User 134 | from rest_framework.serializers import ModelSerializer 135 | 136 | 137 | class UserSerializer(ModelSerializer): 138 | class Meta: 139 | model = User 140 | fields = ('id', 'first_name', 'last_name', 'username') 141 | 142 | ``` 143 | Please refer [DjangoRESTFramework - Serializers](https://www.django-rest-framework.org/api-guide/serializers/) for 144 | detailed usage of serializers 145 | 146 | ## RPCs 147 | We are gonna write some RPCs in `users/views.py` which should look familiar to Django Views 148 | or Django REST Framework Views 149 | ```python 150 | from grpc_django.views import RetrieveGRPCView, ServerStreamGRPCView 151 | from grpc_codegen.user_pb2 import User as UserProto 152 | from django.contrib.auth.models import User 153 | from .serializers import UserSerializer 154 | 155 | class GetUser(RetrieveGRPCView): 156 | """ 157 | RPC to view a single user by ID 158 | """ 159 | queryset = User.objects.all() 160 | response_proto = UserProto 161 | serializer_class = UserSerializer 162 | 163 | 164 | class ListUsers(ServerStreamGRPCView): 165 | """ 166 | RPC to list all users 167 | """ 168 | queryset = User.objects.all() 169 | response_proto = UserProto 170 | serializer_class = UserSerializer 171 | ``` 172 | 173 | Similar to **urls.py** in Django, where we define API endpoints and link them to respective views, 174 | in GRPC Django we are gonna link views to corresponding RPCs as defined in the `users/user.proto` file. 175 | Create a module `users/rpcs.py` with the following: 176 | ```python 177 | from grpc_django.interfaces import rpc 178 | from .views import GetUser, ListUsers 179 | 180 | 181 | rpcs = [ 182 | rpc(name='GetUser', view=GetUser), 183 | rpc(name='ListUsers', view=ListUsers) 184 | ] 185 | ``` 186 | 187 | ## Running the Server 188 | Finally you can run your gRPC server by the following command: 189 | ``` 190 | $ python manage.py run_grpc_server 191 | ``` 192 | This will run your gRPC server on `127.0.0.1:55000` 193 | 194 | ## Testing Our Service 195 | To test our gRPC service we need to create a client that would use the generated client code to 196 | access the RPCs. Create a python module `test_grpc_client.py`, and write the following sample code in it: 197 | ```python 198 | import grpc 199 | import sys 200 | 201 | from grpc_codegen.user_pb2 import GetPayload, Empty 202 | from grpc_codegen.user_pb2_grpc import UserServiceStub 203 | 204 | 205 | def run(): 206 | # Create a connection with the server 207 | channel = grpc.insecure_channel("localhost:55000") 208 | try: 209 | grpc.channel_ready_future(channel).result(timeout=10) 210 | except grpc.FutureTimeoutError: 211 | sys.exit('Error connecting to server') 212 | else: 213 | stub = UserServiceStub(channel) 214 | 215 | # Test the GetUser RPC 216 | print("Calling GetUser with id = 1") 217 | response = stub.GetUser(GetPayload(id=1)) 218 | if response: 219 | print("Received response for GetUser: {}".format(response)) 220 | 221 | # Test the ListUsers RPC 222 | print("Calling ListUsers") 223 | response = stub.ListUsers(Empty()) 224 | for _ in response: 225 | print(_) 226 | 227 | # Close the connection 228 | channel.close() 229 | 230 | 231 | if __name__ == "__main__": 232 | run() 233 | 234 | ``` 235 | Run the above code using `python test_grpc_client.py` 236 | -------------------------------------------------------------------------------- /grpc_django/__init__.py: -------------------------------------------------------------------------------- 1 | from .interfaces import ( 2 | IServer as GRPCServer, IService as GRPCService, ISettings as GRPCSettings 3 | ) 4 | 5 | __all__ = ['GRPCServer', 'GRPCService', 'GRPCSettings'] 6 | -------------------------------------------------------------------------------- /grpc_django/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GrpcDjangoConfig(AppConfig): 5 | name = 'grpc_django' 6 | -------------------------------------------------------------------------------- /grpc_django/exceptions.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import grpc 4 | from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError as DjangoValidationError 5 | 6 | 7 | class GrpcServerStartError(Exception): 8 | """ 9 | Failed to start the GRPC server 10 | """ 11 | pass 12 | 13 | 14 | class GrpcException(Exception): 15 | """ 16 | Base class for GRPC exceptions 17 | """ 18 | status_code = grpc.StatusCode.INTERNAL 19 | default_message = "A server error occurred." 20 | 21 | def __init__(self, message): 22 | if message is None: 23 | self.message = self.default_message 24 | else: 25 | self.message = message 26 | 27 | def __str__(self): 28 | return self.message 29 | 30 | 31 | class NotAuthenticated(GrpcException): 32 | status_code = grpc.StatusCode.UNAUTHENTICATED 33 | default_message = "Authentication credentials were not provided." 34 | 35 | 36 | class AuthenticationFailed(GrpcException): 37 | status_code = grpc.StatusCode.UNAUTHENTICATED 38 | default_message = "Incorrect authentication credentials." 39 | 40 | 41 | class PermissionDenied(GrpcException): 42 | status_code = grpc.StatusCode.PERMISSION_DENIED 43 | default_message = "You do not have permission to perform this action." 44 | 45 | 46 | class InvalidArgument(GrpcException): 47 | status_code = grpc.StatusCode.INVALID_ARGUMENT 48 | default_message = "Invalid input." 49 | 50 | 51 | class ValidationError(GrpcException): 52 | status_code = grpc.StatusCode.FAILED_PRECONDITION 53 | default_message = "Invalid input." 54 | 55 | 56 | class ExceptionHandler(object): 57 | _handlers = { 58 | ObjectDoesNotExist: (grpc.StatusCode.NOT_FOUND, str), 59 | MultipleObjectsReturned: (grpc.StatusCode.ALREADY_EXISTS, str), 60 | DjangoValidationError: (grpc.StatusCode.FAILED_PRECONDITION, str), 61 | } 62 | 63 | def __init__(self, context): 64 | self.context = context 65 | 66 | def __call__(self, exc, stack): 67 | print(stack) 68 | if issubclass(exc.__class__, GrpcException): 69 | status_code, message = exc.status_code, str(exc) 70 | elif self._handlers.get(exc.__class__): 71 | status_code, message = self._handlers[exc.__class__][0], self._handlers[exc.__class__][1](exc) 72 | else: 73 | status_code = grpc.StatusCode.UNKNOWN 74 | message = "{}; ErrorId: {}".format(exc, uuid.uuid4()) 75 | self.context.set_code(status_code) 76 | self.context.set_details(message) 77 | return self.context 78 | -------------------------------------------------------------------------------- /grpc_django/interfaces.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from typing import List 3 | 4 | 5 | rpc = namedtuple('rpc', ['name', 'view']) 6 | 7 | 8 | class IService: 9 | _DEFAULT_STUB_MODULE = 'grpc_codegen' 10 | 11 | def __init__( 12 | self, 13 | name: str, 14 | package_name: str, 15 | proto_path: str, 16 | rpc_conf: str, 17 | stub_conf: str = None, 18 | ): 19 | self.name = name 20 | self.package_name = package_name 21 | self.proto_path = proto_path 22 | self.rpc_conf = rpc_conf 23 | self.stub_conf = stub_conf if stub_conf else self._DEFAULT_STUB_MODULE 24 | 25 | 26 | class IServer: 27 | DEFAULT_SERVER_PORT = 55000 28 | DEFAULT_WORKER_COUNT = 1 29 | 30 | def __init__(self, port: int = None, num_of_workers: int = None): 31 | if port and type(port) != int: 32 | raise TypeError("Invalid port provided, should be int") 33 | self.port = port if port else self.DEFAULT_SERVER_PORT 34 | 35 | if num_of_workers and type(num_of_workers) != int: 36 | raise TypeError("Invalid num_of_workers provided, should be int") 37 | self.num_of_workers = num_of_workers if num_of_workers else self.DEFAULT_WORKER_COUNT 38 | 39 | 40 | class ISettings: 41 | DEFAULT_AUTHENTICATION_KEY = 'user' 42 | DEFAULT_CODEGEN_LOCATION = 'grpc_codegen' 43 | 44 | def __init__( 45 | self, 46 | services: List[IService], 47 | server: IServer = None, 48 | auth_user_key: str = None, 49 | stubs: str = None 50 | ): 51 | self.services = services 52 | self.server = server if server else IServer() 53 | self.auth_user_key = auth_user_key if auth_user_key else self.DEFAULT_AUTHENTICATION_KEY 54 | self.stubs = stubs if stubs is not None else self.DEFAULT_CODEGEN_LOCATION 55 | 56 | 57 | __all__ = ['IService', 'ISettings', 'IServer', 'rpc'] 58 | -------------------------------------------------------------------------------- /grpc_django/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soheltarir/grpc-django/890038de2d7b7c36e4fd37ef1727d3568b3f68f5/grpc_django/management/__init__.py -------------------------------------------------------------------------------- /grpc_django/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soheltarir/grpc-django/890038de2d7b7c36e4fd37ef1727d3568b3f68f5/grpc_django/management/commands/__init__.py -------------------------------------------------------------------------------- /grpc_django/management/commands/generate_grpc_stubs.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | from django.core.management import BaseCommand 3 | from grpc_tools import protoc 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Generates server and client stubs using grpc_tools" 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument( 11 | "inclusion_root", nargs=1, 12 | help="Folder path where the target protos reside" 13 | ) 14 | parser.add_argument( 15 | "proto_files", nargs=1, 16 | help="Comma separated values of proto files" 17 | ) 18 | parser.add_argument( 19 | "--dest", dest="destination_path", 20 | help="Destination path of the generated stub outputs" 21 | ) 22 | 23 | def handle(self, *args, **options): 24 | proto_files = options.get("proto_files")[0].split(",") 25 | inclusion_root = options["inclusion_root"][0] 26 | if options.get("destination_path"): 27 | destination_path = options["destination_path"] 28 | else: 29 | destination_path = "./" 30 | well_known_protos_include = pkg_resources.resource_filename( 31 | 'grpc_tools', '_proto') 32 | for proto_file in proto_files: 33 | command = [ 34 | "grpc_tools.protoc", 35 | "--proto_path={}".format(inclusion_root), 36 | "--proto_path={}".format(well_known_protos_include), 37 | "--python_out={}".format(destination_path), 38 | "--grpc_python_out={}".format(destination_path), 39 | ] + [proto_file] 40 | if protoc.main(command) != 0: 41 | self.stderr.write("Failed to generate {}".format(proto_file)) 42 | -------------------------------------------------------------------------------- /grpc_django/management/commands/run_grpc_server.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | from contextlib import contextmanager 4 | from datetime import datetime 5 | from ipaddress import ip_address 6 | 7 | from django.conf import settings as django_settings 8 | from django.core.management import BaseCommand, CommandError 9 | 10 | from grpc_django.server import init_server 11 | from grpc_django.settings import settings 12 | from grpc_django.views import ServerStreamGRPCView 13 | 14 | naiveip_re = re.compile(r"""^(?:(?P(?P\d{1,3}(?:\.\d{1,3}){3}) |):)?(?P\d+)$""", re.X) 15 | 16 | 17 | class Command(BaseCommand): 18 | help = "Starts a GRPC server" 19 | 20 | default_addr = '127.0.0.1' 21 | 22 | def add_arguments(self, parser): 23 | parser.add_argument( 24 | "addrport", nargs="?", 25 | help="Optional port number, or ipaddr:port" 26 | ) 27 | parser.add_argument( 28 | "--workers", dest="max_workers", 29 | help="Number of maximum worker threads" 30 | ) 31 | 32 | @staticmethod 33 | def _get_rpc_method(rpc_call): 34 | if issubclass(rpc_call.cls, ServerStreamGRPCView): 35 | def method(*args): 36 | yield from rpc_call.cls(args[1], args[2]).__call__() 37 | else: 38 | def method(*args): 39 | return rpc_call.cls(args[1], args[2]).__call__() 40 | return method 41 | 42 | @contextmanager 43 | def serve_forever(self, addr, port, **kwargs): 44 | self.stdout.write("Performing system checks...\n\n") 45 | self.check_migrations() 46 | server = init_server(addr, port, max_workers=kwargs.get('max_workers', 1), stdout=self.stdout) 47 | self.stdout.write(datetime.now().strftime('%B %d, %Y - %X')) 48 | self.stdout.write( 49 | "Django version {version}, using settings {settings}\n" 50 | "Starting GRPC server at {addr}:{port}".format(**{ 51 | 'version': self.get_version(), 52 | 'settings': django_settings.SETTINGS_MODULE, 53 | 'addr': addr, 54 | 'port': port 55 | }) 56 | ) 57 | server.start() 58 | yield 59 | server.stop(0) 60 | 61 | @staticmethod 62 | def get_addrport(value): 63 | error_msg = '"{}" is not a valid port number or address:port pair.'.format(value) 64 | parts = value.split(':') 65 | if len(parts) > 2: 66 | raise CommandError(error_msg) 67 | if len(parts) == 1: 68 | addr = settings.server_port 69 | port = parts[0] 70 | if not port.isdigit(): 71 | raise CommandError(error_msg) 72 | else: 73 | addr, port = parts 74 | try: 75 | ip_address(addr) 76 | except ValueError: 77 | raise CommandError(error_msg) 78 | return addr, port 79 | 80 | def handle(self, *args, **options): 81 | # Initialise Settings 82 | if not options.get('addrport'): 83 | addr, port = self.default_addr, settings.server_port 84 | else: 85 | addr, port = self.get_addrport(options['addrport']) 86 | with self.serve_forever(addr=addr, port=port): 87 | try: 88 | while True: 89 | time.sleep(60*60*24) 90 | except KeyboardInterrupt: 91 | pass 92 | -------------------------------------------------------------------------------- /grpc_django/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soheltarir/grpc-django/890038de2d7b7c36e4fd37ef1727d3568b3f68f5/grpc_django/migrations/__init__.py -------------------------------------------------------------------------------- /grpc_django/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | # Create your models here. 5 | class ContextUser(AbstractUser): 6 | """ 7 | Just an abstract model class to represent Auth user coming as context in GRPC requests 8 | """ 9 | 10 | class Meta: 11 | abstract = True 12 | 13 | def __str__(self): 14 | return " ".format([self.first_name, self.last_name]) 15 | -------------------------------------------------------------------------------- /grpc_django/protobuf_to_dict.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import six 3 | import datetime 4 | 5 | from google.protobuf.message import Message 6 | from google.protobuf.descriptor import FieldDescriptor 7 | from google.protobuf.timestamp_pb2 import Timestamp 8 | 9 | __all__ = ["protobuf_to_dict", "TYPE_CALLABLE_MAP", "dict_to_protobuf", 10 | "REVERSE_TYPE_CALLABLE_MAP"] 11 | 12 | Timestamp_type_name = 'Timestamp' 13 | 14 | 15 | def datetime_to_timestamp(dt): 16 | ts = Timestamp() 17 | ts.FromDatetime(dt) 18 | return ts 19 | 20 | 21 | def timestamp_to_datetime(ts): 22 | dt = ts.ToDatetime() 23 | return dt 24 | 25 | 26 | EXTENSION_CONTAINER = '___X' 27 | 28 | TYPE_CALLABLE_MAP = { 29 | FieldDescriptor.TYPE_DOUBLE: float, 30 | FieldDescriptor.TYPE_FLOAT: float, 31 | FieldDescriptor.TYPE_INT32: int, 32 | FieldDescriptor.TYPE_INT64: int if six.PY3 else six.integer_types[1], 33 | FieldDescriptor.TYPE_UINT32: int, 34 | FieldDescriptor.TYPE_UINT64: int if six.PY3 else six.integer_types[1], 35 | FieldDescriptor.TYPE_SINT32: int, 36 | FieldDescriptor.TYPE_SINT64: int if six.PY3 else six.integer_types[1], 37 | FieldDescriptor.TYPE_FIXED32: int, 38 | FieldDescriptor.TYPE_FIXED64: int if six.PY3 else six.integer_types[1], 39 | FieldDescriptor.TYPE_SFIXED32: int, 40 | FieldDescriptor.TYPE_SFIXED64: int if six.PY3 else six.integer_types[1], 41 | FieldDescriptor.TYPE_BOOL: bool, 42 | FieldDescriptor.TYPE_STRING: six.text_type, 43 | FieldDescriptor.TYPE_BYTES: six.binary_type, 44 | FieldDescriptor.TYPE_ENUM: int, 45 | } 46 | 47 | 48 | def repeated(type_callable): 49 | return lambda value_list: [type_callable(value) for value in value_list] 50 | 51 | 52 | def enum_label_name(field, value): 53 | return field.enum_type.values_by_number[int(value)].name 54 | 55 | 56 | def _is_map_entry(field): 57 | return (field.type == FieldDescriptor.TYPE_MESSAGE and 58 | field.message_type.has_options and 59 | field.message_type.GetOptions().map_entry) 60 | 61 | 62 | def protobuf_to_dict(pb, type_callable_map=TYPE_CALLABLE_MAP, use_enum_labels=False, 63 | including_default_value_fields=False): 64 | result_dict = {} 65 | extensions = {} 66 | for field, value in pb.ListFields(): 67 | if field.message_type and field.message_type.has_options and field.message_type.GetOptions().map_entry: 68 | result_dict[field.name] = dict() 69 | value_field = field.message_type.fields_by_name['value'] 70 | type_callable = _get_field_value_adaptor( 71 | pb, value_field, type_callable_map, 72 | use_enum_labels, including_default_value_fields) 73 | for k, v in value.items(): 74 | result_dict[field.name][k] = type_callable(v) 75 | continue 76 | type_callable = _get_field_value_adaptor(pb, field, type_callable_map, 77 | use_enum_labels, including_default_value_fields) 78 | if field.label == FieldDescriptor.LABEL_REPEATED: 79 | type_callable = repeated(type_callable) 80 | 81 | if field.is_extension: 82 | extensions[str(field.number)] = type_callable(value) 83 | continue 84 | # Custom handling for tri bool enum fields 85 | if field.type == FieldDescriptor.TYPE_ENUM and field.enum_type.name == "Bool": 86 | if value == 1: 87 | value = True 88 | elif value == 2: 89 | value = False 90 | else: 91 | value = None 92 | if field.type == FieldDescriptor.CPPTYPE_STRING and value == "Nil": 93 | # Custom handles for String fields receiving 'Nil' value, which is being mimicked by us to handle default 94 | # values for string in proto buf, i.e., empty string ("") 95 | value = "" 96 | elif field.type == FieldDescriptor.CPPTYPE_STRING and value == "None": 97 | # Custom handles for String fields receiving 'None' value, which is being mimicked by us to handle Null 98 | # values for string 99 | value = None 100 | if value is not None: 101 | result_dict[field.name] = type_callable(value) 102 | else: 103 | result_dict[field.name] = None 104 | 105 | # Serialize default value if including_default_value_fields is True. 106 | if including_default_value_fields: 107 | for field in pb.DESCRIPTOR.fields: 108 | # Singular message fields and oneof fields will not be affected. 109 | if (( 110 | field.label != FieldDescriptor.LABEL_REPEATED and 111 | field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE) or 112 | field.containing_oneof): 113 | continue 114 | if field.name in result_dict: 115 | # Skip the field which has been serailized already. 116 | continue 117 | if _is_map_entry(field): 118 | result_dict[field.name] = {} 119 | else: 120 | result_dict[field.name] = field.default_value 121 | 122 | if extensions: 123 | result_dict[EXTENSION_CONTAINER] = extensions 124 | return result_dict 125 | 126 | 127 | def _get_field_value_adaptor(pb, field, type_callable_map=TYPE_CALLABLE_MAP, use_enum_labels=False, 128 | including_default_value_fields=False): 129 | if field.message_type and field.message_type.name == Timestamp_type_name: 130 | return timestamp_to_datetime 131 | if field.type == FieldDescriptor.TYPE_MESSAGE: 132 | # recursively encode protobuf sub-message 133 | return lambda pb: protobuf_to_dict( 134 | pb, type_callable_map=type_callable_map, 135 | use_enum_labels=use_enum_labels, 136 | including_default_value_fields=including_default_value_fields, 137 | ) 138 | 139 | if use_enum_labels and field.type == FieldDescriptor.TYPE_ENUM: 140 | return lambda value: enum_label_name(field, value) 141 | 142 | if field.type in type_callable_map: 143 | return type_callable_map[field.type] 144 | 145 | raise TypeError("Field %s.%s has unrecognised type id %d" % ( 146 | pb.__class__.__name__, field.name, field.type)) 147 | 148 | 149 | REVERSE_TYPE_CALLABLE_MAP = { 150 | } 151 | 152 | 153 | def dict_to_protobuf(pb_klass_or_instance, values, type_callable_map=REVERSE_TYPE_CALLABLE_MAP, strict=True, 154 | ignore_none=False): 155 | """Populates a protobuf model from a dictionary. 156 | 157 | :param pb_klass_or_instance: a protobuf message class, or an protobuf instance 158 | :type pb_klass_or_instance: a type or instance of a subclass of google.protobuf.message.Message 159 | :param dict values: a dictionary of values. Repeated and nested values are 160 | fully supported. 161 | :param dict type_callable_map: a mapping of protobuf types to callables for setting 162 | values on the target instance. 163 | :param bool strict: complain if keys in the map are not fields on the message. 164 | :param bool strict: ignore None-values of fields, treat them as empty field 165 | """ 166 | if isinstance(pb_klass_or_instance, Message): 167 | instance = pb_klass_or_instance 168 | else: 169 | instance = pb_klass_or_instance() 170 | return _dict_to_protobuf(instance, values, type_callable_map, strict, ignore_none) 171 | 172 | 173 | def _get_field_mapping(pb, dict_value, strict): 174 | field_mapping = [] 175 | for key, value in dict_value.items(): 176 | if key == EXTENSION_CONTAINER: 177 | continue 178 | if key not in pb.DESCRIPTOR.fields_by_name: 179 | if strict: 180 | raise KeyError("%s does not have a field called %s" % (pb, key)) 181 | continue 182 | # if value is None: 183 | # value = "" 184 | field_mapping.append((pb.DESCRIPTOR.fields_by_name[key], value, getattr(pb, key, None))) 185 | 186 | for ext_num, ext_val in dict_value.get(EXTENSION_CONTAINER, {}).items(): 187 | try: 188 | ext_num = int(ext_num) 189 | except ValueError: 190 | raise ValueError("Extension keys must be integers.") 191 | if ext_num not in pb._extensions_by_number: 192 | if strict: 193 | raise KeyError( 194 | "%s does not have a extension with number %s. Perhaps you forgot to import it?" % (pb, key)) 195 | continue 196 | ext_field = pb._extensions_by_number[ext_num] 197 | pb_val = pb.Extensions[ext_field] 198 | field_mapping.append((ext_field, ext_val, pb_val)) 199 | 200 | return field_mapping 201 | 202 | 203 | def _dict_to_protobuf(pb, value, type_callable_map, strict, ignore_none): 204 | fields = _get_field_mapping(pb, value, strict) 205 | 206 | for field, input_value, pb_value in fields: 207 | if ignore_none and input_value is None: 208 | continue 209 | if field.label == FieldDescriptor.LABEL_REPEATED: 210 | if field.message_type and field.message_type.has_options and field.message_type.GetOptions().map_entry: 211 | value_field = field.message_type.fields_by_name['value'] 212 | for key, value in input_value.items(): 213 | if value_field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: 214 | _dict_to_protobuf(getattr(pb, field.name)[key], value, type_callable_map, strict, ignore_none) 215 | else: 216 | getattr(pb, field.name)[key] = value 217 | continue 218 | for item in input_value: 219 | if field.type == FieldDescriptor.TYPE_MESSAGE: 220 | m = pb_value.add() 221 | _dict_to_protobuf(m, item, type_callable_map, strict, ignore_none) 222 | elif field.type == FieldDescriptor.TYPE_ENUM and isinstance(item, six.string_types): 223 | pb_value.append(_string_to_enum(field, item)) 224 | else: 225 | pb_value.append(item) 226 | continue 227 | if isinstance(input_value, datetime.datetime): 228 | input_value = datetime_to_timestamp(input_value) 229 | # Instead of setattr we need to use CopyFrom for composite fields 230 | # Otherwise we will get AttributeError: Assignment not allowed to composite field “field name” in protocol message object 231 | getattr(pb, field.name).CopyFrom(input_value) 232 | continue 233 | elif field.type == FieldDescriptor.TYPE_MESSAGE: 234 | _dict_to_protobuf(pb_value, input_value, type_callable_map, strict, ignore_none) 235 | continue 236 | 237 | if field.type in type_callable_map: 238 | input_value = type_callable_map[field.type](input_value) 239 | 240 | if field.is_extension: 241 | pb.Extensions[field] = input_value 242 | continue 243 | 244 | if field.type == FieldDescriptor.TYPE_ENUM and isinstance(input_value, six.string_types): 245 | input_value = _string_to_enum(field, input_value) 246 | # Handle tri bool enum field 247 | if field.type == FieldDescriptor.TYPE_ENUM and type(input_value) == bool: 248 | if input_value is True: 249 | input_value = 1 250 | elif input_value is False: 251 | input_value = 2 252 | else: 253 | input_value = 0 254 | 255 | setattr(pb, field.name, input_value) 256 | 257 | return pb 258 | 259 | 260 | def _string_to_enum(field, input_value): 261 | enum_dict = field.enum_type.values_by_name 262 | try: 263 | input_value = enum_dict[input_value].number 264 | except KeyError: 265 | raise KeyError("`%s` is not a valid value for field `%s`" % (input_value, field.name)) 266 | return input_value 267 | 268 | 269 | def get_field_names_and_options(pb): 270 | """ 271 | Return a tuple of field names and options. 272 | """ 273 | desc = pb.DESCRIPTOR 274 | 275 | for field in desc.fields: 276 | field_name = field.name 277 | options_dict = {} 278 | if field.has_options: 279 | options = field.GetOptions() 280 | for subfield, value in options.ListFields(): 281 | options_dict[subfield.name] = value 282 | yield field, field_name, options_dict 283 | 284 | 285 | class FieldsMissing(ValueError): 286 | pass 287 | 288 | 289 | def validate_dict_for_required_pb_fields(pb, dic): 290 | """ 291 | Validate that the dictionary has all the required fields for creating a protobuffer object 292 | from pb class. If a field is missing, raise FieldsMissing. 293 | In order to mark a field as optional, add [(is_optional) = true] to the field. 294 | Take a look at the tests for an example. 295 | """ 296 | missing_fields = [] 297 | for field, field_name, field_options in get_field_names_and_options(pb): 298 | if not field_options.get('is_optional', False) and field_name not in dic: 299 | missing_fields.append(field_name) 300 | if missing_fields: 301 | raise FieldsMissing('Missing fields: {}'.format(', '.join(missing_fields))) 302 | -------------------------------------------------------------------------------- /grpc_django/rpcs.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | rpc = namedtuple('rpc', ['name', 'view']) 4 | -------------------------------------------------------------------------------- /grpc_django/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from concurrent import futures 3 | 4 | import grpc 5 | 6 | from grpc_django.settings import settings 7 | 8 | 9 | def init_server(addr, port, max_workers=1, stdout=sys.stdout): 10 | stdout.write("Performing system checks...\n\n") 11 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_workers)) 12 | # Add services to server 13 | stdout.write("\nAdding GRPC services: {}\n\n".format(', '.join([x.name for x in settings.services]))) 14 | for service in settings.services: 15 | servicer = service.load() 16 | handler = service.find_server_handler() 17 | handler(servicer, server) 18 | server.add_insecure_port("{}:{}".format(addr, port)) 19 | return server 20 | -------------------------------------------------------------------------------- /grpc_django/service.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | from types import MethodType 4 | 5 | import pkg_resources 6 | from django.conf import settings as django_settings 7 | from django.core.management import CommandError 8 | from grpc_tools import protoc 9 | 10 | from .exceptions import GrpcServerStartError 11 | from .interfaces import IService, rpc 12 | from .views import ServerStreamGRPCView 13 | 14 | 15 | class GRPCService: 16 | def __init__(self, definition: IService, stdout=None, stderr=None): 17 | self.stdout = stdout if stdout else sys.stdout 18 | self.stderr = stderr if stderr else sys.stderr 19 | 20 | self.name = definition.name 21 | self.proto_path = definition.proto_path 22 | self.proto_filename = self.proto_path.split('/')[-1] 23 | self.stub_destination = definition.stub_conf 24 | self.rpcs = self.get_rpc_paths(definition.rpc_conf) 25 | 26 | # Internal Variables 27 | self._pb = None # Protobuf message interfaces 28 | self._pb_grpc = None # GRPC Service interfaces 29 | 30 | def load(self): 31 | pb, pb_grpc = self.find_stubs() 32 | # Checks the gRPC server interfaces are generated or not 33 | if not pb or not pb_grpc: 34 | raise GrpcServerStartError( 35 | "Failed to find gRPC server interface stubs for the service {}.\n" 36 | "Run 'python manage.py generate_grpc_stubs to generate them.".format(self.name) 37 | ) 38 | 39 | servicer = self.find_servicer(pb_grpc) 40 | 41 | declared_methods = self.get_declared_methods(servicer) 42 | 43 | for _rpc in self.rpcs: 44 | assert isinstance(_rpc, rpc), "Invalid rpc definition {} provided".format(_rpc) 45 | # Check if the rpc is actually defined in the protocol buffer or not 46 | if _rpc.name not in declared_methods: 47 | raise LookupError("RPC {} doesn't exists in proto declarations".format(_rpc.name)) 48 | # Add the method definition in servicer 49 | setattr(servicer, _rpc.name, MethodType(self._get_rpc_method(_rpc), servicer)) 50 | declared_methods.remove(_rpc.name) 51 | 52 | # Show warning if a declared RPC is not implemented 53 | if len(declared_methods): 54 | print("*WARNING* Missing implementations for the RPCs: {}\n\n".format(declared_methods)) 55 | return servicer 56 | 57 | @staticmethod 58 | def _get_rpc_method(_rpc: rpc): 59 | if issubclass(_rpc.view, ServerStreamGRPCView): 60 | def method(*args): 61 | yield from _rpc.view(args[1], args[2]).__call__() 62 | else: 63 | def method(*args): 64 | return _rpc.view(args[1], args[2]).__call__() 65 | return method 66 | 67 | @staticmethod 68 | def get_rpc_paths(path): 69 | return importlib.import_module(path).rpcs 70 | 71 | def find_stubs(self): 72 | dest = self.stub_destination + '.' if self.stub_destination != '.' else '' 73 | filename_without_ext = self.proto_filename.split('.')[0] 74 | pb = importlib.import_module('{}{}_pb2'.format(dest, filename_without_ext)) 75 | pb_grpc = importlib.import_module('{}{}_pb2_grpc'.format(dest, filename_without_ext)) 76 | self._pb, self._pb_grpc = pb, pb_grpc 77 | return pb, pb_grpc 78 | 79 | def find_server_handler(self): 80 | func_name = 'add_{}Servicer_to_server'.format(self.name) 81 | if not hasattr(self._pb_grpc, func_name): 82 | raise AttributeError('No server handler found') 83 | return getattr(self._pb_grpc, func_name) 84 | 85 | def find_servicer(self, pb_grpc): 86 | cls_name = '{}Servicer'.format(self.name) 87 | if not hasattr(pb_grpc, cls_name): 88 | raise AttributeError('No servicer class found') 89 | return getattr(pb_grpc, cls_name) 90 | 91 | @staticmethod 92 | def get_declared_methods(servicer): 93 | return [x for x in servicer.__dict__ if not x.startswith('__')] 94 | 95 | def generate_stubs(self): 96 | well_known_protos_include = pkg_resources.resource_filename( 97 | 'grpc_tools', '_proto') 98 | command = [ 99 | 'grpc_tools.protoc', 100 | "--proto_path={}".format(django_settings.GRPC_PROTO_PATH), 101 | "--proto_path={}".format(well_known_protos_include), 102 | "--python_out={}".format(self.stub_destination), 103 | "--grpc_python_out={}".format(self.stub_destination), 104 | ] + [self.proto_path] 105 | if protoc.main(command) != 0: 106 | raise CommandError('Failed to generate proto stubs for {}'.format(self.proto_path)) 107 | -------------------------------------------------------------------------------- /grpc_django/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings as django_settings 2 | 3 | from .service import GRPCService 4 | from .interfaces import ISettings, IService 5 | 6 | 7 | class GRPCSettings: 8 | def __init__(self): 9 | # Extract GRPC Setting from Django settings 10 | _settings = getattr(django_settings, 'GRPC_SETTINGS') 11 | if not _settings: 12 | raise AssertionError("Missing GRPC_SETTINGS") 13 | if not isinstance(_settings, ISettings): 14 | raise AssertionError("Invalid object type provided for GRPC_SETTINGS, " 15 | "should be an instance of grpc_django.interfaces.GRPCSettings") 16 | 17 | # Server Port 18 | self.server_port = _settings.server.port 19 | 20 | # No. of worker threads 21 | self.workers = _settings.server.num_of_workers 22 | 23 | # List of services 24 | self.services = [] 25 | assert _settings.services, "You must provide at least one gRPC service, in GRPC_SETTINGS.services" 26 | for service in _settings.services: 27 | assert isinstance(service, IService), "Invalid service definition provided" 28 | self.services.append(GRPCService(service)) 29 | 30 | self.auth_user_meta_key = _settings.auth_user_key 31 | 32 | 33 | settings = GRPCSettings() 34 | 35 | __all__ = ['settings'] 36 | -------------------------------------------------------------------------------- /grpc_django/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def convert_to_snakecase(name): 5 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 6 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 7 | 8 | 9 | __all__ = ['convert_to_snakecase'] 10 | -------------------------------------------------------------------------------- /grpc_django/utils/interceptors/__init__.py: -------------------------------------------------------------------------------- 1 | from .bases import UnaryUnaryServerInterceptor, UnaryStreamServerInterceptor, StreamStreamServerInterceptor, \ 2 | StreamUnaryServerInterceptor 3 | 4 | 5 | def intercept_server(server, *interceptors): 6 | """ 7 | Creates an intercepted server. 8 | :param server: A Server object 9 | :param interceptors: Zero or more objects of type UnaryUnaryServerInterceptor, UnaryStreamServerInterceptor, 10 | StreamUnaryServerInterceptor, or StreamStreamServerInterceptor. 11 | Interceptors are given control in the order they are listed. 12 | :return: A Server that intercepts each received RPC via the provided interceptors. 13 | :raises: TypeError: If interceptor does not derive from any of 14 | UnaryUnaryServerInterceptor, 15 | UnaryStreamServerInterceptor, 16 | StreamUnaryServerInterceptor, 17 | StreamStreamServerInterceptor. 18 | """ 19 | from . import _interceptor 20 | return _interceptor.intercept_server(server, *interceptors) 21 | -------------------------------------------------------------------------------- /grpc_django/utils/interceptors/_interceptor.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 gRPC authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ 15 | Implementation of gRPC Python interceptors. 16 | """ 17 | 18 | import grpc 19 | 20 | from .bases import ( 21 | UnaryUnaryServerInterceptor, 22 | UnaryStreamServerInterceptor, 23 | StreamUnaryServerInterceptor, 24 | StreamStreamServerInterceptor 25 | ) 26 | 27 | 28 | class _InterceptingUnaryUnaryMultiCallable(grpc.UnaryUnaryMultiCallable): 29 | 30 | def __init__(self, method, callable_factory, interceptor): 31 | self._method = method 32 | self._callable_factory = callable_factory 33 | self._interceptor = interceptor 34 | 35 | def __call__(self, *args, **kwargs): 36 | 37 | return self.with_call(*args, **kwargs)[0] 38 | 39 | def with_call(self, *args, **kwargs): 40 | 41 | def invoker(method, *args, **kwargs): 42 | return self._callable_factory(method).with_call(*args, **kwargs) 43 | 44 | return self._interceptor.intercept_unary_unary_call( 45 | invoker, self._method, *args, **kwargs) 46 | 47 | def future(self, *args, **kwargs): 48 | 49 | def invoker(method, *args, **kwargs): 50 | return self._callable_factory(method).future(*args, **kwargs) 51 | 52 | return self._interceptor.intercept_unary_unary_future( 53 | invoker, self._method, *args, **kwargs) 54 | 55 | 56 | class _InterceptingUnaryStreamMultiCallable(grpc.UnaryStreamMultiCallable): 57 | 58 | def __init__(self, method, callable_factory, interceptor): 59 | self._method = method 60 | self._callable_factory = callable_factory 61 | self._interceptor = interceptor 62 | 63 | def __call__(self, *args, **kwargs): 64 | 65 | def invoker(method, *args, **kwargs): 66 | return self._callable_factory(method)(*args, **kwargs) 67 | 68 | return self._interceptor.intercept_unary_stream_call( 69 | invoker, self._method, *args, **kwargs) 70 | 71 | 72 | class _InterceptingStreamUnaryMultiCallable(grpc.StreamUnaryMultiCallable): 73 | 74 | def __init__(self, method, callable_factory, interceptor): 75 | self._method = method 76 | self._callable_factory = callable_factory 77 | self._interceptor = interceptor 78 | 79 | def __call__(self, *args, **kwargs): 80 | 81 | return self.with_call(*args, **kwargs)[0] 82 | 83 | def with_call(self, *args, **kwargs): 84 | 85 | def invoker(method, *args, **kwargs): 86 | return self._callable_factory(method).with_call(*args, **kwargs) 87 | 88 | return self._interceptor.intercept_stream_unary_call( 89 | invoker, self._method, *args, **kwargs) 90 | 91 | def future(self, *args, **kwargs): 92 | 93 | def invoker(method, *args, **kwargs): 94 | return self._callable_factory(method).future(*args, **kwargs) 95 | 96 | return self._interceptor.intercept_stream_unary_future( 97 | invoker, self._method, *args, **kwargs) 98 | 99 | 100 | class _InterceptingStreamStreamMultiCallable(grpc.StreamStreamMultiCallable): 101 | 102 | def __init__(self, method, callable_factory, interceptor): 103 | self._method = method 104 | self._callable_factory = callable_factory 105 | self._interceptor = interceptor 106 | 107 | def __call__(self, *args, **kwargs): 108 | 109 | def invoker(method, *args, **kwargs): 110 | return self._callable_factory(method)(*args, **kwargs) 111 | 112 | return self._interceptor.intercept_stream_stream_call( 113 | invoker, self._method, *args, **kwargs) 114 | 115 | 116 | class _InterceptingChannel(grpc.Channel): 117 | 118 | def __init__(self, channel, interceptor): 119 | self._channel = channel 120 | self._interceptor = interceptor 121 | 122 | def subscribe(self, *args, **kwargs): 123 | self._channel.subscribe(*args, **kwargs) 124 | 125 | def unsubscribe(self, *args, **kwargs): 126 | self._channel.unsubscribe(*args, **kwargs) 127 | 128 | def unary_unary(self, 129 | method, 130 | request_serializer=None, 131 | response_deserializer=None): 132 | 133 | def callable_factory(method): 134 | return self._channel.unary_unary(method, request_serializer, 135 | response_deserializer) 136 | 137 | if isinstance(self._interceptor, grpc.UnaryUnaryClientInterceptor): 138 | return _InterceptingUnaryUnaryMultiCallable( 139 | method, callable_factory, self._interceptor) 140 | else: 141 | return callable_factory(method) 142 | 143 | def unary_stream(self, 144 | method, 145 | request_serializer=None, 146 | response_deserializer=None): 147 | 148 | def callable_factory(method): 149 | return self._channel.unary_stream(method, request_serializer, 150 | response_deserializer) 151 | 152 | if isinstance(self._interceptor, grpc.UnaryStreamClientInterceptor): 153 | return _InterceptingUnaryStreamMultiCallable( 154 | method, callable_factory, self._interceptor) 155 | else: 156 | return callable_factory(method) 157 | 158 | def stream_unary(self, 159 | method, 160 | request_serializer=None, 161 | response_deserializer=None): 162 | 163 | def callable_factory(method): 164 | return self._channel.stream_unary(method, request_serializer, 165 | response_deserializer) 166 | 167 | if isinstance(self._interceptor, grpc.StreamUnaryClientInterceptor): 168 | return _InterceptingStreamUnaryMultiCallable( 169 | method, callable_factory, self._interceptor) 170 | else: 171 | return callable_factory(method) 172 | 173 | def stream_stream(self, 174 | method, 175 | request_serializer=None, 176 | response_deserializer=None): 177 | 178 | def callable_factory(method): 179 | return self._channel.stream_stream(method, request_serializer, 180 | response_deserializer) 181 | 182 | if isinstance(self._interceptor, grpc.StreamStreamClientInterceptor): 183 | return _InterceptingStreamStreamMultiCallable( 184 | method, callable_factory, self._interceptor) 185 | else: 186 | return callable_factory(method) 187 | 188 | 189 | def intercept_channel(channel, *interceptors): 190 | for interceptor in reversed(list(interceptors)): 191 | if not isinstance(interceptor, grpc.UnaryUnaryClientInterceptor) and \ 192 | not isinstance(interceptor, grpc.UnaryStreamClientInterceptor) and \ 193 | not isinstance(interceptor, grpc.StreamUnaryClientInterceptor) and \ 194 | not isinstance(interceptor, grpc.StreamStreamClientInterceptor): 195 | raise TypeError('interceptor must be ' 196 | 'UnaryUnaryClientInterceptor or ' 197 | 'UnaryStreamClientInterceptor or ' 198 | 'StreamUnaryClientInterceptor or ' 199 | 'StreamStreamClientInterceptor') 200 | channel = _InterceptingChannel(channel, interceptor) 201 | return channel 202 | 203 | 204 | class _InterceptingRpcMethodHandler(grpc.RpcMethodHandler): 205 | 206 | def __init__(self, rpc_method_handler, method, interceptor): 207 | self._rpc_method_handler = rpc_method_handler 208 | self._method = method 209 | self._interceptor = interceptor 210 | 211 | @property 212 | def request_streaming(self): 213 | return self._rpc_method_handler.request_streaming 214 | 215 | @property 216 | def response_streaming(self): 217 | return self._rpc_method_handler.response_streaming 218 | 219 | @property 220 | def request_deserializer(self): 221 | return self._rpc_method_handler.request_deserializer 222 | 223 | @property 224 | def response_serializer(self): 225 | return self._rpc_method_handler.response_serializer 226 | 227 | @property 228 | def unary_unary(self): 229 | if not isinstance(self._interceptor, UnaryUnaryServerInterceptor): 230 | return self._rpc_method_handler.unary_unary 231 | 232 | def adaptation(request, servicer_context): 233 | 234 | def handler(request, servicer_context): 235 | return self._rpc_method_handler.unary_unary(request, 236 | servicer_context) 237 | 238 | return self._interceptor.intercept_unary_unary_handler( 239 | handler, self._method, request, servicer_context) 240 | 241 | return adaptation 242 | 243 | @property 244 | def unary_stream(self): 245 | if not isinstance(self._interceptor, UnaryStreamServerInterceptor): 246 | return self._rpc_method_handler.unary_stream 247 | 248 | def adaptation(request, servicer_context): 249 | 250 | def handler(request, servicer_context): 251 | return self._rpc_method_handler.unary_stream(request, 252 | servicer_context) 253 | 254 | return self._interceptor.intercept_unary_stream_handler( 255 | handler, self._method, request, servicer_context) 256 | 257 | return adaptation 258 | 259 | @property 260 | def stream_unary(self): 261 | if not isinstance(self._interceptor, StreamUnaryServerInterceptor): 262 | return self._rpc_method_handler.stream_unary 263 | 264 | def adaptation(request_iterator, servicer_context): 265 | 266 | def handler(request_iterator, servicer_context): 267 | return self._rpc_method_handler.stream_unary(request_iterator, 268 | servicer_context) 269 | 270 | return self._interceptor.intercept_stream_unary_handler( 271 | handler, self._method, request_iterator, servicer_context) 272 | 273 | return adaptation 274 | 275 | @property 276 | def stream_stream(self): 277 | if not isinstance(self._interceptor, 278 | StreamStreamServerInterceptor): 279 | return self._rpc_method_handler.stream_stream 280 | 281 | def adaptation(request_iterator, servicer_context): 282 | 283 | def handler(request_iterator, servicer_context): 284 | return self._rpc_method_handler.stream_stream(request_iterator, 285 | servicer_context) 286 | 287 | return self._interceptor.intercept_stream_stream_handler( 288 | handler, self._method, request_iterator, servicer_context) 289 | 290 | return adaptation 291 | 292 | 293 | class _InterceptingGenericRpcHandler(grpc.GenericRpcHandler): 294 | 295 | def __init__(self, handler, interceptor): 296 | self._handler = handler 297 | self._interceptor = interceptor 298 | 299 | def service(self, handler_call_details): 300 | result = self._handler.service(handler_call_details) 301 | if result: 302 | result = _InterceptingRpcMethodHandler( 303 | result, handler_call_details.method, self._interceptor) 304 | return result 305 | 306 | 307 | class _InterceptingServer(grpc.Server): 308 | 309 | def __init__(self, server, interceptor): 310 | self._server = server 311 | self._interceptor = interceptor 312 | 313 | def add_generic_rpc_handlers(self, generic_rpc_handlers): 314 | handlers = (_InterceptingGenericRpcHandler(handler, self._interceptor) 315 | for handler in generic_rpc_handlers) 316 | return self._server.add_generic_rpc_handlers(handlers) 317 | 318 | def add_insecure_port(self, *args, **kwargs): 319 | return self._server.add_insecure_port(*args, **kwargs) 320 | 321 | def add_secure_port(self, *args, **kwargs): 322 | return self._server.add_secure_port(*args, **kwargs) 323 | 324 | def start(self, *args, **kwargs): 325 | return self._server.start(*args, **kwargs) 326 | 327 | def stop(self, *args, **kwargs): 328 | return self._server.stop(*args, **kwargs) 329 | 330 | 331 | def intercept_server(server, *interceptors): 332 | for interceptor in reversed(interceptors): 333 | if not isinstance(interceptor, UnaryUnaryServerInterceptor) and \ 334 | not isinstance(interceptor, UnaryStreamServerInterceptor) and \ 335 | not isinstance(interceptor, StreamUnaryServerInterceptor) and \ 336 | not isinstance(interceptor, StreamStreamServerInterceptor): 337 | raise TypeError('interceptor must be ' 338 | 'grpc.UnaryUnaryServerInterceptor or ' 339 | 'grpc.UnaryStreamServerInterceptor or ' 340 | 'grpc.StreamUnaryServerInterceptor or ' 341 | 'grpc.StreamStreamServerInterceptor or ') 342 | server = _InterceptingServer(server, interceptor) 343 | return server 344 | -------------------------------------------------------------------------------- /grpc_django/utils/interceptors/bases.py: -------------------------------------------------------------------------------- 1 | import six 2 | import abc 3 | 4 | 5 | class UnaryUnaryServerInterceptor(six.with_metaclass(abc.ABCMeta)): 6 | @abc.abstractmethod 7 | def intercept_unary_unary_handler(self, handler, method, request, servicer_context): 8 | """ 9 | Intercepts unary-unary RPCs on the service-side. 10 | :param handler: The handler to continue processing the RPC. It takes a request value and a ServicerContext 11 | object and returns a response value. 12 | :param method: The full method name of the RPC. 13 | :param request: The request value for the RPC. 14 | :param servicer_context: The context of the current RPC. 15 | :return: The RPC response 16 | """ 17 | raise NotImplementedError() 18 | 19 | 20 | class UnaryStreamServerInterceptor(six.with_metaclass(abc.ABCMeta)): 21 | @abc.abstractmethod 22 | def intercept_unary_stream_handler(self, handler, method, request, servicer_context): 23 | """ 24 | Intercepts unary-stream RPCs on the service-side. 25 | :param handler: The handler to continue processing the RPC. It takes a request value and a ServicerContext 26 | object and returns a response value. 27 | :param method: The full method name of the RPC. 28 | :param request: The request value for the RPC. 29 | :param servicer_context: The context of the current RPC. 30 | :return: An iterator of RPC response values. 31 | """ 32 | raise NotImplementedError() 33 | 34 | 35 | class StreamUnaryServerInterceptor(six.with_metaclass(abc.ABCMeta)): 36 | @abc.abstractmethod 37 | def intercept_stream_unary_handler(self, handler, method, request_iterator, servicer_context): 38 | """ 39 | Intercepts stream-unary RPCs on the service-side. 40 | :param handler: The handler to continue processing the RPC. It takes a request value and a ServicerContext 41 | object and returns a response value. 42 | :param method: The full method name of the RPC. 43 | :param request_iterator: An iterator of request values for the RPC. 44 | :param servicer_context: The context of the current RPC. 45 | :return: The RPC response. 46 | """ 47 | raise NotImplementedError() 48 | 49 | 50 | class StreamStreamServerInterceptor(six.with_metaclass(abc.ABCMeta)): 51 | @abc.abstractmethod 52 | def intercept_stream_stream_handler(self, handler, method, request_iterator, servicer_context): 53 | """ 54 | Intercepts stream-stream RPCs on the service-side. 55 | :param handler: The handler to continue processing the RPC. It takes a request value and a ServicerContext 56 | object and returns an iterator of response values. 57 | :param method: The full method name of the RPC. 58 | :param request_iterator: An iterator of request values for the RPC. 59 | :param servicer_context: The context of the current RPC. 60 | :return: An iterator of RPC response values. 61 | """ 62 | raise NotImplementedError() 63 | -------------------------------------------------------------------------------- /grpc_django/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | 4 | from django.contrib.auth.models import AnonymousUser 5 | from django.db.models import QuerySet 6 | 7 | from .models import ContextUser 8 | from .protobuf_to_dict import dict_to_protobuf 9 | from .exceptions import InvalidArgument, NotAuthenticated, ExceptionHandler 10 | 11 | 12 | class GenericGrpcView(object): 13 | queryset = None 14 | serializer_class = None 15 | # Protocol Buffer class which needs to be returned 16 | response_proto = None 17 | # Whether the RPC call requires authentication 18 | requires_authentication = False 19 | # Lookup field identifier in request proto 20 | lookup_kwarg = "id" 21 | # If you want to use object lookups other than pk, set 'lookup_field'. 22 | # For more complex lookup requirements override `get_object()`. 23 | lookup_field = "pk" 24 | 25 | def __init__(self, request, context): 26 | assert self.response_proto, "Missing response_proto declaration" 27 | self.request_user = self.get_user(context) 28 | self.request = request 29 | self.context = context 30 | 31 | @staticmethod 32 | def get_user(context): 33 | from .settings import settings 34 | user_json = json.loads(dict(context.invocation_metadata()).get(settings.auth_user_meta_key, "{}")) 35 | if user_json is None or len(user_json) == 0: 36 | return AnonymousUser() 37 | else: 38 | return ContextUser(**user_json) 39 | 40 | def get_queryset(self): 41 | assert self.queryset is not None, ( 42 | "{}' should either include a `queryset` attribute, " 43 | "or override the `get_queryset()` method.".format(self.__class__.__name__) 44 | ) 45 | queryset = self.queryset 46 | if isinstance(queryset, QuerySet): 47 | # Ensure queryset is re-evaluated on each request. 48 | queryset = queryset.all() 49 | return queryset 50 | 51 | def check_object_permissions(self, user, obj): 52 | """ 53 | Override this function to check if the request should be permitted for a given object. 54 | Raise an appropriate exception if the request is not permitted. 55 | """ 56 | pass 57 | 58 | def perform_authentication(self, user): 59 | """ 60 | Perform authentication on the incoming request. 61 | Raise an appropriate exception if the request is not authenticated. 62 | :param user: 63 | :return: 64 | """ 65 | if self.requires_authentication and isinstance(user, AnonymousUser): 66 | raise NotAuthenticated 67 | pass 68 | 69 | def get_object(self): 70 | if not hasattr(self.request, self.lookup_kwarg): 71 | raise InvalidArgument("Missing argument {}".format(self.lookup_kwarg)) 72 | queryset = self.get_queryset() 73 | obj = queryset.get(**{self.lookup_field: getattr(self.request, self.lookup_kwarg)}) 74 | self.check_object_permissions(self.request_user, obj) 75 | return obj 76 | 77 | 78 | class RetrieveGRPCView(GenericGrpcView): 79 | def retrieve(self): 80 | """ 81 | Override this function to implement retrieval 82 | :return: dictionary of object 83 | """ 84 | instance = self.get_object() 85 | serializer = self.serializer_class(instance) 86 | return serializer.data 87 | 88 | def __call__(self): 89 | try: 90 | self.perform_authentication(self.request_user) 91 | result = self.retrieve() 92 | return dict_to_protobuf(self.response_proto, values=result, ignore_none=True) 93 | except Exception as ex: 94 | self.context = ExceptionHandler(self.context).__call__(ex, traceback.format_exc()) 95 | return self.response_proto() 96 | 97 | 98 | class ServerStreamGRPCView(GenericGrpcView): 99 | def __call__(self): 100 | try: 101 | self.perform_authentication(self.request_user) 102 | queryset = self.get_queryset() 103 | for obj in queryset: 104 | serializer = self.serializer_class(obj) 105 | yield dict_to_protobuf(self.response_proto, values=serializer.data, ignore_none=True) 106 | except Exception as ex: 107 | self.context = ExceptionHandler(self.context).__call__(ex, traceback.format_exc()) 108 | yield self.response_proto() 109 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: GRPC Django Framework 2 | theme: 3 | name: readthedocs 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==4.5.4 2 | Django==2.2.5 3 | djangorestframework==3.10.3 4 | grpcio==1.24.0 5 | grpcio-tools==1.24.0 6 | mkdocs==1.0.4 7 | Pygments==2.4.2 8 | PyYAML==5.1.2 9 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | from django.test.utils import get_runner 7 | 8 | 9 | def run_tests(): 10 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 11 | django.setup() 12 | TestRunner = get_runner(settings) 13 | test_runner = TestRunner() 14 | failures = test_runner.run_tests(['tests']) 15 | sys.exit(bool(failures)) 16 | 17 | 18 | if __name__ == '__main__': 19 | run_tests() 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import pathlib 3 | 4 | try: 5 | codecs.lookup('mbcs') 6 | except LookupError: 7 | ascii = codecs.lookup('ascii') 8 | func = lambda name, enc=ascii: {True: enc}.get(name == 'mbcs') 9 | codecs.register(func) 10 | 11 | from setuptools import find_packages, setup 12 | 13 | version = "1.0.0" 14 | 15 | # The directory containing this file 16 | HERE = pathlib.Path(__file__).parent 17 | 18 | # The text of the README file 19 | README = (HERE / "README.md").read_text() 20 | 21 | setup( 22 | name="grpc-django", 23 | version=version, 24 | packages=find_packages(exclude=["tests*", "manage.py", "docs"]), 25 | include_package_data=True, 26 | python_requires='>3.5.0', 27 | install_requires=[ 28 | "Django >= 1.9", 29 | "grpcio", 30 | "grpcio-tools", 31 | "google", 32 | "six", 33 | "djangorestframework" 34 | ], 35 | author="Sohel Tarir", 36 | author_email="sohel.tarir@gmail.com", 37 | description="gRPC Integration with Django Framework", 38 | long_description=README, 39 | long_description_content_type="text/markdown", 40 | license="MIT", 41 | url="https://github.com/soheltarir/grpc-django", 42 | classifiers=[ 43 | "License :: OSI Approved :: MIT License", 44 | "Programming Language :: Python :: 3", 45 | "Programming Language :: Python :: 3.5", 46 | "Programming Language :: Python :: 3.6", 47 | "Programming Language :: Python :: 3.7", 48 | ], 49 | ) 50 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soheltarir/grpc-django/890038de2d7b7c36e4fd37ef1727d3568b3f68f5/tests/__init__.py -------------------------------------------------------------------------------- /tests/grpc.py: -------------------------------------------------------------------------------- 1 | from grpc_django.settings import settings 2 | from tests.grpc_codegen.test_pb2_grpc import TestServiceServicer, add_TestServiceServicer_to_server 3 | 4 | settings.add_service(add_TestServiceServicer_to_server, TestServiceServicer, "tests.rpcs") 5 | -------------------------------------------------------------------------------- /tests/grpc_codegen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soheltarir/grpc-django/890038de2d7b7c36e4fd37ef1727d3568b3f68f5/tests/grpc_codegen/__init__.py -------------------------------------------------------------------------------- /tests/grpc_codegen/test_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: test.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='test.proto', 20 | package='test', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\ntest.proto\x12\x04test\"2\n\x04User\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\"\x18\n\nGetPayload\x12\n\n\x02id\x18\x01 \x01(\x03\"\x07\n\x05\x45mpty2^\n\x0bTestService\x12\'\n\x07GetUser\x12\x10.test.GetPayload\x1a\n.test.User\x12&\n\tListUsers\x12\x0b.test.Empty\x1a\n.test.User0\x01\x62\x06proto3') 23 | ) 24 | 25 | 26 | 27 | 28 | _USER = _descriptor.Descriptor( 29 | name='User', 30 | full_name='test.User', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | containing_type=None, 34 | fields=[ 35 | _descriptor.FieldDescriptor( 36 | name='id', full_name='test.User.id', index=0, 37 | number=1, type=3, cpp_type=2, label=1, 38 | has_default_value=False, default_value=0, 39 | message_type=None, enum_type=None, containing_type=None, 40 | is_extension=False, extension_scope=None, 41 | options=None, file=DESCRIPTOR), 42 | _descriptor.FieldDescriptor( 43 | name='username', full_name='test.User.username', index=1, 44 | number=2, type=9, cpp_type=9, label=1, 45 | has_default_value=False, default_value=_b("").decode('utf-8'), 46 | message_type=None, enum_type=None, containing_type=None, 47 | is_extension=False, extension_scope=None, 48 | options=None, file=DESCRIPTOR), 49 | _descriptor.FieldDescriptor( 50 | name='name', full_name='test.User.name', index=2, 51 | number=3, type=9, cpp_type=9, label=1, 52 | has_default_value=False, default_value=_b("").decode('utf-8'), 53 | message_type=None, enum_type=None, containing_type=None, 54 | is_extension=False, extension_scope=None, 55 | options=None, file=DESCRIPTOR), 56 | ], 57 | extensions=[ 58 | ], 59 | nested_types=[], 60 | enum_types=[ 61 | ], 62 | options=None, 63 | is_extendable=False, 64 | syntax='proto3', 65 | extension_ranges=[], 66 | oneofs=[ 67 | ], 68 | serialized_start=20, 69 | serialized_end=70, 70 | ) 71 | 72 | 73 | _GETPAYLOAD = _descriptor.Descriptor( 74 | name='GetPayload', 75 | full_name='test.GetPayload', 76 | filename=None, 77 | file=DESCRIPTOR, 78 | containing_type=None, 79 | fields=[ 80 | _descriptor.FieldDescriptor( 81 | name='id', full_name='test.GetPayload.id', index=0, 82 | number=1, type=3, cpp_type=2, label=1, 83 | has_default_value=False, default_value=0, 84 | message_type=None, enum_type=None, containing_type=None, 85 | is_extension=False, extension_scope=None, 86 | options=None, file=DESCRIPTOR), 87 | ], 88 | extensions=[ 89 | ], 90 | nested_types=[], 91 | enum_types=[ 92 | ], 93 | options=None, 94 | is_extendable=False, 95 | syntax='proto3', 96 | extension_ranges=[], 97 | oneofs=[ 98 | ], 99 | serialized_start=72, 100 | serialized_end=96, 101 | ) 102 | 103 | 104 | _EMPTY = _descriptor.Descriptor( 105 | name='Empty', 106 | full_name='test.Empty', 107 | filename=None, 108 | file=DESCRIPTOR, 109 | containing_type=None, 110 | fields=[ 111 | ], 112 | extensions=[ 113 | ], 114 | nested_types=[], 115 | enum_types=[ 116 | ], 117 | options=None, 118 | is_extendable=False, 119 | syntax='proto3', 120 | extension_ranges=[], 121 | oneofs=[ 122 | ], 123 | serialized_start=98, 124 | serialized_end=105, 125 | ) 126 | 127 | DESCRIPTOR.message_types_by_name['User'] = _USER 128 | DESCRIPTOR.message_types_by_name['GetPayload'] = _GETPAYLOAD 129 | DESCRIPTOR.message_types_by_name['Empty'] = _EMPTY 130 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 131 | 132 | User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), dict( 133 | DESCRIPTOR = _USER, 134 | __module__ = 'test_pb2' 135 | # @@protoc_insertion_point(class_scope:test.User) 136 | )) 137 | _sym_db.RegisterMessage(User) 138 | 139 | GetPayload = _reflection.GeneratedProtocolMessageType('GetPayload', (_message.Message,), dict( 140 | DESCRIPTOR = _GETPAYLOAD, 141 | __module__ = 'test_pb2' 142 | # @@protoc_insertion_point(class_scope:test.GetPayload) 143 | )) 144 | _sym_db.RegisterMessage(GetPayload) 145 | 146 | Empty = _reflection.GeneratedProtocolMessageType('Empty', (_message.Message,), dict( 147 | DESCRIPTOR = _EMPTY, 148 | __module__ = 'test_pb2' 149 | # @@protoc_insertion_point(class_scope:test.Empty) 150 | )) 151 | _sym_db.RegisterMessage(Empty) 152 | 153 | 154 | 155 | _TESTSERVICE = _descriptor.ServiceDescriptor( 156 | name='TestService', 157 | full_name='test.TestService', 158 | file=DESCRIPTOR, 159 | index=0, 160 | options=None, 161 | serialized_start=107, 162 | serialized_end=201, 163 | methods=[ 164 | _descriptor.MethodDescriptor( 165 | name='GetUser', 166 | full_name='test.TestService.GetUser', 167 | index=0, 168 | containing_service=None, 169 | input_type=_GETPAYLOAD, 170 | output_type=_USER, 171 | options=None, 172 | ), 173 | _descriptor.MethodDescriptor( 174 | name='ListUsers', 175 | full_name='test.TestService.ListUsers', 176 | index=1, 177 | containing_service=None, 178 | input_type=_EMPTY, 179 | output_type=_USER, 180 | options=None, 181 | ), 182 | ]) 183 | _sym_db.RegisterServiceDescriptor(_TESTSERVICE) 184 | 185 | DESCRIPTOR.services_by_name['TestService'] = _TESTSERVICE 186 | 187 | # @@protoc_insertion_point(module_scope) 188 | -------------------------------------------------------------------------------- /tests/grpc_codegen/test_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | from tests.grpc_codegen import test_pb2 as test__pb2 5 | 6 | 7 | class TestServiceStub(object): 8 | # missing associated documentation comment in .proto file 9 | pass 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.GetUser = channel.unary_unary( 18 | '/test.TestService/GetUser', 19 | request_serializer=test__pb2.GetPayload.SerializeToString, 20 | response_deserializer=test__pb2.User.FromString, 21 | ) 22 | self.ListUsers = channel.unary_stream( 23 | '/test.TestService/ListUsers', 24 | request_serializer=test__pb2.Empty.SerializeToString, 25 | response_deserializer=test__pb2.User.FromString, 26 | ) 27 | 28 | 29 | class TestServiceServicer(object): 30 | # missing associated documentation comment in .proto file 31 | pass 32 | 33 | def GetUser(self, request, context): 34 | # missing associated documentation comment in .proto file 35 | pass 36 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 37 | context.set_details('Method not implemented!') 38 | raise NotImplementedError('Method not implemented!') 39 | 40 | def ListUsers(self, request, context): 41 | # missing associated documentation comment in .proto file 42 | pass 43 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 44 | context.set_details('Method not implemented!') 45 | raise NotImplementedError('Method not implemented!') 46 | 47 | 48 | def add_TestServiceServicer_to_server(servicer, server): 49 | rpc_method_handlers = { 50 | 'GetUser': grpc.unary_unary_rpc_method_handler( 51 | servicer.GetUser, 52 | request_deserializer=test__pb2.GetPayload.FromString, 53 | response_serializer=test__pb2.User.SerializeToString, 54 | ), 55 | 'ListUsers': grpc.unary_stream_rpc_method_handler( 56 | servicer.ListUsers, 57 | request_deserializer=test__pb2.Empty.FromString, 58 | response_serializer=test__pb2.User.SerializeToString, 59 | ), 60 | } 61 | generic_handler = grpc.method_handlers_generic_handler( 62 | 'test.TestService', rpc_method_handlers) 63 | server.add_generic_rpc_handlers((generic_handler,)) 64 | -------------------------------------------------------------------------------- /tests/protos/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package test; 4 | 5 | message User { 6 | int64 id = 1; 7 | string username = 2; 8 | string name = 3; 9 | } 10 | 11 | message GetPayload { 12 | int64 id = 1; 13 | } 14 | 15 | message Empty {} 16 | 17 | service TestService { 18 | rpc GetUser (GetPayload) returns (User); 19 | rpc ListUsers (Empty) returns (stream User); 20 | } 21 | -------------------------------------------------------------------------------- /tests/rpcs.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | 3 | from grpc_django.interfaces import rpc 4 | from grpc_django.views import RetrieveGRPCView, ServerStreamGRPCView 5 | from tests.grpc_codegen.test_pb2 import User 6 | 7 | USERS = [{ 8 | "id": 1, 9 | "name": "Bruce Wayne", 10 | "username": "bruce.wayne" 11 | }, { 12 | "id": 2, 13 | "name": "Clary Fairchild", 14 | "username": "clary.fairchild" 15 | }] 16 | 17 | 18 | class UserSerializer: 19 | def __init__(self, obj): 20 | self.obj = obj 21 | 22 | @property 23 | def data(self): 24 | return self.obj 25 | 26 | 27 | class GetUser(RetrieveGRPCView): 28 | response_proto = User 29 | serializer_class = UserSerializer 30 | 31 | def get_queryset(self): 32 | return USERS 33 | 34 | def get_object(self): 35 | users = self.get_queryset() 36 | for user in users: 37 | if user["id"] == getattr(self.request, self.lookup_kwarg): 38 | return user 39 | raise ObjectDoesNotExist("User matching query does not exists.") 40 | 41 | 42 | class ListUsers(ServerStreamGRPCView): 43 | response_proto = User 44 | serializer_class = UserSerializer 45 | 46 | def get_queryset(self): 47 | return USERS 48 | 49 | 50 | rpcs = [ 51 | rpc("GetUser", GetUser), 52 | rpc("ListUsers", ListUsers) 53 | ] 54 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tests project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from grpc_django import GRPCSettings, GRPCService, GRPCServer 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'q#ofhp_r3z1vmukgy7@$gz8dsk72*&6$$42z*0ne-=%c5aeln-' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'tests', 42 | 'grpc_django' 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'tests.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'tests.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | 125 | GRPC_SETTINGS = GRPCSettings( 126 | services=[ 127 | GRPCService('TestService', 'test', 'tests/protos/test.proto', 'tests.rpcs', 'tests.grpc_codegen') 128 | ], 129 | server=GRPCServer(port=55000, num_of_workers=3), 130 | auth_user_key='user' 131 | ) 132 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | import grpc 4 | from django.test import TestCase 5 | 6 | from grpc_django.server import init_server 7 | from tests.grpc_codegen.test_pb2 import GetPayload, User, Empty 8 | from tests.grpc_codegen.test_pb2_grpc import TestServiceStub 9 | 10 | 11 | TEST_USERS = { 12 | 1: User(id=1, name="Bruce Wayne", username='bruce.wayne'), 13 | 2: User(id=2, name="Diana Prince", username='diana.prince') 14 | } 15 | 16 | 17 | class GrpcServerTest(TestCase): 18 | def setUp(self): 19 | self.users = TEST_USERS 20 | self.server = init_server('127.0.0.1', '55000', max_workers=1, stdout=StringIO()) 21 | self.server.start() 22 | 23 | @property 24 | def client_stub(self): 25 | channel = grpc.insecure_channel("localhost:55000") 26 | grpc.channel_ready_future(channel).result(timeout=10) 27 | stub = TestServiceStub(channel) 28 | return stub 29 | 30 | def test_get(self): 31 | response = self.client_stub.GetUser(GetPayload(id=1)) 32 | self.assertEqual(response, self.users[1]) 33 | 34 | def test_list(self): 35 | response = self.client_stub.ListUsers(Empty()) 36 | for _ in response: 37 | self.assertIsInstance(_, User) 38 | 39 | def tearDown(self): 40 | self.server.stop(0) 41 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | """tests URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /tests/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tests project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------