├── examples ├── demo │ ├── demo │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── wsgi.py │ │ ├── urls.py │ │ └── settings.py │ ├── client.py │ ├── demo.proto │ ├── manage.py │ ├── demo_pb2.py │ └── demo_pb2_grpc.py ├── tutorial │ ├── blog │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── views.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── handlers.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── services.py │ │ ├── tests.py │ │ └── _services.py │ ├── tutorial │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── asgi.py │ │ ├── wsgi.py │ │ └── settings.py │ ├── protos │ │ └── blog_proto │ │ │ └── post.proto │ ├── manage.py │ ├── blog_client.py │ └── blog_proto │ │ ├── post_pb2.py │ │ └── post_pb2_grpc.py ├── null_support │ ├── snippets │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── views.py │ │ ├── tests.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ └── services.py │ ├── null_support │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── asgi.py │ │ ├── wsgi.py │ │ └── settings.py │ ├── snippets.proto │ ├── client.py │ ├── manage.py │ ├── snippets_pb2_grpc.py │ └── snippets_pb2.py ├── partial_update │ ├── hrm │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── views.py │ │ ├── tests.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── serializers.py │ │ └── services.py │ ├── partial_update │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── asgi.py │ │ ├── wsgi.py │ │ └── settings.py │ ├── client.py │ ├── hrm.proto │ ├── manage.py │ ├── hrm_pb2_grpc.py │ └── hrm_pb2.py └── quickstart │ ├── account │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── views.py │ ├── models.py │ ├── admin.py │ ├── apps.py │ ├── serializers.py │ ├── services.py │ └── tests.py │ ├── quickstart │ ├── __init__.py │ ├── urls.py │ ├── asgi.py │ ├── wsgi.py │ └── settings.py │ ├── account.proto │ ├── manage.py │ ├── client.py │ ├── account_pb2.py │ └── account_pb2_grpc.py ├── django_grpc_framework ├── protobuf │ ├── __init__.py │ ├── json_format.py │ └── generators.py ├── utils │ ├── __init__.py │ └── model_meta.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── generateproto.py │ │ └── grpcrunserver.py ├── __init__.py ├── apps.py ├── signals.py ├── services.py ├── proto_serializers.py ├── settings.py ├── mixins.py ├── test.py └── generics.py ├── setup.cfg ├── docs ├── changelog.rst ├── license.rst ├── patterns │ ├── index.rst │ ├── null_support.rst │ └── partial_update.rst ├── tutorial │ ├── index.rst │ ├── writing_tests.rst │ ├── using_generics.rst │ └── building_services.rst ├── Makefile ├── protos.rst ├── index.rst ├── make.bat ├── installation.rst ├── services.rst ├── server.rst ├── settings.rst ├── testing.rst ├── conf.py ├── proto_serializers.rst ├── generics.rst └── quickstart.rst ├── tests └── test_services.py ├── requirements.txt ├── .travis.yml ├── TODO ├── CHANGES ├── Makefile ├── setup.py ├── .gitignore └── README.rst /examples/demo/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/tutorial/blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/tutorial/tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_grpc_framework/protobuf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_grpc_framework/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/null_support/snippets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/quickstart/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /django_grpc_framework/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/null_support/null_support/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/tutorial/blog/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_grpc_framework/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/null_support/snippets/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/partial_update/partial_update/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_grpc_framework/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.1' 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changes: 2 | 3 | .. include:: ../CHANGES 4 | -------------------------------------------------------------------------------- /examples/tutorial/blog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | -------------------------------------------------------------------------------- /tests/test_services.py: -------------------------------------------------------------------------------- 1 | def test_basic_service(): 2 | assert True 3 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | -------------------------------------------------------------------------------- /examples/quickstart/account/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | -------------------------------------------------------------------------------- /examples/null_support/snippets/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/quickstart/account/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/tutorial/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/null_support/snippets/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/quickstart/account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/null_support/snippets/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=2.2,<4.0 2 | djangorestframework>=3.10.0,<4.0 3 | grpcio>=1.16.0,<2.0 4 | grpcio-tools>=1.16.0,<2.0 5 | -------------------------------------------------------------------------------- /examples/tutorial/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = 'blog' 6 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HrmConfig(AppConfig): 5 | name = 'hrm' 6 | -------------------------------------------------------------------------------- /examples/quickstart/account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | name = 'account' 6 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | License 4 | ======= 5 | 6 | This library is licensed under Apache License. 7 | 8 | .. include:: ../LICENSE 9 | -------------------------------------------------------------------------------- /examples/null_support/snippets/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SnippetsConfig(AppConfig): 5 | name = 'snippets' 6 | -------------------------------------------------------------------------------- /examples/tutorial/tutorial/urls.py: -------------------------------------------------------------------------------- 1 | from blog.handlers import grpc_handlers as blog_grpc_handlers 2 | 3 | 4 | urlpatterns = [] 5 | 6 | 7 | def grpc_handlers(server): 8 | blog_grpc_handlers(server) 9 | -------------------------------------------------------------------------------- /django_grpc_framework/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoGrpcFrameworkConfig(AppConfig): 5 | name = 'django_grpc_framework' 6 | verbose_name = "Django gRPC framework" 7 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Person(models.Model): 5 | name = models.CharField(max_length=100) 6 | email = models.CharField(max_length=100) 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | - "3.8" 6 | 7 | install: 8 | - pip install flake8 9 | - pip install pytest 10 | 11 | script: 12 | - make flake8 13 | - make test 14 | -------------------------------------------------------------------------------- /examples/null_support/snippets/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Snippet(models.Model): 5 | title = models.CharField(max_length=100) 6 | language = models.CharField(max_length=20, null=True) 7 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | - relationships 5 | - error handling 6 | - add BulkCreate\BulkDestroy\BulkUpdate with stream-unary mode ? 7 | - authentication 8 | - logging 9 | - add testcases for project 10 | - filter support ? -------------------------------------------------------------------------------- /examples/tutorial/blog/handlers.py: -------------------------------------------------------------------------------- 1 | from blog.services import PostService 2 | from blog_proto import post_pb2_grpc 3 | 4 | 5 | def grpc_handlers(server): 6 | post_pb2_grpc.add_PostControllerServicer_to_server(PostService.as_servicer(), server) 7 | -------------------------------------------------------------------------------- /docs/patterns/index.rst: -------------------------------------------------------------------------------- 1 | .. _patterns: 2 | 3 | Patterns for gRPC 4 | ================= 5 | 6 | This part contains some snippets and patterns for Django gRPC framework. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | partial_update 12 | null_support 13 | -------------------------------------------------------------------------------- /examples/quickstart/quickstart/urls.py: -------------------------------------------------------------------------------- 1 | import account_pb2_grpc 2 | from account.services import UserService 3 | 4 | 5 | urlpatterns = [] 6 | 7 | 8 | def grpc_handlers(server): 9 | account_pb2_grpc.add_UserControllerServicer_to_server(UserService.as_servicer(), server) 10 | -------------------------------------------------------------------------------- /examples/demo/client.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import demo_pb2_grpc 3 | import demo_pb2 4 | 5 | 6 | with grpc.insecure_channel('localhost:50051') as channel: 7 | stub = demo_pb2_grpc.UserControllerStub(channel) 8 | for user in stub.List(demo_pb2.UserListRequest()): 9 | print(user, end='') 10 | -------------------------------------------------------------------------------- /examples/tutorial/blog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Post(models.Model): 5 | title = models.CharField(max_length=100) 6 | content = models.TextField() 7 | created = models.DateTimeField(auto_now_add=True) 8 | 9 | class Meta: 10 | ordering = ['created'] 11 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 5 | Version 0.2.1 6 | ------------- 7 | 8 | - Fixed close_old_connections in test channel 9 | 10 | 11 | Version 0.2 12 | ----------- 13 | 14 | - Added test module 15 | - Added proto serializers 16 | - Added proto generators 17 | 18 | 19 | Version 0.1 20 | ----------- 21 | 22 | First public release. 23 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/serializers.py: -------------------------------------------------------------------------------- 1 | from django_grpc_framework import proto_serializers 2 | from hrm.models import Person 3 | import hrm_pb2 4 | 5 | 6 | class PersonProtoSerializer(proto_serializers.ModelProtoSerializer): 7 | class Meta: 8 | model = Person 9 | proto_class = hrm_pb2.Person 10 | fields = '__all__' 11 | -------------------------------------------------------------------------------- /examples/tutorial/blog/serializers.py: -------------------------------------------------------------------------------- 1 | from django_grpc_framework import proto_serializers 2 | from blog.models import Post 3 | from blog_proto import post_pb2 4 | 5 | 6 | class PostProtoSerializer(proto_serializers.ModelProtoSerializer): 7 | class Meta: 8 | model = Post 9 | proto_class = post_pb2.Post 10 | fields = ['id', 'title', 'content'] 11 | -------------------------------------------------------------------------------- /examples/partial_update/partial_update/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | import hrm_pb2_grpc 4 | from hrm.services import PersonService 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | ] 9 | 10 | 11 | def grpc_handlers(server): 12 | hrm_pb2_grpc.add_PersonControllerServicer_to_server(PersonService.as_servicer(), server) -------------------------------------------------------------------------------- /django_grpc_framework/utils/model_meta.py: -------------------------------------------------------------------------------- 1 | def get_model_pk(model): 2 | opts = model._meta.concrete_model._meta 3 | pk = opts.pk 4 | rel = pk.remote_field 5 | 6 | while rel and rel.parent_link: 7 | # If model is a child via multi-table inheritance, use parent's pk. 8 | pk = pk.remote_field.model._meta.pk 9 | rel = pk.remote_field 10 | 11 | return pk -------------------------------------------------------------------------------- /examples/quickstart/account/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django_grpc_framework import proto_serializers 3 | import account_pb2 4 | 5 | 6 | class UserProtoSerializer(proto_serializers.ModelProtoSerializer): 7 | class Meta: 8 | model = User 9 | proto_class = account_pb2.User 10 | fields = ['id', 'username', 'email', 'groups'] 11 | -------------------------------------------------------------------------------- /examples/null_support/null_support/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from snippets.services import SnippetService 4 | import snippets_pb2_grpc 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | ] 9 | 10 | def grpc_handlers(server): 11 | snippets_pb2_grpc.add_SnippetControllerServicer_to_server(SnippetService.as_servicer(), server) 12 | -------------------------------------------------------------------------------- /django_grpc_framework/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | from django.db import reset_queries, close_old_connections 3 | 4 | 5 | grpc_request_started = Signal() 6 | grpc_request_finished = Signal() 7 | 8 | 9 | # db connection state managed similarly to the wsgi handler 10 | grpc_request_started.connect(reset_queries) 11 | grpc_request_started.connect(close_old_connections) 12 | grpc_request_finished.connect(close_old_connections) -------------------------------------------------------------------------------- /examples/partial_update/client.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import hrm_pb2 3 | import hrm_pb2_grpc 4 | from google.protobuf.wrappers_pb2 import StringValue 5 | 6 | 7 | with grpc.insecure_channel('localhost:50051') as channel: 8 | stub = hrm_pb2_grpc.PersonControllerStub(channel) 9 | request = hrm_pb2.PersonPartialUpdateRequest(id=1, name=StringValue(value="amy")) 10 | response = stub.PartialUpdate(request) 11 | print(response, end='') 12 | -------------------------------------------------------------------------------- /examples/quickstart/account/services.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django_grpc_framework import generics 3 | from account.serializers import UserProtoSerializer 4 | 5 | 6 | class UserService(generics.ModelService): 7 | """ 8 | gRPC service that allows users to be retrieved or updated. 9 | """ 10 | queryset = User.objects.all().order_by('-date_joined') 11 | serializer_class = UserProtoSerializer 12 | -------------------------------------------------------------------------------- /examples/demo/demo/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for demo project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo 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/3.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', 'demo.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/tutorial/tutorial/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for tutorial project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tutorial.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/tutorial/tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tutorial 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/3.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', 'tutorial.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/null_support/snippets.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package snippets; 4 | 5 | import "google/protobuf/struct.proto"; 6 | 7 | service SnippetController { 8 | rpc Update(Snippet) returns (Snippet) {} 9 | } 10 | 11 | message NullableString { 12 | oneof kind { 13 | string value = 1; 14 | google.protobuf.NullValue null = 2; 15 | } 16 | } 17 | 18 | message Snippet { 19 | int32 id = 1; 20 | string title = 2; 21 | NullableString language = 3; 22 | } 23 | -------------------------------------------------------------------------------- /examples/quickstart/quickstart/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for quickstart project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'quickstart.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/quickstart/quickstart/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for quickstart 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/3.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', 'quickstart.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docs/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial: 2 | 3 | Tutorial 4 | ======== 5 | 6 | This part provides a basic introduction to work with Django gRPC framework. 7 | In this tutorial, we will create a simple blog rpc server. You can get the 8 | source code in `tutorial example`_. 9 | 10 | .. _tutorial example: 11 | https://github.com/fengsp/django-grpc-framework/tree/master/examples/tutorial 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | building_services 17 | using_generics 18 | writing_tests -------------------------------------------------------------------------------- /django_grpc_framework/protobuf/json_format.py: -------------------------------------------------------------------------------- 1 | from google.protobuf.json_format import MessageToDict, ParseDict 2 | 3 | 4 | def message_to_dict(message, **kwargs): 5 | kwargs.setdefault('including_default_value_fields', True) 6 | kwargs.setdefault('preserving_proto_field_name', True) 7 | return MessageToDict(message, **kwargs) 8 | 9 | 10 | def parse_dict(js_dict, message, **kwargs): 11 | kwargs.setdefault('ignore_unknown_fields', True) 12 | return ParseDict(js_dict, message, **kwargs) 13 | -------------------------------------------------------------------------------- /examples/null_support/null_support/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for null_support project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'null_support.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/null_support/null_support/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for null_support 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/3.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', 'null_support.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/partial_update/partial_update/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for partial_update project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'partial_update.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/partial_update/partial_update/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for partial_update 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/3.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', 'partial_update.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/partial_update/hrm.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package hrm; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | service PersonController { 8 | rpc PartialUpdate(PersonPartialUpdateRequest) returns (Person) {} 9 | } 10 | 11 | message Person { 12 | int32 id = 1; 13 | string name = 2; 14 | string email = 3; 15 | } 16 | 17 | message PersonPartialUpdateRequest { 18 | int32 id = 1; 19 | google.protobuf.StringValue name = 2; 20 | google.protobuf.StringValue email = 3; 21 | } 22 | -------------------------------------------------------------------------------- /examples/null_support/client.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import snippets_pb2 3 | import snippets_pb2_grpc 4 | from google.protobuf.struct_pb2 import NullValue 5 | 6 | 7 | with grpc.insecure_channel('localhost:50051') as channel: 8 | stub = snippets_pb2_grpc.SnippetControllerStub(channel) 9 | request = snippets_pb2.Snippet(id=1, title='snippet title') 10 | # send non-null value 11 | # request.language.value = "python" 12 | # send null value 13 | request.language.null = NullValue.NULL_VALUE 14 | response = stub.Update(request) 15 | print(response, end='') 16 | -------------------------------------------------------------------------------- /examples/quickstart/account/tests.py: -------------------------------------------------------------------------------- 1 | from django_grpc_framework.test import RPCTestCase 2 | from django.contrib.auth.models import User 3 | import account_pb2 4 | import account_pb2_grpc 5 | 6 | 7 | class UserServiceTest(RPCTestCase): 8 | def test_create_user(self): 9 | stub = account_pb2_grpc.UserControllerStub(self.channel) 10 | response = stub.Create(account_pb2.User(username='tom', email='tom@account.com')) 11 | self.assertEqual(response.username, 'tom') 12 | self.assertEqual(response.email, 'tom@account.com') 13 | self.assertEqual(User.objects.count(), 1) 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clean-pyc test 2 | 3 | tox-test: 4 | tox 5 | 6 | clean-pyc: 7 | find . -name '*.pyc' -exec rm -f {} + 8 | find . -name '*.pyo' -exec rm -f {} + 9 | find . -name '*~' -exec rm -f {} + 10 | 11 | lines: 12 | find . -name "*.py"|xargs cat|wc -l 13 | 14 | release: 15 | python setup.py sdist upload 16 | python setup.py bdist_wheel upload 17 | 18 | test: 19 | @py.test -vv --tb=short tests 20 | 21 | flake8: 22 | @flake8 --ignore=E501,F401,W292,W503 django_grpc_framework examples/tutorial/blog examples/tutorial/tutorial examples/tutorial/blog_client.py examples/quickstart/account examples/null_support/snippets 23 | -------------------------------------------------------------------------------- /examples/demo/demo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package demo; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service UserController { 8 | rpc List(UserListRequest) returns (stream User) {} 9 | rpc Create(User) returns (User) {} 10 | rpc Retrieve(UserRetrieveRequest) returns (User) {} 11 | rpc Update(User) returns (User) {} 12 | rpc Destroy(User) returns (google.protobuf.Empty) {} 13 | } 14 | 15 | message User { 16 | int32 id = 1; 17 | string username = 2; 18 | string email = 3; 19 | } 20 | 21 | message UserListRequest { 22 | } 23 | 24 | message UserRetrieveRequest { 25 | int32 id = 1; 26 | } 27 | -------------------------------------------------------------------------------- /examples/tutorial/protos/blog_proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package blog_proto; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service PostController { 8 | rpc List(PostListRequest) returns (stream Post) {} 9 | rpc Create(Post) returns (Post) {} 10 | rpc Retrieve(PostRetrieveRequest) returns (Post) {} 11 | rpc Update(Post) returns (Post) {} 12 | rpc Destroy(Post) returns (google.protobuf.Empty) {} 13 | } 14 | 15 | message Post { 16 | int32 id = 1; 17 | string title = 2; 18 | string content = 3; 19 | } 20 | 21 | message PostListRequest { 22 | } 23 | 24 | message PostRetrieveRequest { 25 | int32 id = 1; 26 | } 27 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-17 08:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Person', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('email', models.CharField(max_length=100)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /examples/quickstart/account.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package account; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service UserController { 8 | rpc List(UserListRequest) returns (stream User) {} 9 | rpc Create(User) returns (User) {} 10 | rpc Retrieve(UserRetrieveRequest) returns (User) {} 11 | rpc Update(User) returns (User) {} 12 | rpc Destroy(User) returns (google.protobuf.Empty) {} 13 | } 14 | 15 | message User { 16 | int32 id = 1; 17 | string username = 2; 18 | string email = 3; 19 | repeated int32 groups = 4; 20 | } 21 | 22 | message UserListRequest { 23 | } 24 | 25 | message UserRetrieveRequest { 26 | int32 id = 1; 27 | } 28 | -------------------------------------------------------------------------------- /examples/demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django_grpc_framework import generics, proto_serializers 3 | import demo_pb2 4 | import demo_pb2_grpc 5 | 6 | 7 | class UserProtoSerializer(proto_serializers.ModelProtoSerializer): 8 | class Meta: 9 | model = User 10 | proto_class = demo_pb2.User 11 | fields = ['id', 'username', 'email'] 12 | 13 | 14 | class UserService(generics.ModelService): 15 | queryset = User.objects.all() 16 | serializer_class = UserProtoSerializer 17 | 18 | 19 | urlpatterns = [] 20 | 21 | 22 | def grpc_handlers(server): 23 | demo_pb2_grpc.add_UserControllerServicer_to_server(UserService.as_servicer(), server) -------------------------------------------------------------------------------- /examples/null_support/snippets/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-20 16:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Snippet', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=100)), 19 | ('language', models.CharField(max_length=20, null=True)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /examples/demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /examples/tutorial/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tutorial.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /examples/quickstart/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'quickstart.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /examples/null_support/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'null_support.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /examples/partial_update/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'partial_update.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /examples/tutorial/blog/services.py: -------------------------------------------------------------------------------- 1 | from blog.models import Post 2 | from blog.serializers import PostProtoSerializer 3 | from django_grpc_framework import mixins 4 | from django_grpc_framework import generics 5 | 6 | 7 | """ 8 | class PostService(mixins.ListModelMixin, 9 | mixins.CreateModelMixin, 10 | mixins.RetrieveModelMixin, 11 | mixins.UpdateModelMixin, 12 | mixins.DestroyModelMixin, 13 | generics.GenericService): 14 | queryset = Post.objects.all() 15 | serializer_class = PostProtoSerializer 16 | """ 17 | 18 | 19 | class PostService(generics.ModelService): 20 | queryset = Post.objects.all() 21 | serializer_class = PostProtoSerializer 22 | -------------------------------------------------------------------------------- /docs/protos.rst: -------------------------------------------------------------------------------- 1 | .. _protos: 2 | 3 | Proto 4 | ===== 5 | 6 | Django gRPC framework provides support for automatic generation of proto_. 7 | 8 | .. _proto: https://developers.google.com/protocol-buffers/docs/proto3 9 | 10 | 11 | Generate proto for model 12 | ------------------------ 13 | 14 | If you want to automatically generate proto definition based on a model, 15 | you can use the ``generateproto`` management command:: 16 | 17 | python manage.py generateproto --model django.contrib.auth.models.User 18 | 19 | To specify fields and save it to a file, use:: 20 | 21 | python manage.py generateproto --model django.contrib.auth.models.User --fields id,username,email --file demo.proto 22 | 23 | Once you've generated a proto file in this way, you can edit it as you wish. -------------------------------------------------------------------------------- /examples/quickstart/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run the ``python manage.py shell``, create two groups first:: 3 | 4 | from django.contrib.auth.models import Group 5 | Group.objects.create(name='group1') 6 | Group.objects.create(name='group2') 7 | """ 8 | import grpc 9 | import account_pb2_grpc 10 | import account_pb2 11 | 12 | 13 | with grpc.insecure_channel('localhost:50051') as channel: 14 | stub = account_pb2_grpc.UserControllerStub(channel) 15 | print('----- Create -----') 16 | request = account_pb2.User(username='tom', email='tom@account.com') 17 | request.groups.extend([1,2]) 18 | response = stub.Create(request) 19 | print(response, end='') 20 | print('----- List -----') 21 | for user in stub.List(account_pb2.UserListRequest()): 22 | print(user, end='') 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django gRPC Framework 2 | ===================== 3 | 4 | Django gRPC framework is a toolkit for building gRPC services with Django. 5 | Officially we only support proto3. 6 | 7 | 8 | User's Guide 9 | ------------ 10 | 11 | This part of the documentation begins with installation, followed by more 12 | instructions for building services. 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | installation 18 | quickstart 19 | tutorial/index 20 | services 21 | generics 22 | proto_serializers 23 | protos 24 | server 25 | testing 26 | settings 27 | patterns/index 28 | 29 | 30 | Additional Stuff 31 | ---------------- 32 | 33 | Changelog and license here if you are interested. 34 | 35 | .. toctree:: 36 | :maxdepth: 2 37 | 38 | changelog 39 | license 40 | -------------------------------------------------------------------------------- /examples/tutorial/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-02 09:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Post', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=100)), 19 | ('content', models.TextField()), 20 | ('created', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | options={ 23 | 'ordering': ['created'], 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /examples/partial_update/hrm/services.py: -------------------------------------------------------------------------------- 1 | from django_grpc_framework import generics, mixins 2 | from hrm.serializers import PersonProtoSerializer 3 | from hrm.models import Person 4 | 5 | 6 | """ 7 | class PersonService(generics.GenericService): 8 | queryset = Person.objects.all() 9 | serializer_class = PersonProtoSerializer 10 | 11 | def PartialUpdate(self, request, context): 12 | instance = self.get_object() 13 | serializer = self.get_serializer(instance, message=request, partial=True) 14 | serializer.is_valid(raise_exception=True) 15 | serializer.save() 16 | return serializer.message 17 | """ 18 | 19 | 20 | class PersonService(mixins.PartialUpdateModelMixin, 21 | generics.GenericService): 22 | queryset = Person.objects.all() 23 | serializer_class = PersonProtoSerializer 24 | -------------------------------------------------------------------------------- /examples/tutorial/blog_client.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | from blog_proto import post_pb2, post_pb2_grpc 3 | 4 | 5 | with grpc.insecure_channel('localhost:50051') as channel: 6 | stub = post_pb2_grpc.PostControllerStub(channel) 7 | print('----- Create -----') 8 | response = stub.Create(post_pb2.Post(title='t1', content='c1')) 9 | print(response, end='') 10 | print('----- List -----') 11 | for post in stub.List(post_pb2.PostListRequest()): 12 | print(post, end='') 13 | print('----- Retrieve -----') 14 | response = stub.Retrieve(post_pb2.PostRetrieveRequest(id=response.id)) 15 | print(response, end='') 16 | print('----- Update -----') 17 | response = stub.Update(post_pb2.Post(id=response.id, title='t2', content='c2')) 18 | print(response, end='') 19 | print('----- Delete -----') 20 | stub.Destroy(post_pb2.Post(id=response.id)) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/tutorial/blog/tests.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | from django_grpc_framework.test import RPCTestCase 3 | from blog_proto import post_pb2, post_pb2_grpc 4 | from blog.models import Post 5 | 6 | 7 | class PostServiceTest(RPCTestCase): 8 | def test_create_post(self): 9 | stub = post_pb2_grpc.PostControllerStub(self.channel) 10 | response = stub.Create(post_pb2.Post(title='title', content='content')) 11 | self.assertEqual(response.title, 'title') 12 | self.assertEqual(response.content, 'content') 13 | self.assertEqual(Post.objects.count(), 1) 14 | 15 | post_list = list(stub.List(post_pb2.PostListRequest())) 16 | self.assertEqual(len(post_list), 1) 17 | 18 | def test_list_posts(self): 19 | Post.objects.create(title='title1', content='content1') 20 | Post.objects.create(title='title2', content='content2') 21 | stub = post_pb2_grpc.PostControllerStub(self.channel) 22 | post_list = list(stub.List(post_pb2.PostListRequest())) 23 | self.assertEqual(len(post_list), 2) 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from setuptools import find_packages, setup 3 | 4 | 5 | with open('django_grpc_framework/__init__.py', 'rb') as f: 6 | version = str(eval(re.search(r'__version__\s+=\s+(.*)', 7 | f.read().decode('utf-8')).group(1))) 8 | 9 | 10 | setup( 11 | name='djangogrpcframework', 12 | version=version, 13 | description='gRPC for Django.', 14 | long_description=open('README.rst', 'r', encoding='utf-8').read(), 15 | url='https://github.com/fengsp/django-grpc-framework', 16 | author='Shipeng Feng', 17 | author_email='fsp261@gmail.com', 18 | packages=find_packages(), 19 | install_requires=[], 20 | python_requires=">=3.6", 21 | zip_safe=False, 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: Apache Software License', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.6', 29 | 'Programming Language :: Python :: 3.7', 30 | 'Programming Language :: Python :: 3.8', 31 | 'Programming Language :: Python :: 3 :: Only', 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | 7 | Requirements 8 | ------------ 9 | 10 | We requires the following: 11 | 12 | - Python (3.6, 3.7, 3.8) 13 | - Django (2.2, 3.0) 14 | - Django REST Framework (3.10.x, 3.11.x) 15 | - gRPC 16 | - gRPC tools 17 | - proto3 18 | 19 | 20 | virtualenv 21 | ---------- 22 | 23 | Virtualenv might be something you want to use for development! let's create 24 | one working environment:: 25 | 26 | $ mkdir myproject 27 | $ cd myproject 28 | $ python3 -m venv env 29 | $ source env/bin/activate 30 | 31 | It is time to get the django grpc framework:: 32 | 33 | $ pip install djangogrpcframework 34 | $ pip install django 35 | $ pip install djangorestframework 36 | $ pip install grpcio 37 | $ pip install grpcio-tools 38 | 39 | 40 | System Wide 41 | ----------- 42 | 43 | Install it for all users on the system:: 44 | 45 | $ sudo pip install djangogrpcframework 46 | 47 | 48 | Development Version 49 | ------------------- 50 | 51 | Try the latest version:: 52 | 53 | $ source env/bin/activate 54 | $ git clone https://github.com/fengsp/django-grpc-framework.git 55 | $ cd django-grpc-framework 56 | $ python setup.py develop 57 | -------------------------------------------------------------------------------- /examples/null_support/snippets/services.py: -------------------------------------------------------------------------------- 1 | from django_grpc_framework import generics, mixins 2 | from django_grpc_framework import proto_serializers 3 | from snippets.models import Snippet 4 | import snippets_pb2 5 | from google.protobuf.struct_pb2 import NullValue 6 | 7 | 8 | class SnippetProtoSerializer(proto_serializers.ModelProtoSerializer): 9 | class Meta: 10 | model = Snippet 11 | fields = '__all__' 12 | 13 | def message_to_data(self, message): 14 | data = { 15 | 'title': message.title, 16 | } 17 | if message.language.HasField('value'): 18 | data['language'] = message.language.value 19 | elif message.language.HasField('null'): 20 | data['language'] = None 21 | return data 22 | 23 | def data_to_message(self, data): 24 | message = snippets_pb2.Snippet( 25 | id=data['id'], 26 | title=data['title'], 27 | ) 28 | if data['language'] is None: 29 | message.language.null = NullValue.NULL_VALUE 30 | else: 31 | message.language.value = data['language'] 32 | return message 33 | 34 | 35 | class SnippetService(mixins.UpdateModelMixin, 36 | generics.GenericService): 37 | queryset = Snippet.objects.all() 38 | serializer_class = SnippetProtoSerializer 39 | -------------------------------------------------------------------------------- /docs/services.rst: -------------------------------------------------------------------------------- 1 | .. _services: 2 | 3 | Services 4 | ======== 5 | 6 | Django gRPC framework provides an ``Service`` class, which is pretty much the 7 | same as using a regular gRPC generated servicer interface. For example:: 8 | 9 | import grpc 10 | from django_grpc_framework.services import Service 11 | from blog.models import Post 12 | from blog.serializers import PostProtoSerializer 13 | 14 | 15 | class PostService(Service): 16 | def get_object(self, pk): 17 | try: 18 | return Post.objects.get(pk=pk) 19 | except Post.DoesNotExist: 20 | self.context.abort(grpc.StatusCode.NOT_FOUND, 'Post:%s not found!' % pk) 21 | 22 | def Retrieve(self, request, context): 23 | post = self.get_object(request.id) 24 | serializer = PostProtoSerializer(post) 25 | return serializer.message 26 | 27 | 28 | Service instance attributes 29 | --------------------------- 30 | 31 | The following attributes are available in a service instance. 32 | 33 | - ``.request`` - the gRPC request object 34 | - ``.context`` - the ``grpc.ServicerContext`` object 35 | - ``.action`` - the name of the current service method 36 | 37 | 38 | As servicer method 39 | ------------------ 40 | 41 | .. currentmodule:: django_grpc_framework.services 42 | 43 | .. automethod:: Service.as_servicer -------------------------------------------------------------------------------- /docs/tutorial/writing_tests.rst: -------------------------------------------------------------------------------- 1 | .. _writing_tests: 2 | 3 | Writing and running tests 4 | ========================= 5 | 6 | Let's write some tests for our service and run them. 7 | 8 | 9 | Writing tests 10 | ------------- 11 | 12 | Let's edit the ``blog/tests.py`` file:: 13 | 14 | import grpc 15 | from django_grpc_framework.test import RPCTestCase 16 | from blog_proto import post_pb2, post_pb2_grpc 17 | from blog.models import Post 18 | 19 | 20 | class PostServiceTest(RPCTestCase): 21 | def test_create_post(self): 22 | stub = post_pb2_grpc.PostControllerStub(self.channel) 23 | response = stub.Create(post_pb2.Post(title='title', content='content')) 24 | self.assertEqual(response.title, 'title') 25 | self.assertEqual(response.content, 'content') 26 | self.assertEqual(Post.objects.count(), 1) 27 | 28 | def test_list_posts(self): 29 | Post.objects.create(title='title1', content='content1') 30 | Post.objects.create(title='title2', content='content2') 31 | stub = post_pb2_grpc.PostControllerStub(self.channel) 32 | post_list = list(stub.List(post_pb2.PostListRequest())) 33 | self.assertEqual(len(post_list), 2) 34 | 35 | 36 | Running tests 37 | ------------- 38 | 39 | Once you've written tests, run them:: 40 | 41 | python manage.py test -------------------------------------------------------------------------------- /examples/tutorial/blog/_services.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | from google.protobuf import empty_pb2 3 | from django_grpc_framework.services import Service 4 | from blog.models import Post 5 | from blog.serializers import PostProtoSerializer 6 | 7 | 8 | class PostService(Service): 9 | def List(self, request, context): 10 | posts = Post.objects.all() 11 | serializer = PostProtoSerializer(posts, many=True) 12 | for msg in serializer.message: 13 | yield msg 14 | 15 | def Create(self, request, context): 16 | serializer = PostProtoSerializer(message=request) 17 | serializer.is_valid(raise_exception=True) 18 | serializer.save() 19 | return serializer.message 20 | 21 | def get_object(self, pk): 22 | try: 23 | return Post.objects.get(pk=pk) 24 | except Post.DoesNotExist: 25 | self.context.abort(grpc.StatusCode.NOT_FOUND, 'Post:%s not found!' % pk) 26 | 27 | def Retrieve(self, request, context): 28 | post = self.get_object(request.id) 29 | serializer = PostProtoSerializer(post) 30 | return serializer.message 31 | 32 | def Update(self, request, context): 33 | post = self.get_object(request.id) 34 | serializer = PostProtoSerializer(post, message=request) 35 | serializer.is_valid(raise_exception=True) 36 | serializer.save() 37 | return serializer.message 38 | 39 | def Destroy(self, request, context): 40 | post = self.get_object(request.id) 41 | post.delete() 42 | return empty_pb2.Empty() 43 | -------------------------------------------------------------------------------- /docs/server.rst: -------------------------------------------------------------------------------- 1 | .. _server: 2 | 3 | Server 4 | ====== 5 | 6 | grpcrunserver 7 | ------------- 8 | 9 | Run a grpc server:: 10 | 11 | $ python manage.py grpcrunserver 12 | 13 | Run a grpc development server, this tells Django to use the auto-reloader and 14 | run checks:: 15 | 16 | $ python manage.py grpcrunserver --dev 17 | 18 | Run the server with a certain address:: 19 | 20 | $ python manage.py grpcrunserver 127.0.0.1:8000 --max-workers 5 21 | 22 | 23 | Configuration 24 | ------------- 25 | 26 | Root handlers hook 27 | ``````````````````` 28 | 29 | We need a hanlders hook function to add all servicers to the server, for 30 | example:: 31 | 32 | def grpc_handlers(server): 33 | demo_pb2_grpc.add_UserControllerServicer_to_server(UserService.as_servicer(), server) 34 | 35 | You can set the root handlers hook using the ``ROOT_HANDLERS_HOOK`` setting 36 | key, for example set the following in your ``settings.py`` file:: 37 | 38 | GRPC_FRAMEWORK = { 39 | ... 40 | 'ROOT_HANDLERS_HOOK': 'path.to.your.curtom_grpc_handlers', 41 | } 42 | 43 | The default setting is ``'{settings.ROOT_URLCONF}.grpc_handlers'``. 44 | 45 | Setting the server interceptors 46 | ``````````````````````````````` 47 | 48 | If you need to add server interceptors, you can do so by setting the 49 | 50 | ``SERVER_INTERCEPTORS`` setting. For example, have something like this 51 | in your ``settings.py`` file:: 52 | 53 | GRPC_FRAMEWORK = { 54 | ... 55 | 'SERVER_INTERCEPTORS': [ 56 | 'path.to.DoSomethingInterceptor', 57 | 'path.to.DoAnotherThingInterceptor', 58 | ] 59 | } -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | .. _settings: 2 | 3 | Settings 4 | ======== 5 | 6 | Configuration for gRPC framework is all namespaced inside a single Django 7 | setting, named ``GRPC_FRAMEWORK``, for example your project's ``settings.py`` 8 | file might look like this:: 9 | 10 | GRPC_FRAMEWORK = { 11 | 'ROOT_HANDLERS_HOOK': 'project.urls.grpc_handlers', 12 | } 13 | 14 | 15 | Accessing settings 16 | ------------------ 17 | 18 | If you need to access the values of gRPC framework's settings in your project, 19 | you should use the ``grpc_settings`` object. For example:: 20 | 21 | from django_grpc_framework.settings import grpc_settings 22 | print(grpc_settings.ROOT_HANDLERS_HOOK) 23 | 24 | The ``grpc_settings`` object will check for any user-defined settings, and 25 | otherwise fall back to the default values. Any setting that uses string import 26 | paths to refer to a class will automatically import and return the referenced 27 | class, instead of the string literal. 28 | 29 | 30 | Configuration values 31 | -------------------- 32 | 33 | .. py:data:: ROOT_HANDLERS_HOOK 34 | 35 | A hook function that takes gRPC server object as a single parameter and add 36 | all servicers to the server. 37 | 38 | Default: ``'{settings.ROOT_URLCONF}.grpc_handlers'`` 39 | 40 | One example for the hook function:: 41 | 42 | def grpc_handlers(server): 43 | demo_pb2_grpc.add_UserControllerServicer_to_server(UserService.as_servicer(), server) 44 | 45 | .. py:data:: SERVER_INTERCEPTORS 46 | 47 | An optional list of ServerInterceptor objects that observe and optionally 48 | manipulate the incoming RPCs before handing them over to handlers. 49 | 50 | Default: ``None`` -------------------------------------------------------------------------------- /django_grpc_framework/management/commands/generateproto.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.management.base import BaseCommand, CommandError 4 | from django.utils.module_loading import import_string 5 | 6 | from django_grpc_framework.protobuf.generators import ModelProtoGenerator 7 | 8 | 9 | class Command(BaseCommand): 10 | help = "Generates proto." 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument( 14 | '--model', dest='model', type=str, required=True, 15 | help='dotted path to a model class', 16 | ) 17 | parser.add_argument( 18 | '--fields', dest='fields', default=None, type=str, 19 | help='specify which fields to include, comma-seperated' 20 | ) 21 | parser.add_argument( 22 | '--file', dest='file', default=None, type=str, 23 | help='the generated proto file path' 24 | ) 25 | 26 | def handle(self, *args, **options): 27 | model = import_string(options['model']) 28 | fields = options['fields'].split(',') if options['fields'] else None 29 | filepath = options['file'] 30 | if filepath and os.path.exists(filepath): 31 | raise CommandError('File "%s" already exists.' % filepath) 32 | if filepath: 33 | package = os.path.splitext(os.path.basename(filepath))[0] 34 | else: 35 | package = None 36 | generator = ModelProtoGenerator( 37 | model=model, 38 | field_names=fields, 39 | package=package, 40 | ) 41 | proto = generator.get_proto() 42 | if filepath: 43 | with open(filepath, 'w') as f: 44 | f.write(proto) 45 | else: 46 | self.stdout.write(proto) -------------------------------------------------------------------------------- /docs/tutorial/using_generics.rst: -------------------------------------------------------------------------------- 1 | .. _using_generics: 2 | 3 | Using Generic Services 4 | ====================== 5 | 6 | We provide a number of pre-built services as a shortcut for common usage 7 | patterns. The generic services allow you to quickly build services that 8 | map closely to database models. 9 | 10 | 11 | Using mixins 12 | ------------ 13 | 14 | The create/list/retrieve/update/destroy operations that we've been using 15 | so far are going to be similar for any model-backend services. Those 16 | operations are implemented in gRPC framework's mixin classes. 17 | 18 | Let's take a look at how we can compose the services by using the mixin 19 | classes, here is our ``blog/services`` file again:: 20 | 21 | from blog.models import Post 22 | from blog.serializers import PostProtoSerializer 23 | from django_grpc_framework import mixins 24 | from django_grpc_framework import generics 25 | 26 | 27 | class PostService(mixins.ListModelMixin, 28 | mixins.CreateModelMixin, 29 | mixins.RetrieveModelMixin, 30 | mixins.UpdateModelMixin, 31 | mixins.DestroyModelMixin, 32 | generics.GenericService): 33 | queryset = Post.objects.all() 34 | serializer_class = PostProtoSerializer 35 | 36 | We are building our service with ``GenericService``, and adding in 37 | ``ListModelMixin``,``CreateModelMixin``, etc. The base class provides the 38 | core functionality, and the mixin classes provice the ``.List()`` and 39 | ``.Create()`` handlers. 40 | 41 | 42 | Using model service 43 | ------------------- 44 | 45 | If you want all operations of create/list/retrieve/update/destroy, we provide 46 | one already mixed-in generic services:: 47 | 48 | class PostService(generics.ModelService): 49 | queryset = Post.objects.all() 50 | serializer_class = PostProtoSerializer -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | .. _testing: 2 | 3 | Testing 4 | ======= 5 | 6 | Django gRPC framework includes a few helper classes that come in handy when 7 | writing tests for services. 8 | 9 | 10 | The test channel 11 | ---------------- 12 | 13 | The test channel is a Python class that acts as a dummy gRPC channel, 14 | allowing you to test you services. You can simulate gRPC requests on a 15 | service method and get the response. Here is a quick example, let's open 16 | Django shell ``python manage.py shell``: 17 | 18 | .. code-block:: pycon 19 | 20 | >>> from django_grpc_framework.test import Channel 21 | >>> channel = Channel() 22 | >>> stub = post_pb2_grpc.PostControllerStub(channel) 23 | >>> response = stub.Retrieve(post_pb2.PostRetrieveRequest(id=post_id)) 24 | >>> response.title 25 | 'This is a title' 26 | 27 | 28 | RPC test cases 29 | -------------- 30 | 31 | Django gRPC framework includes the following test case classes, that mirror 32 | the existing Django test case classes, but provide a test ``Channel`` 33 | instead of ``Client``. 34 | 35 | - ``RPCSimpleTestCase`` 36 | - ``RPCTransactionTestCase`` 37 | - ``RPCTestCase`` 38 | 39 | You can use these test case classes as you would for the regular Django test 40 | case classes, the ``self.channel`` attribute will be an ``Channel`` instance:: 41 | 42 | from django_grpc_framework.test import RPCTestCase 43 | from django.contrib.auth.models import User 44 | import account_pb2 45 | import account_pb2_grpc 46 | 47 | 48 | class UserServiceTest(RPCTestCase): 49 | def test_create_user(self): 50 | stub = account_pb2_grpc.UserControllerStub(self.channel) 51 | response = stub.Create(account_pb2.User(username='tom', email='tom@account.com')) 52 | self.assertEqual(response.username, 'tom') 53 | self.assertEqual(response.email, 'tom@account.com') 54 | self.assertEqual(User.objects.count(), 1) 55 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .vscode/ 132 | -------------------------------------------------------------------------------- /examples/null_support/snippets_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | import snippets_pb2 as snippets__pb2 5 | 6 | 7 | class SnippetControllerStub(object): 8 | """Missing associated documentation comment in .proto file""" 9 | 10 | def __init__(self, channel): 11 | """Constructor. 12 | 13 | Args: 14 | channel: A grpc.Channel. 15 | """ 16 | self.Update = channel.unary_unary( 17 | '/snippets.SnippetController/Update', 18 | request_serializer=snippets__pb2.Snippet.SerializeToString, 19 | response_deserializer=snippets__pb2.Snippet.FromString, 20 | ) 21 | 22 | 23 | class SnippetControllerServicer(object): 24 | """Missing associated documentation comment in .proto file""" 25 | 26 | def Update(self, request, context): 27 | """Missing associated documentation comment in .proto file""" 28 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 29 | context.set_details('Method not implemented!') 30 | raise NotImplementedError('Method not implemented!') 31 | 32 | 33 | def add_SnippetControllerServicer_to_server(servicer, server): 34 | rpc_method_handlers = { 35 | 'Update': grpc.unary_unary_rpc_method_handler( 36 | servicer.Update, 37 | request_deserializer=snippets__pb2.Snippet.FromString, 38 | response_serializer=snippets__pb2.Snippet.SerializeToString, 39 | ), 40 | } 41 | generic_handler = grpc.method_handlers_generic_handler( 42 | 'snippets.SnippetController', rpc_method_handlers) 43 | server.add_generic_rpc_handlers((generic_handler,)) 44 | 45 | 46 | # This class is part of an EXPERIMENTAL API. 47 | class SnippetController(object): 48 | """Missing associated documentation comment in .proto file""" 49 | 50 | @staticmethod 51 | def Update(request, 52 | target, 53 | options=(), 54 | channel_credentials=None, 55 | call_credentials=None, 56 | compression=None, 57 | wait_for_ready=None, 58 | timeout=None, 59 | metadata=None): 60 | return grpc.experimental.unary_unary(request, target, '/snippets.SnippetController/Update', 61 | snippets__pb2.Snippet.SerializeToString, 62 | snippets__pb2.Snippet.FromString, 63 | options, channel_credentials, 64 | call_credentials, compression, wait_for_ready, timeout, metadata) 65 | -------------------------------------------------------------------------------- /examples/partial_update/hrm_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | import hrm_pb2 as hrm__pb2 5 | 6 | 7 | class PersonControllerStub(object): 8 | """Missing associated documentation comment in .proto file""" 9 | 10 | def __init__(self, channel): 11 | """Constructor. 12 | 13 | Args: 14 | channel: A grpc.Channel. 15 | """ 16 | self.PartialUpdate = channel.unary_unary( 17 | '/hrm.PersonController/PartialUpdate', 18 | request_serializer=hrm__pb2.PersonPartialUpdateRequest.SerializeToString, 19 | response_deserializer=hrm__pb2.Person.FromString, 20 | ) 21 | 22 | 23 | class PersonControllerServicer(object): 24 | """Missing associated documentation comment in .proto file""" 25 | 26 | def PartialUpdate(self, request, context): 27 | """Missing associated documentation comment in .proto file""" 28 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 29 | context.set_details('Method not implemented!') 30 | raise NotImplementedError('Method not implemented!') 31 | 32 | 33 | def add_PersonControllerServicer_to_server(servicer, server): 34 | rpc_method_handlers = { 35 | 'PartialUpdate': grpc.unary_unary_rpc_method_handler( 36 | servicer.PartialUpdate, 37 | request_deserializer=hrm__pb2.PersonPartialUpdateRequest.FromString, 38 | response_serializer=hrm__pb2.Person.SerializeToString, 39 | ), 40 | } 41 | generic_handler = grpc.method_handlers_generic_handler( 42 | 'hrm.PersonController', rpc_method_handlers) 43 | server.add_generic_rpc_handlers((generic_handler,)) 44 | 45 | 46 | # This class is part of an EXPERIMENTAL API. 47 | class PersonController(object): 48 | """Missing associated documentation comment in .proto file""" 49 | 50 | @staticmethod 51 | def PartialUpdate(request, 52 | target, 53 | options=(), 54 | channel_credentials=None, 55 | call_credentials=None, 56 | compression=None, 57 | wait_for_ready=None, 58 | timeout=None, 59 | metadata=None): 60 | return grpc.experimental.unary_unary(request, target, '/hrm.PersonController/PartialUpdate', 61 | hrm__pb2.PersonPartialUpdateRequest.SerializeToString, 62 | hrm__pb2.Person.FromString, 63 | options, channel_credentials, 64 | call_credentials, compression, wait_for_ready, timeout, metadata) 65 | -------------------------------------------------------------------------------- /django_grpc_framework/services.py: -------------------------------------------------------------------------------- 1 | from functools import update_wrapper 2 | 3 | import grpc 4 | from django.db.models.query import QuerySet 5 | 6 | from django_grpc_framework.signals import grpc_request_started, grpc_request_finished 7 | 8 | 9 | class Service: 10 | def __init__(self, **kwargs): 11 | for key, value in kwargs.items(): 12 | setattr(self, key, value) 13 | 14 | @classmethod 15 | def as_servicer(cls, **initkwargs): 16 | """ 17 | Returns a gRPC servicer instance:: 18 | 19 | servicer = PostService.as_servicer() 20 | add_PostControllerServicer_to_server(servicer, server) 21 | """ 22 | for key in initkwargs: 23 | if not hasattr(cls, key): 24 | raise TypeError( 25 | "%s() received an invalid keyword %r. as_servicer only " 26 | "accepts arguments that are already attributes of the " 27 | "class." % (cls.__name__, key) 28 | ) 29 | if isinstance(getattr(cls, 'queryset', None), QuerySet): 30 | def force_evaluation(): 31 | raise RuntimeError( 32 | 'Do not evaluate the `.queryset` attribute directly, ' 33 | 'as the result will be cached and reused between requests.' 34 | ' Use `.all()` or call `.get_queryset()` instead.' 35 | ) 36 | cls.queryset._fetch_all = force_evaluation 37 | 38 | class Servicer: 39 | def __getattr__(self, action): 40 | if not hasattr(cls, action): 41 | return not_implemented 42 | 43 | def handler(request, context): 44 | grpc_request_started.send(sender=handler, request=request, context=context) 45 | try: 46 | self = cls(**initkwargs) 47 | self.request = request 48 | self.context = context 49 | self.action = action 50 | return getattr(self, action)(request, context) 51 | finally: 52 | grpc_request_finished.send(sender=handler) 53 | update_wrapper(handler, getattr(cls, action)) 54 | return handler 55 | update_wrapper(Servicer, cls, updated=()) 56 | return Servicer() 57 | 58 | 59 | def not_implemented(request, context): 60 | """Method not implemented""" 61 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 62 | context.set_details('Method not implemented!') 63 | raise NotImplementedError('Method not implemented!') 64 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'django-grpc-framework' 21 | copyright = '2020, Shipeng Feng' 22 | author = 'Shipeng Feng' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | import pkg_resources 26 | try: 27 | release = pkg_resources.get_distribution('djangogrpcframework').version 28 | except pkg_resources.DistributionNotFound: 29 | print('To build the documentation, the distribution information of') 30 | print('django-grpc-framework has to be available. Run "setup.py develop"') 31 | print('to setup the metadata. A virtualenv is recommended!') 32 | sys.exit(1) 33 | del pkg_resources 34 | 35 | 36 | # -- General configuration --------------------------------------------------- 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.intersphinx', 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 53 | 54 | 55 | # -- Options for HTML output ------------------------------------------------- 56 | 57 | # The theme to use for HTML and HTML Help pages. See the documentation for 58 | # a list of builtin themes. 59 | # 60 | html_theme = 'alabaster' 61 | 62 | html_theme_options = { 63 | 'github_button': True, 64 | 'github_user': 'fengsp', 65 | 'github_repo': 'django-grpc-framework', 66 | } 67 | 68 | # Add any paths that contain custom static files (such as style sheets) here, 69 | # relative to this directory. They are copied after the builtin static files, 70 | # so a file named "default.css" will overwrite the builtin "default.css". 71 | html_static_path = ['_static'] 72 | 73 | # The master toctree document. 74 | master_doc = 'index' 75 | -------------------------------------------------------------------------------- /docs/patterns/null_support.rst: -------------------------------------------------------------------------------- 1 | .. _null_support: 2 | 3 | Null Support 4 | ============ 5 | 6 | In proto3, all fields are never null. However, we can use ``Oneof`` to define 7 | a nullable type, for example: 8 | 9 | .. code-block:: protobuf 10 | 11 | syntax = "proto3"; 12 | 13 | package snippets; 14 | 15 | import "google/protobuf/struct.proto"; 16 | 17 | service SnippetController { 18 | rpc Update(Snippet) returns (Snippet) {} 19 | } 20 | 21 | message NullableString { 22 | oneof kind { 23 | string value = 1; 24 | google.protobuf.NullValue null = 2; 25 | } 26 | } 27 | 28 | message Snippet { 29 | int32 id = 1; 30 | string title = 2; 31 | NullableString language = 3; 32 | } 33 | 34 | The client example:: 35 | 36 | import grpc 37 | import snippets_pb2 38 | import snippets_pb2_grpc 39 | from google.protobuf.struct_pb2 import NullValue 40 | 41 | 42 | with grpc.insecure_channel('localhost:50051') as channel: 43 | stub = snippets_pb2_grpc.SnippetControllerStub(channel) 44 | request = snippets_pb2.Snippet(id=1, title='snippet title') 45 | # send non-null value 46 | # request.language.value = "python" 47 | # send null value 48 | request.language.null = NullValue.NULL_VALUE 49 | response = stub.Update(request) 50 | print(response, end='') 51 | 52 | The service implementation:: 53 | 54 | from django_grpc_framework import generics, mixins 55 | from django_grpc_framework import proto_serializers 56 | from snippets.models import Snippet 57 | import snippets_pb2 58 | from google.protobuf.struct_pb2 import NullValue 59 | 60 | 61 | class SnippetProtoSerializer(proto_serializers.ModelProtoSerializer): 62 | class Meta: 63 | model = Snippet 64 | fields = '__all__' 65 | 66 | def message_to_data(self, message): 67 | data = { 68 | 'title': message.title, 69 | } 70 | if message.language.HasField('value'): 71 | data['language'] = message.language.value 72 | elif message.language.HasField('null'): 73 | data['language'] = None 74 | return data 75 | 76 | def data_to_message(self, data): 77 | message = snippets_pb2.Snippet( 78 | id=data['id'], 79 | title=data['title'], 80 | ) 81 | if data['language'] is None: 82 | message.language.null = NullValue.NULL_VALUE 83 | else: 84 | message.language.value = data['language'] 85 | return message 86 | 87 | 88 | class SnippetService(mixins.UpdateModelMixin, 89 | generics.GenericService): 90 | queryset = Snippet.objects.all() 91 | serializer_class = SnippetProtoSerializer -------------------------------------------------------------------------------- /docs/patterns/partial_update.rst: -------------------------------------------------------------------------------- 1 | .. _partial_update: 2 | 3 | Handling Partial Update 4 | ======================= 5 | 6 | In proto3: 7 | 8 | 1. All fields are optional 9 | 2. Singular primitive fields, repeated fields, and map fields are initialized 10 | with default values (0, empty list, etc). There's no way of telling whether 11 | a field was explicitly set to the default value (for example whether a 12 | boolean was set to false) or just not set at all. 13 | 14 | If we want to do a partial update on resources, we need to know whether a field 15 | was set or not set at all. There are different strategies that can be used to 16 | represent ``unset``, we'll use a pattern called ``"Has Pattern"`` here. 17 | 18 | Singular field absence 19 | ---------------------- 20 | 21 | In proto3, for singular field types, you can use the parent message's 22 | ``HasField()`` method to check if a message type field value has been set, 23 | but you can't do it with non-message singular types. 24 | 25 | For primitive types if you need ``HasField`` to you could use 26 | ``"google/protobuf/wrappers.proto"``. Wrappers are useful for places where you 27 | need to distinguish between the absence of a primitive typed field and its 28 | default value: 29 | 30 | .. code-block:: protobuf 31 | 32 | import "google/protobuf/wrappers.proto"; 33 | 34 | service PersonController { 35 | rpc PartialUpdate(PersonPartialUpdateRequest) returns (Person) {} 36 | } 37 | 38 | message Person { 39 | int32 id = 1; 40 | string name = 2; 41 | string email = 3; 42 | } 43 | 44 | message PersonPartialUpdateRequest { 45 | int32 id = 1; 46 | google.protobuf.StringValue name = 2; 47 | google.protobuf.StringValue email = 3; 48 | } 49 | 50 | Here is the client usage:: 51 | 52 | from google.protobuf.wrappers_pb2 import StringValue 53 | 54 | 55 | with grpc.insecure_channel('localhost:50051') as channel: 56 | stub = hrm_pb2_grpc.PersonControllerStub(channel) 57 | request = hrm_pb2.PersonPartialUpdateRequest(id=1, name=StringValue(value="amy")) 58 | response = stub.PartialUpdate(request) 59 | print(response, end='') 60 | 61 | The service implementation:: 62 | 63 | class PersonService(generics.GenericService): 64 | queryset = Person.objects.all() 65 | serializer_class = PersonProtoSerializer 66 | 67 | def PartialUpdate(self, request, context): 68 | instance = self.get_object() 69 | serializer = self.get_serializer(instance, message=request, partial=True) 70 | serializer.is_valid(raise_exception=True) 71 | serializer.save() 72 | return serializer.message 73 | 74 | Or you can just use ``PartialUpdateModelMixin`` to get the same behavior:: 75 | 76 | class PersonService(mixins.PartialUpdateModelMixin, 77 | generics.GenericService): 78 | queryset = Person.objects.all() 79 | serializer_class = PersonProtoSerializer 80 | 81 | 82 | Repeated and map field absence 83 | ------------------------------ 84 | 85 | If you need to check whether repeated fields and map fields are set or not, 86 | you need to do it manually: 87 | 88 | .. code-block:: protobuf 89 | 90 | message PersonPartialUpdateRequest { 91 | int32 id = 1; 92 | google.protobuf.StringValue name = 2; 93 | google.protobuf.StringValue email = 3; 94 | repeated int32 groups = 4; 95 | bool is_groups_set = 5; 96 | } -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django gRPC Framework 2 | ===================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/djangogrpcframework.svg 5 | :target: https://img.shields.io/pypi/v/djangogrpcframework.svg 6 | 7 | .. image:: https://readthedocs.org/projects/djangogrpcframework/badge/?version=latest 8 | :target: https://readthedocs.org/projects/djangogrpcframework/badge/?version=latest 9 | 10 | .. image:: https://travis-ci.org/fengsp/django-grpc-framework.svg?branch=master 11 | :target: https://travis-ci.org/fengsp/django-grpc-framework.svg?branch=master 12 | 13 | .. image:: https://img.shields.io/pypi/pyversions/djangogrpcframework 14 | :target: https://img.shields.io/pypi/pyversions/djangogrpcframework 15 | 16 | .. image:: https://img.shields.io/pypi/l/djangogrpcframework 17 | :target: https://img.shields.io/pypi/l/djangogrpcframework 18 | 19 | Django gRPC framework is a toolkit for building gRPC services, inspired by 20 | djangorestframework. 21 | 22 | 23 | Requirements 24 | ------------ 25 | 26 | - Python (3.6, 3.7, 3.8) 27 | - Django (2.2, 3.0), Django REST Framework (3.10.x, 3.11.x) 28 | - gRPC, gRPC tools, proto3 29 | 30 | 31 | Installation 32 | ------------ 33 | 34 | .. code-block:: bash 35 | 36 | $ pip install djangogrpcframework 37 | 38 | Add ``django_grpc_framework`` to ``INSTALLED_APPS`` setting: 39 | 40 | .. code-block:: python 41 | 42 | INSTALLED_APPS = [ 43 | ... 44 | 'django_grpc_framework', 45 | ] 46 | 47 | 48 | Demo 49 | ---- 50 | 51 | Here is a quick example of using gRPC framework to build a simple 52 | model-backed service for accessing users, startup a new project: 53 | 54 | .. code-block:: bash 55 | 56 | $ django-admin startproject demo 57 | $ python manage.py migrate 58 | 59 | Generate ``.proto`` file demo.proto_: 60 | 61 | .. _demo.proto: https://github.com/fengsp/django-grpc-framework/blob/master/examples/demo/demo.proto 62 | 63 | .. code-block:: bash 64 | 65 | python manage.py generateproto --model django.contrib.auth.models.User --fields id,username,email --file demo.proto 66 | 67 | Generate gRPC code: 68 | 69 | .. code-block:: bash 70 | 71 | python -m grpc_tools.protoc --proto_path=./ --python_out=./ --grpc_python_out=./ ./demo.proto 72 | 73 | Now edit the ``demo/urls.py`` module: 74 | 75 | .. code-block:: python 76 | 77 | from django.contrib.auth.models import User 78 | from django_grpc_framework import generics, proto_serializers 79 | import demo_pb2 80 | import demo_pb2_grpc 81 | 82 | 83 | class UserProtoSerializer(proto_serializers.ModelProtoSerializer): 84 | class Meta: 85 | model = User 86 | proto_class = demo_pb2.User 87 | fields = ['id', 'username', 'email'] 88 | 89 | 90 | class UserService(generics.ModelService): 91 | queryset = User.objects.all() 92 | serializer_class = UserProtoSerializer 93 | 94 | 95 | urlpatterns = [] 96 | def grpc_handlers(server): 97 | demo_pb2_grpc.add_UserControllerServicer_to_server(UserService.as_servicer(), server) 98 | 99 | That's it, we're done! 100 | 101 | .. code-block:: bash 102 | 103 | $ python manage.py grpcrunserver --dev 104 | 105 | You can now run a gRPC client to access the service: 106 | 107 | .. code-block:: python 108 | 109 | with grpc.insecure_channel('localhost:50051') as channel: 110 | stub = demo_pb2_grpc.UserControllerStub(channel) 111 | for user in stub.List(demo_pb2.UserListRequest()): 112 | print(user, end='') 113 | -------------------------------------------------------------------------------- /docs/proto_serializers.rst: -------------------------------------------------------------------------------- 1 | .. _proto_serializers: 2 | 3 | Proto Serializers 4 | ================= 5 | 6 | The serializers work almost exactly the same with REST framework's ``Serializer`` 7 | class and ``ModelSerializer``, but use ``message`` instead of ``data`` as 8 | input and output. 9 | 10 | 11 | Declaring serializers 12 | --------------------- 13 | 14 | Declaring a serializer looks very similar to declaring a rest framework 15 | serializer:: 16 | 17 | from rest_framework import serializers 18 | from django_grpc_framework import proto_serializers 19 | 20 | class PersonProtoSerializer(proto_serializers.ProtoSerializer): 21 | name = serializers.CharField(max_length=100) 22 | email = serializers.EmailField(max_length=100) 23 | 24 | class Meta: 25 | proto_class = hrm_pb2.Person 26 | 27 | 28 | Overriding serialization and deserialization behavior 29 | ----------------------------------------------------- 30 | 31 | A proto serializer is the same as one rest framework serializer, but we are 32 | adding the following logic: 33 | 34 | - Protobuf message -> Dict of python primitive datatypes. 35 | - Protobuf message <- Dict of python primitive datatypes. 36 | 37 | If you need to alter the convert behavior of a serializer class, you can do so 38 | by overriding the ``.message_to_data()`` or ``.data_to_message`` methods. 39 | 40 | Here is the default implementation:: 41 | 42 | from google.protobuf.json_format import MessageToDict, ParseDict 43 | 44 | class ProtoSerializer(BaseProtoSerializer, Serializer): 45 | def message_to_data(self, message): 46 | """Protobuf message -> Dict of python primitive datatypes. 47 | """ 48 | return MessageToDict( 49 | message, including_default_value_fields=True, 50 | preserving_proto_field_name=True 51 | ) 52 | 53 | def data_to_message(self, data): 54 | """Protobuf message <- Dict of python primitive datatypes.""" 55 | return ParseDict( 56 | data, self.Meta.proto_class(), 57 | ignore_unknown_fields=True 58 | ) 59 | 60 | The default behavior requires you to provide ``ProtoSerializer.Meta.proto_class``, 61 | it is the protobuf class that should be used for create output proto message 62 | object. You must either set this attribute, or override the 63 | ``data_to_message()`` method. 64 | 65 | 66 | Serializing objects 67 | ------------------- 68 | 69 | We can now use ``PersonProtoSerializer`` to serialize a person object:: 70 | 71 | >>> serializer = PersonProtoSerializer(person) 72 | >>> serializer.message 73 | name: "amy" 74 | email: "amy@demo.com" 75 | >>> type(serializer.message) 76 | 77 | 78 | 79 | Deserializing objects 80 | --------------------- 81 | 82 | Deserialization is similar:: 83 | 84 | >>> serializer = PersonProtoSerializer(message=message) 85 | >>> serializer.is_valid() 86 | True 87 | >>> serializer.validated_data 88 | OrderedDict([('name', 'amy'), ('email', 'amy@demo.com')]) 89 | 90 | 91 | ModelProtoSerializer 92 | -------------------- 93 | 94 | This is the same as a rest framework ``ModelSerializer``:: 95 | 96 | from django_grpc_framework import proto_serializers 97 | from hrm.models import Person 98 | import hrm_pb2 99 | 100 | 101 | class PersonProtoSerializer(proto_serializers.ModelProtoSerializer): 102 | class Meta: 103 | model = Person 104 | proto_class = hrm_pb2.Person 105 | fields = '__all__' -------------------------------------------------------------------------------- /examples/demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'x74$)-q*_*x9wobbyrhgdh_9o@jmg_c#i@7p(y37j1sz(tlo+y' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_grpc_framework', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'demo.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'demo.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | -------------------------------------------------------------------------------- /examples/quickstart/quickstart/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for quickstart project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'q!d15i8ms-arkm&g2f!9ib30l8n#2k1$lf6=xk0smgp2y^02@z' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_grpc_framework', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'quickstart.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'quickstart.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | -------------------------------------------------------------------------------- /examples/tutorial/tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'fmypsab!0i8e(*!t1x03kkxgnzo7e&p1%d=ws+o1d@@h_)zps2' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_grpc_framework', 41 | 'blog', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'tutorial.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'tutorial.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | -------------------------------------------------------------------------------- /examples/null_support/null_support/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for null_support project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'jaf2czbo&pa@sxssvg6%ecjb1-)t1!f7dc3td1jl532c&wqw60' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_grpc_framework', 41 | 'snippets.apps.SnippetsConfig', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'null_support.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'null_support.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | -------------------------------------------------------------------------------- /examples/partial_update/partial_update/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for partial_update project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'r5g7)bb39!xaw^vyua&!prxln5dqook2z=)m2b5c4=39ggt!ap' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_grpc_framework', 41 | 'hrm.apps.HrmConfig', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'partial_update.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'partial_update.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | -------------------------------------------------------------------------------- /django_grpc_framework/proto_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ( 2 | BaseSerializer, Serializer, ListSerializer, ModelSerializer, 3 | LIST_SERIALIZER_KWARGS, 4 | ) 5 | from rest_framework.settings import api_settings 6 | from rest_framework.exceptions import ValidationError 7 | from django_grpc_framework.protobuf.json_format import ( 8 | message_to_dict, parse_dict 9 | ) 10 | 11 | 12 | class BaseProtoSerializer(BaseSerializer): 13 | def __init__(self, *args, **kwargs): 14 | message = kwargs.pop('message', None) 15 | if message is not None: 16 | self.initial_message = message 17 | kwargs['data'] = self.message_to_data(message) 18 | super().__init__(*args, **kwargs) 19 | 20 | def message_to_data(self, message): 21 | """Protobuf message -> Dict of python primitive datatypes.""" 22 | raise NotImplementedError('`message_to_data()` must be implemented.') 23 | 24 | def data_to_message(self, data): 25 | """Protobuf message <- Dict of python primitive datatypes.""" 26 | raise NotImplementedError('`data_to_message()` must be implemented.') 27 | 28 | @property 29 | def message(self): 30 | if not hasattr(self, '_message'): 31 | self._message = self.data_to_message(self.data) 32 | return self._message 33 | 34 | @classmethod 35 | def many_init(cls, *args, **kwargs): 36 | allow_empty = kwargs.pop('allow_empty', None) 37 | child_serializer = cls(*args, **kwargs) 38 | list_kwargs = { 39 | 'child': child_serializer, 40 | } 41 | if allow_empty is not None: 42 | list_kwargs['allow_empty'] = allow_empty 43 | list_kwargs.update({ 44 | key: value for key, value in kwargs.items() 45 | if key in LIST_SERIALIZER_KWARGS 46 | }) 47 | meta = getattr(cls, 'Meta', None) 48 | list_serializer_class = getattr(meta, 'list_serializer_class', ListProtoSerializer) 49 | return list_serializer_class(*args, **list_kwargs) 50 | 51 | 52 | class ProtoSerializer(BaseProtoSerializer, Serializer): 53 | def message_to_data(self, message): 54 | """Protobuf message -> Dict of python primitive datatypes. 55 | """ 56 | return message_to_dict(message) 57 | 58 | def data_to_message(self, data): 59 | """Protobuf message <- Dict of python primitive datatypes.""" 60 | assert hasattr(self, 'Meta'), ( 61 | 'Class {serializer_class} missing "Meta" attribute'.format( 62 | serializer_class=self.__class__.__name__ 63 | ) 64 | ) 65 | assert hasattr(self.Meta, 'proto_class'), ( 66 | 'Class {serializer_class} missing "Meta.proto_class" attribute'.format( 67 | serializer_class=self.__class__.__name__ 68 | ) 69 | ) 70 | return parse_dict(data, self.Meta.proto_class()) 71 | 72 | 73 | class ListProtoSerializer(BaseProtoSerializer, ListSerializer): 74 | def message_to_data(self, message): 75 | """ 76 | List of protobuf messages -> List of dicts of python primitive datatypes. 77 | """ 78 | if not isinstance(message, list): 79 | error_message = self.error_messages['not_a_list'].format( 80 | input_type=type(message).__name__ 81 | ) 82 | raise ValidationError({ 83 | api_settings.NON_FIELD_ERRORS_KEY: [error_message] 84 | }, code='not_a_list') 85 | ret = [] 86 | for item in message: 87 | ret.append(self.child.message_to_data(item)) 88 | return ret 89 | 90 | def data_to_message(self, data): 91 | """ 92 | List of protobuf messages <- List of dicts of python primitive datatypes. 93 | """ 94 | return [ 95 | self.child.data_to_message(item) for item in data 96 | ] 97 | 98 | 99 | class ModelProtoSerializer(ProtoSerializer, ModelSerializer): 100 | pass -------------------------------------------------------------------------------- /docs/generics.rst: -------------------------------------------------------------------------------- 1 | .. _generics: 2 | 3 | Generic services 4 | ================ 5 | 6 | The generic services provided by gRPC framework allow you to quickly build 7 | gRPC services that map closely to your database models. If the generic services 8 | don't suit your needs, use the regular ``Service`` class, or reuse the mixins 9 | and base classes used by the generic services to compose your own set of 10 | ressable generic services. 11 | 12 | For example:: 13 | 14 | from blog.models import Post 15 | from blog.serializers import PostProtoSerializer 16 | from django_grpc_framework import generics 17 | 18 | 19 | class PostService(generics.ModelService): 20 | queryset = Post.objects.all() 21 | serializer_class = PostProtoSerializer 22 | 23 | 24 | GenericService 25 | -------------- 26 | 27 | This class extends ``Service`` class, adding commonly required behavior for 28 | standard list and detail services. All concrete generic services is built by 29 | composing ``GenericService``, with one or more mixin classes. 30 | 31 | Attributes 32 | `````````` 33 | 34 | **Basic settings:** 35 | 36 | The following attributes control the basic service behavior: 37 | 38 | - ``queryset`` - The queryset that should be used for returning objects from this 39 | service. You must set this or override the ``get_queryset`` method, you should 40 | call ``get_queryset`` instead of accessing this property directly, as ``queryset`` 41 | will get evaluated once, and those results will be cached for all subsequent 42 | requests. 43 | - ``serializer_class`` - The serializer class that should be used for validating 44 | and deserializing input, and for serializing output. You must either set this 45 | attribute, or override the ``get_serializer_class()`` method. 46 | - ``lookup_field`` - The model field that should be used to for performing object 47 | lookup of individual model instances. Defaults to primary key field name. 48 | - ``lookup_request_field`` - The request field that should be used for object 49 | lookup. If unset this defaults to using the same value as ``lookup_field``. 50 | 51 | Methods 52 | ``````` 53 | 54 | .. currentmodule:: django_grpc_framework.generics 55 | 56 | .. autoclass:: GenericService 57 | :members: 58 | 59 | 60 | Mixins 61 | ------ 62 | 63 | The mixin classes provide the actions that are used to privide the basic 64 | service behavior. The mixin classes can be imported from 65 | ``django_grpc_framework.mixins``. 66 | 67 | .. currentmodule:: django_grpc_framework.mixins 68 | 69 | .. autoclass:: ListModelMixin 70 | :members: 71 | 72 | .. autoclass:: CreateModelMixin 73 | :members: 74 | 75 | .. autoclass:: RetrieveModelMixin 76 | :members: 77 | 78 | .. autoclass:: UpdateModelMixin 79 | :members: 80 | 81 | .. autoclass:: DestroyModelMixin 82 | :members: 83 | 84 | 85 | Concrete service classes 86 | ------------------------ 87 | 88 | The following classes are the concrete generic services. They can be imported 89 | from ``django_grpc_framework.generics``. 90 | 91 | .. currentmodule:: django_grpc_framework.generics 92 | 93 | .. autoclass:: CreateService 94 | :members: 95 | 96 | .. autoclass:: ListService 97 | :members: 98 | 99 | .. autoclass:: RetrieveService 100 | :members: 101 | 102 | .. autoclass:: DestroyService 103 | :members: 104 | 105 | .. autoclass:: UpdateService 106 | :members: 107 | 108 | .. autoclass:: ReadOnlyModelService 109 | :members: 110 | 111 | .. autoclass:: ModelService 112 | :members: 113 | 114 | You may need to provide custom classes that have certain actions, to create 115 | a base class that provides ``List()`` and ``Create()`` handlers, inherit from 116 | ``GenericService`` and mixin the required handlers:: 117 | 118 | from django_grpc_framework import mixins 119 | from django_grpc_framework import generics 120 | 121 | class ListCreateService(mixins.CreateModelMixin, 122 | mixins.ListModelMixin, 123 | GenericService): 124 | """ 125 | Concrete service that provides ``Create()`` and ``List()`` handlers. 126 | """ 127 | pass 128 | -------------------------------------------------------------------------------- /django_grpc_framework/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings for gRPC framework are all namespaced in the GRPC_FRAMEWORK setting. 3 | For example your project's `settings.py` file might look like this: 4 | 5 | GRPC_FRAMEWORK = { 6 | 'ROOT_HANDLERS_HOOK': 'path.to.my.custom_grpc_handlers', 7 | } 8 | 9 | This module provides the `grpc_setting` object, that is used to access 10 | gRPC framework settings, checking for user settings first, then falling 11 | back to the defaults. 12 | """ 13 | from django.conf import settings 14 | from django.test.signals import setting_changed 15 | from django.utils.module_loading import import_string 16 | 17 | 18 | DEFAULTS = { 19 | # Root grpc handlers hook configuration 20 | 'ROOT_HANDLERS_HOOK': None, 21 | 22 | # gRPC server configuration 23 | 'SERVER_INTERCEPTORS': None, 24 | } 25 | 26 | 27 | # List of settings that may be in string import notation. 28 | IMPORT_STRINGS = [ 29 | 'ROOT_HANDLERS_HOOK', 30 | 'SERVER_INTERCEPTORS', 31 | ] 32 | 33 | 34 | def perform_import(val, setting_name): 35 | """ 36 | If the given setting is a string import notation, 37 | then perform the necessary import or imports. 38 | """ 39 | if val is None: 40 | # We need the ROOT_URLCONF so we do this lazily 41 | if setting_name == 'ROOT_HANDLERS_HOOK': 42 | return import_from_string( 43 | '%s.grpc_handlers' % settings.ROOT_URLCONF, 44 | setting_name, 45 | ) 46 | return None 47 | elif isinstance(val, str): 48 | return import_from_string(val, setting_name) 49 | elif isinstance(val, (list, tuple)): 50 | return [import_from_string(item, setting_name) for item in val] 51 | return val 52 | 53 | 54 | def import_from_string(val, setting_name): 55 | """ 56 | Attempt to import a class from a string representation. 57 | """ 58 | try: 59 | return import_string(val) 60 | except ImportError as e: 61 | raise ImportError( 62 | "Could not import '%s' for GRPC setting '%s'. %s: %s." % 63 | (val, setting_name, e.__class__.__name__, e) 64 | ) 65 | 66 | 67 | class GRPCSettings: 68 | """ 69 | A settings object that allows gRPC Framework settings to be accessed as 70 | properties. For example: 71 | 72 | from django_grpc_framework.settings import grpc_settings 73 | print(grpc_settings.ROOT_HANDLERS_HOOK) 74 | 75 | Any setting with string import paths will be automatically resolved 76 | and return the class, rather than the string literal. 77 | """ 78 | def __init__(self, user_settings=None, defaults=None, import_strings=None): 79 | if user_settings: 80 | self._user_settings = user_settings 81 | self.defaults = defaults or DEFAULTS 82 | self.import_strings = import_strings or IMPORT_STRINGS 83 | self._cached_attrs = set() 84 | 85 | @property 86 | def user_settings(self): 87 | if not hasattr(self, '_user_settings'): 88 | self._user_settings = getattr(settings, 'GRPC_FRAMEWORK', {}) 89 | return self._user_settings 90 | 91 | def __getattr__(self, attr): 92 | if attr not in self.defaults: 93 | raise AttributeError("Invalid gRPC setting: '%s'" % attr) 94 | 95 | try: 96 | # Check if present in user settings 97 | val = self.user_settings[attr] 98 | except KeyError: 99 | # Fall back to defaults 100 | val = self.defaults[attr] 101 | 102 | # Coerce import strings into classes 103 | if attr in self.import_strings: 104 | val = perform_import(val, attr) 105 | 106 | # Cache the result 107 | self._cached_attrs.add(attr) 108 | setattr(self, attr, val) 109 | return val 110 | 111 | def reload(self): 112 | for attr in self._cached_attrs: 113 | delattr(self, attr) 114 | self._cached_attrs.clear() 115 | if hasattr(self, '_user_settings'): 116 | delattr(self, '_user_settings') 117 | 118 | 119 | grpc_settings = GRPCSettings(None, DEFAULTS, IMPORT_STRINGS) 120 | 121 | 122 | def reload_grpc_settings(*args, **kwargs): 123 | setting = kwargs['setting'] 124 | if setting == 'GRPC_FRAMEWORK' or setting == 'ROOT_URLCONF': 125 | grpc_settings.reload() 126 | 127 | 128 | setting_changed.connect(reload_grpc_settings) 129 | -------------------------------------------------------------------------------- /django_grpc_framework/management/commands/grpcrunserver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from concurrent import futures 3 | from datetime import datetime 4 | import sys 5 | import errno 6 | import os 7 | 8 | import grpc 9 | from django.utils import autoreload 10 | from django.conf import settings 11 | from django.core.management.base import BaseCommand 12 | 13 | from django_grpc_framework.settings import grpc_settings 14 | 15 | 16 | class Command(BaseCommand): 17 | help = 'Starts a gRPC server.' 18 | 19 | # Validation is called explicitly each time the server is reloaded. 20 | requires_system_checks = False 21 | 22 | def add_arguments(self, parser): 23 | parser.add_argument( 24 | 'address', nargs='?', default='[::]:50051', 25 | help='Optional address for which to open a port.' 26 | ) 27 | parser.add_argument( 28 | '--max-workers', type=int, default=10, dest='max_workers', 29 | help='Number of maximum worker threads.' 30 | ) 31 | parser.add_argument( 32 | '--dev', action='store_true', dest='development_mode', 33 | help=( 34 | 'Run the server in development mode. This tells Django to use ' 35 | 'the auto-reloader and run checks.' 36 | ) 37 | ) 38 | 39 | def handle(self, *args, **options): 40 | self.address = options['address'] 41 | self.development_mode = options['development_mode'] 42 | self.max_workers = options['max_workers'] 43 | self.run(**options) 44 | 45 | def run(self, **options): 46 | """Run the server, using the autoreloader if needed.""" 47 | if self.development_mode: 48 | if hasattr(autoreload, "run_with_reloader"): 49 | autoreload.run_with_reloader(self.inner_run, **options) 50 | else: 51 | autoreload.main(self.inner_run, None, options) 52 | else: 53 | self.stdout.write(( 54 | "Starting gRPC server at %(address)s\n" 55 | ) % { 56 | "address": self.address, 57 | }) 58 | self._serve() 59 | 60 | def _serve(self): 61 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=self.max_workers), 62 | interceptors=grpc_settings.SERVER_INTERCEPTORS) 63 | grpc_settings.ROOT_HANDLERS_HOOK(server) 64 | server.add_insecure_port(self.address) 65 | server.start() 66 | server.wait_for_termination() 67 | 68 | def inner_run(self, *args, **options): 69 | # If an exception was silenced in ManagementUtility.execute in order 70 | # to be raised in the child process, raise it now. 71 | autoreload.raise_last_exception() 72 | 73 | self.stdout.write("Performing system checks...\n\n") 74 | self.check(display_num_errors=True) 75 | # Need to check migrations here, so can't use the 76 | # requires_migrations_check attribute. 77 | self.check_migrations() 78 | now = datetime.now().strftime('%B %d, %Y - %X') 79 | self.stdout.write(now) 80 | quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C' 81 | self.stdout.write(( 82 | "Django version %(version)s, using settings %(settings)r\n" 83 | "Starting development gRPC server at %(address)s\n" 84 | "Quit the server with %(quit_command)s.\n" 85 | ) % { 86 | "version": self.get_version(), 87 | "settings": settings.SETTINGS_MODULE, 88 | "address": self.address, 89 | "quit_command": quit_command, 90 | }) 91 | try: 92 | self._serve() 93 | except OSError as e: 94 | # Use helpful error messages instead of ugly tracebacks. 95 | ERRORS = { 96 | errno.EACCES: "You don't have permission to access that port.", 97 | errno.EADDRINUSE: "That port is already in use.", 98 | errno.EADDRNOTAVAIL: "That IP address can't be assigned to.", 99 | } 100 | try: 101 | error_text = ERRORS[e.errno] 102 | except KeyError: 103 | error_text = e 104 | self.stderr.write("Error: %s" % error_text) 105 | # Need to use an OS exit because sys.exit doesn't work in a thread 106 | os._exit(1) 107 | except KeyboardInterrupt: 108 | sys.exit(0) 109 | -------------------------------------------------------------------------------- /django_grpc_framework/mixins.py: -------------------------------------------------------------------------------- 1 | from google.protobuf import empty_pb2 2 | 3 | 4 | class CreateModelMixin: 5 | def Create(self, request, context): 6 | """ 7 | Create a model instance. 8 | 9 | The request should be a proto message of ``serializer.Meta.proto_class``. 10 | If an object is created this returns a proto message of 11 | ``serializer.Meta.proto_class``. 12 | """ 13 | serializer = self.get_serializer(message=request) 14 | serializer.is_valid(raise_exception=True) 15 | self.perform_create(serializer) 16 | return serializer.message 17 | 18 | def perform_create(self, serializer): 19 | """Save a new object instance.""" 20 | serializer.save() 21 | 22 | 23 | class ListModelMixin: 24 | def List(self, request, context): 25 | """ 26 | List a queryset. This sends a sequence of messages of 27 | ``serializer.Meta.proto_class`` to the client. 28 | 29 | .. note:: 30 | 31 | This is a server streaming RPC. 32 | """ 33 | queryset = self.filter_queryset(self.get_queryset()) 34 | serializer = self.get_serializer(queryset, many=True) 35 | for message in serializer.message: 36 | yield message 37 | 38 | 39 | class RetrieveModelMixin: 40 | def Retrieve(self, request, context): 41 | """ 42 | Retrieve a model instance. 43 | 44 | The request have to include a field corresponding to 45 | ``lookup_request_field``. If an object can be retrieved this returns 46 | a proto message of ``serializer.Meta.proto_class``. 47 | """ 48 | instance = self.get_object() 49 | serializer = self.get_serializer(instance) 50 | return serializer.message 51 | 52 | 53 | class UpdateModelMixin: 54 | def Update(self, request, context): 55 | """ 56 | Update a model instance. 57 | 58 | The request should be a proto message of ``serializer.Meta.proto_class``. 59 | If an object is updated this returns a proto message of 60 | ``serializer.Meta.proto_class``. 61 | """ 62 | instance = self.get_object() 63 | serializer = self.get_serializer(instance, message=request) 64 | serializer.is_valid(raise_exception=True) 65 | self.perform_update(serializer) 66 | 67 | if getattr(instance, '_prefetched_objects_cache', None): 68 | # If 'prefetch_related' has been applied to a queryset, we need to 69 | # forcibly invalidate the prefetch cache on the instance. 70 | instance._prefetched_objects_cache = {} 71 | 72 | return serializer.message 73 | 74 | def perform_update(self, serializer): 75 | """Save an existing object instance.""" 76 | serializer.save() 77 | 78 | 79 | class PartialUpdateModelMixin: 80 | def PartialUpdate(self, request, context): 81 | """ 82 | Partial update a model instance. 83 | 84 | The request have to include a field corresponding to 85 | ``lookup_request_field`` and you need to explicitly set the fields that 86 | you want to update. If an object is updated this returns a proto 87 | message of ``serializer.Meta.proto_class``. 88 | """ 89 | instance = self.get_object() 90 | serializer = self.get_serializer(instance, message=request, partial=True) 91 | serializer.is_valid(raise_exception=True) 92 | self.perform_partial_update(serializer) 93 | 94 | if getattr(instance, '_prefetched_objects_cache', None): 95 | # If 'prefetch_related' has been applied to a queryset, we need to 96 | # forcibly invalidate the prefetch cache on the instance. 97 | instance._prefetched_objects_cache = {} 98 | 99 | return serializer.message 100 | 101 | def perform_partial_update(self, serializer): 102 | """Save an existing object instance.""" 103 | serializer.save() 104 | 105 | 106 | class DestroyModelMixin: 107 | def Destroy(self, request, context): 108 | """ 109 | Destroy a model instance. 110 | 111 | The request have to include a field corresponding to 112 | ``lookup_request_field``. If an object is deleted this returns 113 | a proto message of ``google.protobuf.empty_pb2.Empty``. 114 | """ 115 | instance = self.get_object() 116 | self.perform_destroy(instance) 117 | return empty_pb2.Empty() 118 | 119 | def perform_destroy(self, instance): 120 | """Delete an object instance.""" 121 | instance.delete() 122 | -------------------------------------------------------------------------------- /django_grpc_framework/test.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from django.test import testcases 4 | import grpc 5 | from django.db import close_old_connections 6 | 7 | from django_grpc_framework.settings import grpc_settings 8 | from django_grpc_framework.signals import grpc_request_started, grpc_request_finished 9 | 10 | 11 | @contextmanager 12 | def _disable_close_old_connections(): 13 | try: 14 | grpc_request_started.disconnect(close_old_connections) 15 | grpc_request_finished.disconnect(close_old_connections) 16 | yield 17 | finally: 18 | grpc_request_started.connect(close_old_connections) 19 | grpc_request_finished.connect(close_old_connections) 20 | 21 | 22 | class Channel: 23 | def __init__(self): 24 | server = FakeServer() 25 | grpc_settings.ROOT_HANDLERS_HOOK(server) 26 | self.server = server 27 | 28 | def __enter__(self): 29 | return self 30 | 31 | def __exit__(self, exc_tp, exc_val, exc_tb): 32 | pass 33 | 34 | def unary_unary(self, method, *args, **kwargs): 35 | return UnaryUnary(self, method) 36 | 37 | def unary_stream(self, method, *args, **kwargs): 38 | return UnaryStream(self, method) 39 | 40 | def stream_unary(self, method, *args, **kwargs): 41 | return StreamUnary(self, method) 42 | 43 | def stream_stream(self, method, *args, **kwargs): 44 | return StreamStream(self, method) 45 | 46 | 47 | class _MultiCallable: 48 | def __init__(self, channel, method_full_rpc_name): 49 | self._handler = channel.server._find_method_handler(method_full_rpc_name) 50 | 51 | def with_call(self, *args, **kwargs): 52 | raise NotImplementedError 53 | 54 | def future(self, *args, **kwargs): 55 | raise NotImplementedError 56 | 57 | 58 | class UnaryUnary(_MultiCallable, grpc.UnaryUnaryMultiCallable): 59 | def __call__(self, request, timeout=None, metadata=None, *args, **kwargs): 60 | with _disable_close_old_connections(): 61 | context = FakeContext() 62 | context._invocation_metadata.extend(metadata or []) 63 | return self._handler.unary_unary(request, context) 64 | 65 | 66 | class UnaryStream(_MultiCallable, grpc.UnaryStreamMultiCallable): 67 | def __call__(self, request, timeout=None, metadata=None, *args, **kwargs): 68 | with _disable_close_old_connections(): 69 | context = FakeContext() 70 | context._invocation_metadata.extend(metadata or []) 71 | return self._handler.unary_stream(request, context) 72 | 73 | 74 | class StreamUnary(_MultiCallable, grpc.StreamUnaryMultiCallable): 75 | def __call__(self, request_iterator, timeout=None, metadata=None, *args, **kwargs): 76 | with _disable_close_old_connections(): 77 | context = FakeContext() 78 | context._invocation_metadata.extend(metadata or []) 79 | return self._handler.stream_unary(request_iterator, context) 80 | 81 | 82 | class StreamStream(_MultiCallable, grpc.StreamStreamMultiCallable): 83 | def __call__(self, request_iterator, timeout=None, metadata=None, *args, **kwargs): 84 | with _disable_close_old_connections(): 85 | context = FakeContext() 86 | context._invocation_metadata.extend(metadata or []) 87 | return self._handler.stream_stream(request_iterator, context) 88 | 89 | 90 | class FakeRpcError(grpc.RpcError): 91 | def __init__(self, code, details): 92 | self._code = code 93 | self._details = details 94 | 95 | def code(self): 96 | return self._code 97 | 98 | def details(self): 99 | return self._details 100 | 101 | def __repr__(self): 102 | return '' % (self._code, self._details) 103 | 104 | 105 | class FakeServer: 106 | def __init__(self): 107 | self.rpc_method_handlers = {} 108 | 109 | def add_generic_rpc_handlers(self, generic_rpc_handlers): 110 | from grpc._server import _validate_generic_rpc_handlers 111 | _validate_generic_rpc_handlers(generic_rpc_handlers) 112 | self.rpc_method_handlers.update(generic_rpc_handlers[0]._method_handlers) 113 | 114 | def _find_method_handler(self, method_full_rpc_name): 115 | return self.rpc_method_handlers[method_full_rpc_name] 116 | 117 | 118 | class FakeContext: 119 | def __init__(self): 120 | self._invocation_metadata = [] 121 | 122 | def abort(self, code, details): 123 | raise FakeRpcError(code, details) 124 | 125 | def invocation_metadata(self): 126 | return self._invocation_metadata 127 | 128 | 129 | class RPCSimpleTestCase(testcases.SimpleTestCase): 130 | channel_class = Channel 131 | 132 | def setUp(self): 133 | super().setUp() 134 | self.channel = self.channel_class() 135 | 136 | 137 | class RPCTransactionTestCase(testcases.TransactionTestCase): 138 | channel_class = Channel 139 | 140 | def setUp(self): 141 | super().setUp() 142 | self.channel = self.channel_class() 143 | 144 | 145 | class RPCTestCase(testcases.TestCase): 146 | channel_class = Channel 147 | 148 | def setUp(self): 149 | super().setUp() 150 | self.channel = self.channel_class() 151 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quickstart 4 | ========== 5 | 6 | We're going to create a simple service to allow clients to retrieve and edit the 7 | users in the system. 8 | 9 | 10 | Project setup 11 | ------------- 12 | 13 | Create a new Django project named ``quickstart``, then start a new app called 14 | ``account``:: 15 | 16 | # Create a virtual environment 17 | python3 -m venv env 18 | source env/bin/activate 19 | # Install Django and Django gRPC framework 20 | pip install django 21 | pip install djangorestframework 22 | pip install djangogrpcframework 23 | pip install grpcio 24 | pip install grpcio-tools 25 | # Create a new project and a new application 26 | django-admin startproject quickstart 27 | cd quickstart 28 | django-admin startapp account 29 | 30 | Now sync the database:: 31 | 32 | python manage.py migrate 33 | 34 | 35 | Update settings 36 | --------------- 37 | 38 | Add ``django_grpc_framework`` to ``INSTALLED_APPS``, settings module is in 39 | ``quickstart/settings.py``:: 40 | 41 | INSTALLED_APPS = [ 42 | ... 43 | 'django_grpc_framework', 44 | ] 45 | 46 | 47 | Defining protos 48 | --------------- 49 | 50 | Our first step is to define the gRPC service and messages, create a file 51 | ``quickstart/account.proto`` next to ``quickstart/manage.py``: 52 | 53 | .. code-block:: protobuf 54 | 55 | syntax = "proto3"; 56 | 57 | package account; 58 | 59 | import "google/protobuf/empty.proto"; 60 | 61 | service UserController { 62 | rpc List(UserListRequest) returns (stream User) {} 63 | rpc Create(User) returns (User) {} 64 | rpc Retrieve(UserRetrieveRequest) returns (User) {} 65 | rpc Update(User) returns (User) {} 66 | rpc Destroy(User) returns (google.protobuf.Empty) {} 67 | } 68 | 69 | message User { 70 | int32 id = 1; 71 | string username = 2; 72 | string email = 3; 73 | repeated int32 groups = 4; 74 | } 75 | 76 | message UserListRequest { 77 | } 78 | 79 | message UserRetrieveRequest { 80 | int32 id = 1; 81 | } 82 | 83 | Or you can generate it automatically based on ``User`` model:: 84 | 85 | python manage.py generateproto --model django.contrib.auth.models.User --fields id,username,email,groups --file account.proto 86 | 87 | Next we need to generate gRPC code, from the ``quickstart`` directory, run:: 88 | 89 | python -m grpc_tools.protoc --proto_path=./ --python_out=./ --grpc_python_out=./ ./account.proto 90 | 91 | 92 | Writing serializers 93 | ------------------- 94 | 95 | Then we're going to define a serializer, let's create a new module named 96 | ``account/serializers.py``:: 97 | 98 | from django.contrib.auth.models import User 99 | from django_grpc_framework import proto_serializers 100 | import account_pb2 101 | 102 | 103 | class UserProtoSerializer(proto_serializers.ModelProtoSerializer): 104 | class Meta: 105 | model = User 106 | proto_class = account_pb2.User 107 | fields = ['id', 'username', 'email', 'groups'] 108 | 109 | 110 | Writing services 111 | ---------------- 112 | 113 | Now we'd write some a service, create ``account/services.py``:: 114 | 115 | from django.contrib.auth.models import User 116 | from django_grpc_framework import generics 117 | from account.serializers import UserProtoSerializer 118 | 119 | 120 | class UserService(generics.ModelService): 121 | """ 122 | gRPC service that allows users to be retrieved or updated. 123 | """ 124 | queryset = User.objects.all().order_by('-date_joined') 125 | serializer_class = UserProtoSerializer 126 | 127 | 128 | Register handlers 129 | ----------------- 130 | 131 | Ok, let's wire up the gRPC handlers, edit ``quickstart/urls.py``:: 132 | 133 | import account_pb2_grpc 134 | from account.services import UserService 135 | 136 | 137 | urlpatterns = [] 138 | 139 | 140 | def grpc_handlers(server): 141 | account_pb2_grpc.add_UserControllerServicer_to_server(UserService.as_servicer(), server) 142 | 143 | We're done, the project layout should look like:: 144 | 145 | . 146 | ./quickstart 147 | ./quickstart/asgi.py 148 | ./quickstart/__init__.py 149 | ./quickstart/settings.py 150 | ./quickstart/urls.py 151 | ./quickstart/wsgi.py 152 | ./manage.py 153 | ./account 154 | ./account/migrations 155 | ./account/migrations/__init__.py 156 | ./account/services.py 157 | ./account/models.py 158 | ./account/serializers.py 159 | ./account/__init__.py 160 | ./account/apps.py 161 | ./account/admin.py 162 | ./account/tests.py 163 | ./account.proto 164 | ./account_pb2_grpc.py 165 | ./account_pb2.py 166 | 167 | 168 | Calling our service 169 | ------------------- 170 | 171 | Fire up the server with development mode:: 172 | 173 | python manage.py grpcrunserver --dev 174 | 175 | We can now access our service from the gRPC client:: 176 | 177 | import grpc 178 | import account_pb2 179 | import account_pb2_grpc 180 | 181 | 182 | with grpc.insecure_channel('localhost:50051') as channel: 183 | stub = account_pb2_grpc.UserControllerStub(channel) 184 | for user in stub.List(account_pb2.UserListRequest()): 185 | print(user, end='') 186 | -------------------------------------------------------------------------------- /examples/partial_update/hrm_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: hrm.proto 4 | 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='hrm.proto', 19 | package='hrm', 20 | syntax='proto3', 21 | serialized_options=None, 22 | serialized_pb=b'\n\thrm.proto\x12\x03hrm\x1a\x1egoogle/protobuf/wrappers.proto\"1\n\x06Person\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\"\x81\x01\n\x1aPersonPartialUpdateRequest\x12\n\n\x02id\x18\x01 \x01(\x05\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05\x65mail\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue2S\n\x10PersonController\x12?\n\rPartialUpdate\x12\x1f.hrm.PersonPartialUpdateRequest\x1a\x0b.hrm.Person\"\x00\x62\x06proto3' 23 | , 24 | dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,]) 25 | 26 | 27 | 28 | 29 | _PERSON = _descriptor.Descriptor( 30 | name='Person', 31 | full_name='hrm.Person', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='id', full_name='hrm.Person.id', index=0, 38 | number=1, type=5, cpp_type=1, label=1, 39 | has_default_value=False, default_value=0, 40 | message_type=None, enum_type=None, containing_type=None, 41 | is_extension=False, extension_scope=None, 42 | serialized_options=None, file=DESCRIPTOR), 43 | _descriptor.FieldDescriptor( 44 | name='name', full_name='hrm.Person.name', index=1, 45 | number=2, type=9, cpp_type=9, label=1, 46 | has_default_value=False, default_value=b"".decode('utf-8'), 47 | message_type=None, enum_type=None, containing_type=None, 48 | is_extension=False, extension_scope=None, 49 | serialized_options=None, file=DESCRIPTOR), 50 | _descriptor.FieldDescriptor( 51 | name='email', full_name='hrm.Person.email', index=2, 52 | number=3, type=9, cpp_type=9, label=1, 53 | has_default_value=False, default_value=b"".decode('utf-8'), 54 | message_type=None, enum_type=None, containing_type=None, 55 | is_extension=False, extension_scope=None, 56 | serialized_options=None, file=DESCRIPTOR), 57 | ], 58 | extensions=[ 59 | ], 60 | nested_types=[], 61 | enum_types=[ 62 | ], 63 | serialized_options=None, 64 | is_extendable=False, 65 | syntax='proto3', 66 | extension_ranges=[], 67 | oneofs=[ 68 | ], 69 | serialized_start=50, 70 | serialized_end=99, 71 | ) 72 | 73 | 74 | _PERSONPARTIALUPDATEREQUEST = _descriptor.Descriptor( 75 | name='PersonPartialUpdateRequest', 76 | full_name='hrm.PersonPartialUpdateRequest', 77 | filename=None, 78 | file=DESCRIPTOR, 79 | containing_type=None, 80 | fields=[ 81 | _descriptor.FieldDescriptor( 82 | name='id', full_name='hrm.PersonPartialUpdateRequest.id', index=0, 83 | number=1, type=5, cpp_type=1, label=1, 84 | has_default_value=False, default_value=0, 85 | message_type=None, enum_type=None, containing_type=None, 86 | is_extension=False, extension_scope=None, 87 | serialized_options=None, file=DESCRIPTOR), 88 | _descriptor.FieldDescriptor( 89 | name='name', full_name='hrm.PersonPartialUpdateRequest.name', index=1, 90 | number=2, type=11, cpp_type=10, label=1, 91 | has_default_value=False, default_value=None, 92 | message_type=None, enum_type=None, containing_type=None, 93 | is_extension=False, extension_scope=None, 94 | serialized_options=None, file=DESCRIPTOR), 95 | _descriptor.FieldDescriptor( 96 | name='email', full_name='hrm.PersonPartialUpdateRequest.email', index=2, 97 | number=3, type=11, cpp_type=10, label=1, 98 | has_default_value=False, default_value=None, 99 | message_type=None, enum_type=None, containing_type=None, 100 | is_extension=False, extension_scope=None, 101 | serialized_options=None, file=DESCRIPTOR), 102 | ], 103 | extensions=[ 104 | ], 105 | nested_types=[], 106 | enum_types=[ 107 | ], 108 | serialized_options=None, 109 | is_extendable=False, 110 | syntax='proto3', 111 | extension_ranges=[], 112 | oneofs=[ 113 | ], 114 | serialized_start=102, 115 | serialized_end=231, 116 | ) 117 | 118 | _PERSONPARTIALUPDATEREQUEST.fields_by_name['name'].message_type = google_dot_protobuf_dot_wrappers__pb2._STRINGVALUE 119 | _PERSONPARTIALUPDATEREQUEST.fields_by_name['email'].message_type = google_dot_protobuf_dot_wrappers__pb2._STRINGVALUE 120 | DESCRIPTOR.message_types_by_name['Person'] = _PERSON 121 | DESCRIPTOR.message_types_by_name['PersonPartialUpdateRequest'] = _PERSONPARTIALUPDATEREQUEST 122 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 123 | 124 | Person = _reflection.GeneratedProtocolMessageType('Person', (_message.Message,), { 125 | 'DESCRIPTOR' : _PERSON, 126 | '__module__' : 'hrm_pb2' 127 | # @@protoc_insertion_point(class_scope:hrm.Person) 128 | }) 129 | _sym_db.RegisterMessage(Person) 130 | 131 | PersonPartialUpdateRequest = _reflection.GeneratedProtocolMessageType('PersonPartialUpdateRequest', (_message.Message,), { 132 | 'DESCRIPTOR' : _PERSONPARTIALUPDATEREQUEST, 133 | '__module__' : 'hrm_pb2' 134 | # @@protoc_insertion_point(class_scope:hrm.PersonPartialUpdateRequest) 135 | }) 136 | _sym_db.RegisterMessage(PersonPartialUpdateRequest) 137 | 138 | 139 | 140 | _PERSONCONTROLLER = _descriptor.ServiceDescriptor( 141 | name='PersonController', 142 | full_name='hrm.PersonController', 143 | file=DESCRIPTOR, 144 | index=0, 145 | serialized_options=None, 146 | serialized_start=233, 147 | serialized_end=316, 148 | methods=[ 149 | _descriptor.MethodDescriptor( 150 | name='PartialUpdate', 151 | full_name='hrm.PersonController.PartialUpdate', 152 | index=0, 153 | containing_service=None, 154 | input_type=_PERSONPARTIALUPDATEREQUEST, 155 | output_type=_PERSON, 156 | serialized_options=None, 157 | ), 158 | ]) 159 | _sym_db.RegisterServiceDescriptor(_PERSONCONTROLLER) 160 | 161 | DESCRIPTOR.services_by_name['PersonController'] = _PERSONCONTROLLER 162 | 163 | # @@protoc_insertion_point(module_scope) 164 | -------------------------------------------------------------------------------- /examples/null_support/snippets_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: snippets.proto 4 | 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='snippets.proto', 19 | package='snippets', 20 | syntax='proto3', 21 | serialized_options=None, 22 | serialized_pb=b'\n\x0esnippets.proto\x12\x08snippets\x1a\x1cgoogle/protobuf/struct.proto\"U\n\x0eNullableString\x12\x0f\n\x05value\x18\x01 \x01(\tH\x00\x12*\n\x04null\x18\x02 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x42\x06\n\x04kind\"P\n\x07Snippet\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05title\x18\x02 \x01(\t\x12*\n\x08language\x18\x03 \x01(\x0b\x32\x18.snippets.NullableString2E\n\x11SnippetController\x12\x30\n\x06Update\x12\x11.snippets.Snippet\x1a\x11.snippets.Snippet\"\x00\x62\x06proto3' 23 | , 24 | dependencies=[google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,]) 25 | 26 | 27 | 28 | 29 | _NULLABLESTRING = _descriptor.Descriptor( 30 | name='NullableString', 31 | full_name='snippets.NullableString', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='value', full_name='snippets.NullableString.value', index=0, 38 | number=1, type=9, cpp_type=9, label=1, 39 | has_default_value=False, default_value=b"".decode('utf-8'), 40 | message_type=None, enum_type=None, containing_type=None, 41 | is_extension=False, extension_scope=None, 42 | serialized_options=None, file=DESCRIPTOR), 43 | _descriptor.FieldDescriptor( 44 | name='null', full_name='snippets.NullableString.null', index=1, 45 | number=2, type=14, cpp_type=8, label=1, 46 | has_default_value=False, default_value=0, 47 | message_type=None, enum_type=None, containing_type=None, 48 | is_extension=False, extension_scope=None, 49 | serialized_options=None, file=DESCRIPTOR), 50 | ], 51 | extensions=[ 52 | ], 53 | nested_types=[], 54 | enum_types=[ 55 | ], 56 | serialized_options=None, 57 | is_extendable=False, 58 | syntax='proto3', 59 | extension_ranges=[], 60 | oneofs=[ 61 | _descriptor.OneofDescriptor( 62 | name='kind', full_name='snippets.NullableString.kind', 63 | index=0, containing_type=None, fields=[]), 64 | ], 65 | serialized_start=58, 66 | serialized_end=143, 67 | ) 68 | 69 | 70 | _SNIPPET = _descriptor.Descriptor( 71 | name='Snippet', 72 | full_name='snippets.Snippet', 73 | filename=None, 74 | file=DESCRIPTOR, 75 | containing_type=None, 76 | fields=[ 77 | _descriptor.FieldDescriptor( 78 | name='id', full_name='snippets.Snippet.id', index=0, 79 | number=1, type=5, cpp_type=1, label=1, 80 | has_default_value=False, default_value=0, 81 | message_type=None, enum_type=None, containing_type=None, 82 | is_extension=False, extension_scope=None, 83 | serialized_options=None, file=DESCRIPTOR), 84 | _descriptor.FieldDescriptor( 85 | name='title', full_name='snippets.Snippet.title', index=1, 86 | number=2, type=9, cpp_type=9, label=1, 87 | has_default_value=False, default_value=b"".decode('utf-8'), 88 | message_type=None, enum_type=None, containing_type=None, 89 | is_extension=False, extension_scope=None, 90 | serialized_options=None, file=DESCRIPTOR), 91 | _descriptor.FieldDescriptor( 92 | name='language', full_name='snippets.Snippet.language', index=2, 93 | number=3, type=11, cpp_type=10, label=1, 94 | has_default_value=False, default_value=None, 95 | message_type=None, enum_type=None, containing_type=None, 96 | is_extension=False, extension_scope=None, 97 | serialized_options=None, file=DESCRIPTOR), 98 | ], 99 | extensions=[ 100 | ], 101 | nested_types=[], 102 | enum_types=[ 103 | ], 104 | serialized_options=None, 105 | is_extendable=False, 106 | syntax='proto3', 107 | extension_ranges=[], 108 | oneofs=[ 109 | ], 110 | serialized_start=145, 111 | serialized_end=225, 112 | ) 113 | 114 | _NULLABLESTRING.fields_by_name['null'].enum_type = google_dot_protobuf_dot_struct__pb2._NULLVALUE 115 | _NULLABLESTRING.oneofs_by_name['kind'].fields.append( 116 | _NULLABLESTRING.fields_by_name['value']) 117 | _NULLABLESTRING.fields_by_name['value'].containing_oneof = _NULLABLESTRING.oneofs_by_name['kind'] 118 | _NULLABLESTRING.oneofs_by_name['kind'].fields.append( 119 | _NULLABLESTRING.fields_by_name['null']) 120 | _NULLABLESTRING.fields_by_name['null'].containing_oneof = _NULLABLESTRING.oneofs_by_name['kind'] 121 | _SNIPPET.fields_by_name['language'].message_type = _NULLABLESTRING 122 | DESCRIPTOR.message_types_by_name['NullableString'] = _NULLABLESTRING 123 | DESCRIPTOR.message_types_by_name['Snippet'] = _SNIPPET 124 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 125 | 126 | NullableString = _reflection.GeneratedProtocolMessageType('NullableString', (_message.Message,), { 127 | 'DESCRIPTOR' : _NULLABLESTRING, 128 | '__module__' : 'snippets_pb2' 129 | # @@protoc_insertion_point(class_scope:snippets.NullableString) 130 | }) 131 | _sym_db.RegisterMessage(NullableString) 132 | 133 | Snippet = _reflection.GeneratedProtocolMessageType('Snippet', (_message.Message,), { 134 | 'DESCRIPTOR' : _SNIPPET, 135 | '__module__' : 'snippets_pb2' 136 | # @@protoc_insertion_point(class_scope:snippets.Snippet) 137 | }) 138 | _sym_db.RegisterMessage(Snippet) 139 | 140 | 141 | 142 | _SNIPPETCONTROLLER = _descriptor.ServiceDescriptor( 143 | name='SnippetController', 144 | full_name='snippets.SnippetController', 145 | file=DESCRIPTOR, 146 | index=0, 147 | serialized_options=None, 148 | serialized_start=227, 149 | serialized_end=296, 150 | methods=[ 151 | _descriptor.MethodDescriptor( 152 | name='Update', 153 | full_name='snippets.SnippetController.Update', 154 | index=0, 155 | containing_service=None, 156 | input_type=_SNIPPET, 157 | output_type=_SNIPPET, 158 | serialized_options=None, 159 | ), 160 | ]) 161 | _sym_db.RegisterServiceDescriptor(_SNIPPETCONTROLLER) 162 | 163 | DESCRIPTOR.services_by_name['SnippetController'] = _SNIPPETCONTROLLER 164 | 165 | # @@protoc_insertion_point(module_scope) 166 | -------------------------------------------------------------------------------- /django_grpc_framework/generics.py: -------------------------------------------------------------------------------- 1 | from django.db.models.query import QuerySet 2 | from django.shortcuts import get_object_or_404 3 | from django.core.exceptions import ValidationError 4 | from django.http import Http404 5 | import grpc 6 | 7 | from django_grpc_framework.utils import model_meta 8 | from django_grpc_framework import mixins, services 9 | 10 | 11 | class GenericService(services.Service): 12 | """ 13 | Base class for all other generic services. 14 | """ 15 | # Either set this attribute or override ``get_queryset()``. 16 | queryset = None 17 | # Either set this attribute or override ``get_serializer_class()``. 18 | serializer_class = None 19 | # Set this if you want to use object lookups other than id 20 | lookup_field = None 21 | lookup_request_field = None 22 | 23 | def get_queryset(self): 24 | """ 25 | Get the list of items for this service. 26 | This must be an iterable, and may be a queryset. 27 | Defaults to using ``self.queryset``. 28 | 29 | If you are overriding a handler method, it is important that you call 30 | ``get_queryset()`` instead of accessing the ``queryset`` attribute as 31 | ``queryset`` will get evaluated only once. 32 | 33 | Override this to provide dynamic behavior, for example:: 34 | 35 | def get_queryset(self): 36 | if self.action == 'ListSpecialUser': 37 | return SpecialUser.objects.all() 38 | return super().get_queryset() 39 | """ 40 | assert self.queryset is not None, ( 41 | "'%s' should either include a ``queryset`` attribute, " 42 | "or override the ``get_queryset()`` method." 43 | % 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 get_serializer_class(self): 52 | """ 53 | Return the class to use for the serializer. Defaults to using 54 | `self.serializer_class`. 55 | """ 56 | assert self.serializer_class is not None, ( 57 | "'%s' should either include a `serializer_class` attribute, " 58 | "or override the `get_serializer_class()` method." 59 | % self.__class__.__name__ 60 | ) 61 | return self.serializer_class 62 | 63 | def get_object(self): 64 | """ 65 | Returns an object instance that should be used for detail services. 66 | Defaults to using the lookup_field parameter to filter the base 67 | queryset. 68 | """ 69 | queryset = self.filter_queryset(self.get_queryset()) 70 | lookup_field = ( 71 | self.lookup_field 72 | or model_meta.get_model_pk(queryset.model).name 73 | ) 74 | lookup_request_field = self.lookup_request_field or lookup_field 75 | assert hasattr(self.request, lookup_request_field), ( 76 | 'Expected service %s to be called with request that has a field ' 77 | 'named "%s". Fix your request protocol definition, or set the ' 78 | '`.lookup_field` attribute on the service correctly.' % 79 | (self.__class__.__name__, lookup_request_field) 80 | ) 81 | lookup_value = getattr(self.request, lookup_request_field) 82 | filter_kwargs = {lookup_field: lookup_value} 83 | try: 84 | return get_object_or_404(queryset, **filter_kwargs) 85 | except (TypeError, ValueError, ValidationError, Http404): 86 | self.context.abort(grpc.StatusCode.NOT_FOUND, ( 87 | '%s: %s not found!' % 88 | (queryset.model.__name__, lookup_value) 89 | )) 90 | 91 | def get_serializer(self, *args, **kwargs): 92 | """ 93 | Return the serializer instance that should be used for validating and 94 | deserializing input, and for serializing output. 95 | """ 96 | serializer_class = self.get_serializer_class() 97 | kwargs.setdefault('context', self.get_serializer_context()) 98 | return serializer_class(*args, **kwargs) 99 | 100 | def get_serializer_context(self): 101 | """ 102 | Extra context provided to the serializer class. Defaults to including 103 | ``grpc_request``, ``grpc_context``, and ``service`` keys. 104 | """ 105 | return { 106 | 'grpc_request': self.request, 107 | 'grpc_context': self.context, 108 | 'service': self, 109 | } 110 | 111 | def filter_queryset(self, queryset): 112 | """Given a queryset, filter it, returning a new queryset.""" 113 | return queryset 114 | 115 | 116 | class CreateService(mixins.CreateModelMixin, 117 | GenericService): 118 | """ 119 | Concrete service for creating a model instance that provides a ``Create()`` 120 | handler. 121 | """ 122 | pass 123 | 124 | 125 | class ListService(mixins.ListModelMixin, 126 | GenericService): 127 | """ 128 | Concrete service for listing a queryset that provides a ``List()`` handler. 129 | """ 130 | pass 131 | 132 | 133 | class RetrieveService(mixins.RetrieveModelMixin, 134 | GenericService): 135 | """ 136 | Concrete service for retrieving a model instance that provides a 137 | ``Retrieve()`` handler. 138 | """ 139 | pass 140 | 141 | 142 | class DestroyService(mixins.DestroyModelMixin, 143 | GenericService): 144 | """ 145 | Concrete service for deleting a model instance that provides a ``Destroy()`` 146 | handler. 147 | """ 148 | pass 149 | 150 | 151 | class UpdateService(mixins.UpdateModelMixin, 152 | GenericService): 153 | """ 154 | Concrete service for updating a model instance that provides a 155 | ``Update()`` handler. 156 | """ 157 | pass 158 | 159 | 160 | class ReadOnlyModelService(mixins.RetrieveModelMixin, 161 | mixins.ListModelMixin, 162 | GenericService): 163 | """ 164 | Concrete service that provides default ``List()`` and ``Retrieve()`` 165 | handlers. 166 | """ 167 | pass 168 | 169 | 170 | class ModelService(mixins.CreateModelMixin, 171 | mixins.RetrieveModelMixin, 172 | mixins.UpdateModelMixin, 173 | mixins.DestroyModelMixin, 174 | mixins.ListModelMixin, 175 | GenericService): 176 | """ 177 | Concrete service that provides default ``Create()``, ``Retrieve()``, 178 | ``Update()``, ``Destroy()`` and ``List()`` handlers. 179 | """ 180 | pass 181 | -------------------------------------------------------------------------------- /examples/demo/demo_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: demo.proto 4 | 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='demo.proto', 19 | package='demo', 20 | syntax='proto3', 21 | serialized_options=None, 22 | serialized_pb=b'\n\ndemo.proto\x12\x04\x64\x65mo\x1a\x1bgoogle/protobuf/empty.proto\"3\n\x04User\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08username\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\"\x11\n\x0fUserListRequest\"!\n\x13UserRetrieveRequest\x12\n\n\x02id\x18\x01 \x01(\x05\x32\xed\x01\n\x0eUserController\x12-\n\x04List\x12\x15.demo.UserListRequest\x1a\n.demo.User\"\x00\x30\x01\x12\"\n\x06\x43reate\x12\n.demo.User\x1a\n.demo.User\"\x00\x12\x33\n\x08Retrieve\x12\x19.demo.UserRetrieveRequest\x1a\n.demo.User\"\x00\x12\"\n\x06Update\x12\n.demo.User\x1a\n.demo.User\"\x00\x12/\n\x07\x44\x65stroy\x12\n.demo.User\x1a\x16.google.protobuf.Empty\"\x00\x62\x06proto3' 23 | , 24 | dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,]) 25 | 26 | 27 | 28 | 29 | _USER = _descriptor.Descriptor( 30 | name='User', 31 | full_name='demo.User', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='id', full_name='demo.User.id', index=0, 38 | number=1, type=5, cpp_type=1, label=1, 39 | has_default_value=False, default_value=0, 40 | message_type=None, enum_type=None, containing_type=None, 41 | is_extension=False, extension_scope=None, 42 | serialized_options=None, file=DESCRIPTOR), 43 | _descriptor.FieldDescriptor( 44 | name='username', full_name='demo.User.username', index=1, 45 | number=2, type=9, cpp_type=9, label=1, 46 | has_default_value=False, default_value=b"".decode('utf-8'), 47 | message_type=None, enum_type=None, containing_type=None, 48 | is_extension=False, extension_scope=None, 49 | serialized_options=None, file=DESCRIPTOR), 50 | _descriptor.FieldDescriptor( 51 | name='email', full_name='demo.User.email', index=2, 52 | number=3, type=9, cpp_type=9, label=1, 53 | has_default_value=False, default_value=b"".decode('utf-8'), 54 | message_type=None, enum_type=None, containing_type=None, 55 | is_extension=False, extension_scope=None, 56 | serialized_options=None, file=DESCRIPTOR), 57 | ], 58 | extensions=[ 59 | ], 60 | nested_types=[], 61 | enum_types=[ 62 | ], 63 | serialized_options=None, 64 | is_extendable=False, 65 | syntax='proto3', 66 | extension_ranges=[], 67 | oneofs=[ 68 | ], 69 | serialized_start=49, 70 | serialized_end=100, 71 | ) 72 | 73 | 74 | _USERLISTREQUEST = _descriptor.Descriptor( 75 | name='UserListRequest', 76 | full_name='demo.UserListRequest', 77 | filename=None, 78 | file=DESCRIPTOR, 79 | containing_type=None, 80 | fields=[ 81 | ], 82 | extensions=[ 83 | ], 84 | nested_types=[], 85 | enum_types=[ 86 | ], 87 | serialized_options=None, 88 | is_extendable=False, 89 | syntax='proto3', 90 | extension_ranges=[], 91 | oneofs=[ 92 | ], 93 | serialized_start=102, 94 | serialized_end=119, 95 | ) 96 | 97 | 98 | _USERRETRIEVEREQUEST = _descriptor.Descriptor( 99 | name='UserRetrieveRequest', 100 | full_name='demo.UserRetrieveRequest', 101 | filename=None, 102 | file=DESCRIPTOR, 103 | containing_type=None, 104 | fields=[ 105 | _descriptor.FieldDescriptor( 106 | name='id', full_name='demo.UserRetrieveRequest.id', index=0, 107 | number=1, type=5, cpp_type=1, label=1, 108 | has_default_value=False, default_value=0, 109 | message_type=None, enum_type=None, containing_type=None, 110 | is_extension=False, extension_scope=None, 111 | serialized_options=None, file=DESCRIPTOR), 112 | ], 113 | extensions=[ 114 | ], 115 | nested_types=[], 116 | enum_types=[ 117 | ], 118 | serialized_options=None, 119 | is_extendable=False, 120 | syntax='proto3', 121 | extension_ranges=[], 122 | oneofs=[ 123 | ], 124 | serialized_start=121, 125 | serialized_end=154, 126 | ) 127 | 128 | DESCRIPTOR.message_types_by_name['User'] = _USER 129 | DESCRIPTOR.message_types_by_name['UserListRequest'] = _USERLISTREQUEST 130 | DESCRIPTOR.message_types_by_name['UserRetrieveRequest'] = _USERRETRIEVEREQUEST 131 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 132 | 133 | User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), { 134 | 'DESCRIPTOR' : _USER, 135 | '__module__' : 'demo_pb2' 136 | # @@protoc_insertion_point(class_scope:demo.User) 137 | }) 138 | _sym_db.RegisterMessage(User) 139 | 140 | UserListRequest = _reflection.GeneratedProtocolMessageType('UserListRequest', (_message.Message,), { 141 | 'DESCRIPTOR' : _USERLISTREQUEST, 142 | '__module__' : 'demo_pb2' 143 | # @@protoc_insertion_point(class_scope:demo.UserListRequest) 144 | }) 145 | _sym_db.RegisterMessage(UserListRequest) 146 | 147 | UserRetrieveRequest = _reflection.GeneratedProtocolMessageType('UserRetrieveRequest', (_message.Message,), { 148 | 'DESCRIPTOR' : _USERRETRIEVEREQUEST, 149 | '__module__' : 'demo_pb2' 150 | # @@protoc_insertion_point(class_scope:demo.UserRetrieveRequest) 151 | }) 152 | _sym_db.RegisterMessage(UserRetrieveRequest) 153 | 154 | 155 | 156 | _USERCONTROLLER = _descriptor.ServiceDescriptor( 157 | name='UserController', 158 | full_name='demo.UserController', 159 | file=DESCRIPTOR, 160 | index=0, 161 | serialized_options=None, 162 | serialized_start=157, 163 | serialized_end=394, 164 | methods=[ 165 | _descriptor.MethodDescriptor( 166 | name='List', 167 | full_name='demo.UserController.List', 168 | index=0, 169 | containing_service=None, 170 | input_type=_USERLISTREQUEST, 171 | output_type=_USER, 172 | serialized_options=None, 173 | ), 174 | _descriptor.MethodDescriptor( 175 | name='Create', 176 | full_name='demo.UserController.Create', 177 | index=1, 178 | containing_service=None, 179 | input_type=_USER, 180 | output_type=_USER, 181 | serialized_options=None, 182 | ), 183 | _descriptor.MethodDescriptor( 184 | name='Retrieve', 185 | full_name='demo.UserController.Retrieve', 186 | index=2, 187 | containing_service=None, 188 | input_type=_USERRETRIEVEREQUEST, 189 | output_type=_USER, 190 | serialized_options=None, 191 | ), 192 | _descriptor.MethodDescriptor( 193 | name='Update', 194 | full_name='demo.UserController.Update', 195 | index=3, 196 | containing_service=None, 197 | input_type=_USER, 198 | output_type=_USER, 199 | serialized_options=None, 200 | ), 201 | _descriptor.MethodDescriptor( 202 | name='Destroy', 203 | full_name='demo.UserController.Destroy', 204 | index=4, 205 | containing_service=None, 206 | input_type=_USER, 207 | output_type=google_dot_protobuf_dot_empty__pb2._EMPTY, 208 | serialized_options=None, 209 | ), 210 | ]) 211 | _sym_db.RegisterServiceDescriptor(_USERCONTROLLER) 212 | 213 | DESCRIPTOR.services_by_name['UserController'] = _USERCONTROLLER 214 | 215 | # @@protoc_insertion_point(module_scope) 216 | -------------------------------------------------------------------------------- /examples/tutorial/blog_proto/post_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: blog_proto/post.proto 4 | 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='blog_proto/post.proto', 19 | package='blog_proto', 20 | syntax='proto3', 21 | serialized_options=None, 22 | serialized_pb=b'\n\x15\x62log_proto/post.proto\x12\nblog_proto\x1a\x1bgoogle/protobuf/empty.proto\"2\n\x04Post\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\"\x11\n\x0fPostListRequest\"!\n\x13PostRetrieveRequest\x12\n\n\x02id\x18\x01 \x01(\x05\x32\xa3\x02\n\x0ePostController\x12\x39\n\x04List\x12\x1b.blog_proto.PostListRequest\x1a\x10.blog_proto.Post\"\x00\x30\x01\x12.\n\x06\x43reate\x12\x10.blog_proto.Post\x1a\x10.blog_proto.Post\"\x00\x12?\n\x08Retrieve\x12\x1f.blog_proto.PostRetrieveRequest\x1a\x10.blog_proto.Post\"\x00\x12.\n\x06Update\x12\x10.blog_proto.Post\x1a\x10.blog_proto.Post\"\x00\x12\x35\n\x07\x44\x65stroy\x12\x10.blog_proto.Post\x1a\x16.google.protobuf.Empty\"\x00\x62\x06proto3' 23 | , 24 | dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,]) 25 | 26 | 27 | 28 | 29 | _POST = _descriptor.Descriptor( 30 | name='Post', 31 | full_name='blog_proto.Post', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='id', full_name='blog_proto.Post.id', index=0, 38 | number=1, type=5, cpp_type=1, label=1, 39 | has_default_value=False, default_value=0, 40 | message_type=None, enum_type=None, containing_type=None, 41 | is_extension=False, extension_scope=None, 42 | serialized_options=None, file=DESCRIPTOR), 43 | _descriptor.FieldDescriptor( 44 | name='title', full_name='blog_proto.Post.title', index=1, 45 | number=2, type=9, cpp_type=9, label=1, 46 | has_default_value=False, default_value=b"".decode('utf-8'), 47 | message_type=None, enum_type=None, containing_type=None, 48 | is_extension=False, extension_scope=None, 49 | serialized_options=None, file=DESCRIPTOR), 50 | _descriptor.FieldDescriptor( 51 | name='content', full_name='blog_proto.Post.content', index=2, 52 | number=3, type=9, cpp_type=9, label=1, 53 | has_default_value=False, default_value=b"".decode('utf-8'), 54 | message_type=None, enum_type=None, containing_type=None, 55 | is_extension=False, extension_scope=None, 56 | serialized_options=None, file=DESCRIPTOR), 57 | ], 58 | extensions=[ 59 | ], 60 | nested_types=[], 61 | enum_types=[ 62 | ], 63 | serialized_options=None, 64 | is_extendable=False, 65 | syntax='proto3', 66 | extension_ranges=[], 67 | oneofs=[ 68 | ], 69 | serialized_start=66, 70 | serialized_end=116, 71 | ) 72 | 73 | 74 | _POSTLISTREQUEST = _descriptor.Descriptor( 75 | name='PostListRequest', 76 | full_name='blog_proto.PostListRequest', 77 | filename=None, 78 | file=DESCRIPTOR, 79 | containing_type=None, 80 | fields=[ 81 | ], 82 | extensions=[ 83 | ], 84 | nested_types=[], 85 | enum_types=[ 86 | ], 87 | serialized_options=None, 88 | is_extendable=False, 89 | syntax='proto3', 90 | extension_ranges=[], 91 | oneofs=[ 92 | ], 93 | serialized_start=118, 94 | serialized_end=135, 95 | ) 96 | 97 | 98 | _POSTRETRIEVEREQUEST = _descriptor.Descriptor( 99 | name='PostRetrieveRequest', 100 | full_name='blog_proto.PostRetrieveRequest', 101 | filename=None, 102 | file=DESCRIPTOR, 103 | containing_type=None, 104 | fields=[ 105 | _descriptor.FieldDescriptor( 106 | name='id', full_name='blog_proto.PostRetrieveRequest.id', index=0, 107 | number=1, type=5, cpp_type=1, label=1, 108 | has_default_value=False, default_value=0, 109 | message_type=None, enum_type=None, containing_type=None, 110 | is_extension=False, extension_scope=None, 111 | serialized_options=None, file=DESCRIPTOR), 112 | ], 113 | extensions=[ 114 | ], 115 | nested_types=[], 116 | enum_types=[ 117 | ], 118 | serialized_options=None, 119 | is_extendable=False, 120 | syntax='proto3', 121 | extension_ranges=[], 122 | oneofs=[ 123 | ], 124 | serialized_start=137, 125 | serialized_end=170, 126 | ) 127 | 128 | DESCRIPTOR.message_types_by_name['Post'] = _POST 129 | DESCRIPTOR.message_types_by_name['PostListRequest'] = _POSTLISTREQUEST 130 | DESCRIPTOR.message_types_by_name['PostRetrieveRequest'] = _POSTRETRIEVEREQUEST 131 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 132 | 133 | Post = _reflection.GeneratedProtocolMessageType('Post', (_message.Message,), { 134 | 'DESCRIPTOR' : _POST, 135 | '__module__' : 'blog_proto.post_pb2' 136 | # @@protoc_insertion_point(class_scope:blog_proto.Post) 137 | }) 138 | _sym_db.RegisterMessage(Post) 139 | 140 | PostListRequest = _reflection.GeneratedProtocolMessageType('PostListRequest', (_message.Message,), { 141 | 'DESCRIPTOR' : _POSTLISTREQUEST, 142 | '__module__' : 'blog_proto.post_pb2' 143 | # @@protoc_insertion_point(class_scope:blog_proto.PostListRequest) 144 | }) 145 | _sym_db.RegisterMessage(PostListRequest) 146 | 147 | PostRetrieveRequest = _reflection.GeneratedProtocolMessageType('PostRetrieveRequest', (_message.Message,), { 148 | 'DESCRIPTOR' : _POSTRETRIEVEREQUEST, 149 | '__module__' : 'blog_proto.post_pb2' 150 | # @@protoc_insertion_point(class_scope:blog_proto.PostRetrieveRequest) 151 | }) 152 | _sym_db.RegisterMessage(PostRetrieveRequest) 153 | 154 | 155 | 156 | _POSTCONTROLLER = _descriptor.ServiceDescriptor( 157 | name='PostController', 158 | full_name='blog_proto.PostController', 159 | file=DESCRIPTOR, 160 | index=0, 161 | serialized_options=None, 162 | serialized_start=173, 163 | serialized_end=464, 164 | methods=[ 165 | _descriptor.MethodDescriptor( 166 | name='List', 167 | full_name='blog_proto.PostController.List', 168 | index=0, 169 | containing_service=None, 170 | input_type=_POSTLISTREQUEST, 171 | output_type=_POST, 172 | serialized_options=None, 173 | ), 174 | _descriptor.MethodDescriptor( 175 | name='Create', 176 | full_name='blog_proto.PostController.Create', 177 | index=1, 178 | containing_service=None, 179 | input_type=_POST, 180 | output_type=_POST, 181 | serialized_options=None, 182 | ), 183 | _descriptor.MethodDescriptor( 184 | name='Retrieve', 185 | full_name='blog_proto.PostController.Retrieve', 186 | index=2, 187 | containing_service=None, 188 | input_type=_POSTRETRIEVEREQUEST, 189 | output_type=_POST, 190 | serialized_options=None, 191 | ), 192 | _descriptor.MethodDescriptor( 193 | name='Update', 194 | full_name='blog_proto.PostController.Update', 195 | index=3, 196 | containing_service=None, 197 | input_type=_POST, 198 | output_type=_POST, 199 | serialized_options=None, 200 | ), 201 | _descriptor.MethodDescriptor( 202 | name='Destroy', 203 | full_name='blog_proto.PostController.Destroy', 204 | index=4, 205 | containing_service=None, 206 | input_type=_POST, 207 | output_type=google_dot_protobuf_dot_empty__pb2._EMPTY, 208 | serialized_options=None, 209 | ), 210 | ]) 211 | _sym_db.RegisterServiceDescriptor(_POSTCONTROLLER) 212 | 213 | DESCRIPTOR.services_by_name['PostController'] = _POSTCONTROLLER 214 | 215 | # @@protoc_insertion_point(module_scope) 216 | -------------------------------------------------------------------------------- /django_grpc_framework/protobuf/generators.py: -------------------------------------------------------------------------------- 1 | import io 2 | from collections import OrderedDict 3 | 4 | from django.db import models 5 | from rest_framework.utils import model_meta 6 | from rest_framework.utils.field_mapping import ClassLookupDict 7 | 8 | 9 | class ModelProtoGenerator: 10 | type_mapping = { 11 | # Numeric 12 | models.AutoField: 'int32', 13 | models.SmallIntegerField: 'int32', 14 | models.IntegerField: 'int32', 15 | models.BigIntegerField: 'int64', 16 | models.PositiveSmallIntegerField: 'int32', 17 | models.PositiveIntegerField: 'int32', 18 | models.FloatField: 'float', 19 | models.DecimalField: 'string', 20 | # Boolean 21 | models.BooleanField: 'bool', 22 | models.NullBooleanField: 'bool', 23 | # Date and time 24 | models.DateField: 'string', 25 | models.TimeField: 'string', 26 | models.DateTimeField: 'string', 27 | models.DurationField: 'string', 28 | # String 29 | models.CharField: 'string', 30 | models.TextField: 'string', 31 | models.EmailField: 'string', 32 | models.SlugField: 'string', 33 | models.URLField: 'string', 34 | models.UUIDField: 'string', 35 | models.GenericIPAddressField: 'string', 36 | models.FilePathField: 'string', 37 | # Default 38 | models.Field: 'string', 39 | } 40 | 41 | def __init__(self, model, field_names=None, package=None): 42 | self.model = model 43 | self.field_names = field_names 44 | if not package: 45 | package = model.__name__.lower() 46 | self.package = package 47 | self.type_mapping = ClassLookupDict(self.type_mapping) 48 | # Retrieve metadata about fields & relationships on the model class. 49 | self.field_info = model_meta.get_field_info(model) 50 | self._writer = _CodeWriter() 51 | 52 | def get_proto(self): 53 | self._writer.write_line('syntax = "proto3";') 54 | self._writer.write_line('') 55 | self._writer.write_line('package %s;' % self.package) 56 | self._writer.write_line('') 57 | self._writer.write_line('import "google/protobuf/empty.proto";') 58 | self._writer.write_line('') 59 | self._generate_service() 60 | self._writer.write_line('') 61 | self._generate_message() 62 | return self._writer.get_code() 63 | 64 | def _generate_service(self): 65 | self._writer.write_line('service %sController {' % self.model.__name__) 66 | with self._writer.indent(): 67 | self._writer.write_line( 68 | 'rpc List(%sListRequest) returns (stream %s) {}' % 69 | (self.model.__name__, self.model.__name__) 70 | ) 71 | self._writer.write_line( 72 | 'rpc Create(%s) returns (%s) {}' % 73 | (self.model.__name__, self.model.__name__) 74 | ) 75 | self._writer.write_line( 76 | 'rpc Retrieve(%sRetrieveRequest) returns (%s) {}' % 77 | (self.model.__name__, self.model.__name__) 78 | ) 79 | self._writer.write_line( 80 | 'rpc Update(%s) returns (%s) {}' % 81 | (self.model.__name__, self.model.__name__) 82 | ) 83 | self._writer.write_line( 84 | 'rpc Destroy(%s) returns (google.protobuf.Empty) {}' % 85 | self.model.__name__ 86 | ) 87 | self._writer.write_line('}') 88 | 89 | def _generate_message(self): 90 | self._writer.write_line('message %s {' % self.model.__name__) 91 | with self._writer.indent(): 92 | number = 0 93 | for field_name, proto_type in self.get_fields().items(): 94 | number += 1 95 | self._writer.write_line( 96 | '%s %s = %s;' % 97 | (proto_type, field_name, number) 98 | ) 99 | self._writer.write_line('}') 100 | self._writer.write_line('') 101 | self._writer.write_line('message %sListRequest {' % self.model.__name__) 102 | self._writer.write_line('}') 103 | self._writer.write_line('') 104 | self._writer.write_line('message %sRetrieveRequest {' % self.model.__name__) 105 | with self._writer.indent(): 106 | pk_field_name = self.field_info.pk.name 107 | pk_proto_type = self.build_proto_type( 108 | pk_field_name, self.field_info, self.model 109 | ) 110 | self._writer.write_line( 111 | '%s %s = 1;' % 112 | (pk_proto_type, pk_field_name) 113 | ) 114 | self._writer.write_line('}') 115 | 116 | def get_fields(self): 117 | """ 118 | Return the dict of field names -> proto types. 119 | """ 120 | if model_meta.is_abstract_model(self.model): 121 | raise ValueError('Cannot generate proto for abstract model.') 122 | fields = OrderedDict() 123 | for field_name in self.get_field_names(): 124 | if field_name in fields: 125 | continue 126 | fields[field_name] = self.build_proto_type( 127 | field_name, self.field_info, self.model 128 | ) 129 | return fields 130 | 131 | def get_field_names(self): 132 | field_names = self.field_names 133 | if not field_names: 134 | field_names = ( 135 | [self.field_info.pk.name] 136 | + list(self.field_info.fields) 137 | + list(self.field_info.forward_relations) 138 | ) 139 | return field_names 140 | 141 | def build_proto_type(self, field_name, field_info, model_class): 142 | if field_name in field_info.fields_and_pk: 143 | model_field = field_info.fields_and_pk[field_name] 144 | return self._build_standard_proto_type(model_field) 145 | elif field_name in field_info.relations: 146 | relation_info = field_info.relations[field_name] 147 | return self._build_relational_proto_type(relation_info) 148 | else: 149 | raise ValueError( 150 | 'Field name `%s` is not valid for model `%s`.' % 151 | (field_name, model_class.__name__) 152 | ) 153 | 154 | def _build_standard_proto_type(self, model_field): 155 | if model_field.one_to_one and model_field.primary_key: 156 | info = model_meta.get_field_info(model_field.related_model) 157 | return self.build_proto_type( 158 | info.pk.name, info, model_field.related_model 159 | ) 160 | else: 161 | return self.type_mapping[model_field] 162 | 163 | def _build_relational_proto_type(self, relation_info): 164 | info = model_meta.get_field_info(relation_info.related_model) 165 | to_field = info.pk.name 166 | if relation_info.to_field and not relation_info.reverse: 167 | to_field = relation_info.to_field 168 | proto_type = self.build_proto_type( 169 | to_field, info, relation_info.related_model 170 | ) 171 | if relation_info.to_many: 172 | proto_type = 'repeated ' + proto_type 173 | return proto_type 174 | 175 | 176 | class _CodeWriter: 177 | def __init__(self): 178 | self.buffer = io.StringIO() 179 | self._indent = 0 180 | 181 | def indent(self): 182 | return self 183 | 184 | def __enter__(self): 185 | self._indent += 1 186 | return self 187 | 188 | def __exit__(self, *args): 189 | self._indent -= 1 190 | 191 | def write_line(self, line): 192 | for i in range(self._indent): 193 | self.buffer.write(" ") 194 | print(line, file=self.buffer) 195 | 196 | def get_code(self): 197 | return self.buffer.getvalue() -------------------------------------------------------------------------------- /examples/quickstart/account_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: account.proto 4 | 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='account.proto', 19 | package='account', 20 | syntax='proto3', 21 | serialized_options=None, 22 | serialized_pb=b'\n\raccount.proto\x12\x07\x61\x63\x63ount\x1a\x1bgoogle/protobuf/empty.proto\"C\n\x04User\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08username\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0e\n\x06groups\x18\x04 \x03(\x05\"\x11\n\x0fUserListRequest\"!\n\x13UserRetrieveRequest\x12\n\n\x02id\x18\x01 \x01(\x05\x32\x88\x02\n\x0eUserController\x12\x33\n\x04List\x12\x18.account.UserListRequest\x1a\r.account.User\"\x00\x30\x01\x12(\n\x06\x43reate\x12\r.account.User\x1a\r.account.User\"\x00\x12\x39\n\x08Retrieve\x12\x1c.account.UserRetrieveRequest\x1a\r.account.User\"\x00\x12(\n\x06Update\x12\r.account.User\x1a\r.account.User\"\x00\x12\x32\n\x07\x44\x65stroy\x12\r.account.User\x1a\x16.google.protobuf.Empty\"\x00\x62\x06proto3' 23 | , 24 | dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,]) 25 | 26 | 27 | 28 | 29 | _USER = _descriptor.Descriptor( 30 | name='User', 31 | full_name='account.User', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='id', full_name='account.User.id', index=0, 38 | number=1, type=5, cpp_type=1, label=1, 39 | has_default_value=False, default_value=0, 40 | message_type=None, enum_type=None, containing_type=None, 41 | is_extension=False, extension_scope=None, 42 | serialized_options=None, file=DESCRIPTOR), 43 | _descriptor.FieldDescriptor( 44 | name='username', full_name='account.User.username', index=1, 45 | number=2, type=9, cpp_type=9, label=1, 46 | has_default_value=False, default_value=b"".decode('utf-8'), 47 | message_type=None, enum_type=None, containing_type=None, 48 | is_extension=False, extension_scope=None, 49 | serialized_options=None, file=DESCRIPTOR), 50 | _descriptor.FieldDescriptor( 51 | name='email', full_name='account.User.email', index=2, 52 | number=3, type=9, cpp_type=9, label=1, 53 | has_default_value=False, default_value=b"".decode('utf-8'), 54 | message_type=None, enum_type=None, containing_type=None, 55 | is_extension=False, extension_scope=None, 56 | serialized_options=None, file=DESCRIPTOR), 57 | _descriptor.FieldDescriptor( 58 | name='groups', full_name='account.User.groups', index=3, 59 | number=4, type=5, cpp_type=1, label=3, 60 | has_default_value=False, default_value=[], 61 | message_type=None, enum_type=None, containing_type=None, 62 | is_extension=False, extension_scope=None, 63 | serialized_options=None, file=DESCRIPTOR), 64 | ], 65 | extensions=[ 66 | ], 67 | nested_types=[], 68 | enum_types=[ 69 | ], 70 | serialized_options=None, 71 | is_extendable=False, 72 | syntax='proto3', 73 | extension_ranges=[], 74 | oneofs=[ 75 | ], 76 | serialized_start=55, 77 | serialized_end=122, 78 | ) 79 | 80 | 81 | _USERLISTREQUEST = _descriptor.Descriptor( 82 | name='UserListRequest', 83 | full_name='account.UserListRequest', 84 | filename=None, 85 | file=DESCRIPTOR, 86 | containing_type=None, 87 | fields=[ 88 | ], 89 | extensions=[ 90 | ], 91 | nested_types=[], 92 | enum_types=[ 93 | ], 94 | serialized_options=None, 95 | is_extendable=False, 96 | syntax='proto3', 97 | extension_ranges=[], 98 | oneofs=[ 99 | ], 100 | serialized_start=124, 101 | serialized_end=141, 102 | ) 103 | 104 | 105 | _USERRETRIEVEREQUEST = _descriptor.Descriptor( 106 | name='UserRetrieveRequest', 107 | full_name='account.UserRetrieveRequest', 108 | filename=None, 109 | file=DESCRIPTOR, 110 | containing_type=None, 111 | fields=[ 112 | _descriptor.FieldDescriptor( 113 | name='id', full_name='account.UserRetrieveRequest.id', index=0, 114 | number=1, type=5, cpp_type=1, label=1, 115 | has_default_value=False, default_value=0, 116 | message_type=None, enum_type=None, containing_type=None, 117 | is_extension=False, extension_scope=None, 118 | serialized_options=None, file=DESCRIPTOR), 119 | ], 120 | extensions=[ 121 | ], 122 | nested_types=[], 123 | enum_types=[ 124 | ], 125 | serialized_options=None, 126 | is_extendable=False, 127 | syntax='proto3', 128 | extension_ranges=[], 129 | oneofs=[ 130 | ], 131 | serialized_start=143, 132 | serialized_end=176, 133 | ) 134 | 135 | DESCRIPTOR.message_types_by_name['User'] = _USER 136 | DESCRIPTOR.message_types_by_name['UserListRequest'] = _USERLISTREQUEST 137 | DESCRIPTOR.message_types_by_name['UserRetrieveRequest'] = _USERRETRIEVEREQUEST 138 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 139 | 140 | User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), { 141 | 'DESCRIPTOR' : _USER, 142 | '__module__' : 'account_pb2' 143 | # @@protoc_insertion_point(class_scope:account.User) 144 | }) 145 | _sym_db.RegisterMessage(User) 146 | 147 | UserListRequest = _reflection.GeneratedProtocolMessageType('UserListRequest', (_message.Message,), { 148 | 'DESCRIPTOR' : _USERLISTREQUEST, 149 | '__module__' : 'account_pb2' 150 | # @@protoc_insertion_point(class_scope:account.UserListRequest) 151 | }) 152 | _sym_db.RegisterMessage(UserListRequest) 153 | 154 | UserRetrieveRequest = _reflection.GeneratedProtocolMessageType('UserRetrieveRequest', (_message.Message,), { 155 | 'DESCRIPTOR' : _USERRETRIEVEREQUEST, 156 | '__module__' : 'account_pb2' 157 | # @@protoc_insertion_point(class_scope:account.UserRetrieveRequest) 158 | }) 159 | _sym_db.RegisterMessage(UserRetrieveRequest) 160 | 161 | 162 | 163 | _USERCONTROLLER = _descriptor.ServiceDescriptor( 164 | name='UserController', 165 | full_name='account.UserController', 166 | file=DESCRIPTOR, 167 | index=0, 168 | serialized_options=None, 169 | serialized_start=179, 170 | serialized_end=443, 171 | methods=[ 172 | _descriptor.MethodDescriptor( 173 | name='List', 174 | full_name='account.UserController.List', 175 | index=0, 176 | containing_service=None, 177 | input_type=_USERLISTREQUEST, 178 | output_type=_USER, 179 | serialized_options=None, 180 | ), 181 | _descriptor.MethodDescriptor( 182 | name='Create', 183 | full_name='account.UserController.Create', 184 | index=1, 185 | containing_service=None, 186 | input_type=_USER, 187 | output_type=_USER, 188 | serialized_options=None, 189 | ), 190 | _descriptor.MethodDescriptor( 191 | name='Retrieve', 192 | full_name='account.UserController.Retrieve', 193 | index=2, 194 | containing_service=None, 195 | input_type=_USERRETRIEVEREQUEST, 196 | output_type=_USER, 197 | serialized_options=None, 198 | ), 199 | _descriptor.MethodDescriptor( 200 | name='Update', 201 | full_name='account.UserController.Update', 202 | index=3, 203 | containing_service=None, 204 | input_type=_USER, 205 | output_type=_USER, 206 | serialized_options=None, 207 | ), 208 | _descriptor.MethodDescriptor( 209 | name='Destroy', 210 | full_name='account.UserController.Destroy', 211 | index=4, 212 | containing_service=None, 213 | input_type=_USER, 214 | output_type=google_dot_protobuf_dot_empty__pb2._EMPTY, 215 | serialized_options=None, 216 | ), 217 | ]) 218 | _sym_db.RegisterServiceDescriptor(_USERCONTROLLER) 219 | 220 | DESCRIPTOR.services_by_name['UserController'] = _USERCONTROLLER 221 | 222 | # @@protoc_insertion_point(module_scope) 223 | -------------------------------------------------------------------------------- /docs/tutorial/building_services.rst: -------------------------------------------------------------------------------- 1 | .. _building_services: 2 | 3 | Building Services 4 | ================= 5 | 6 | This tutorial will create a simple blog gRPC Service. 7 | 8 | 9 | Environment setup 10 | ----------------- 11 | 12 | Create a new virtual environment for our project:: 13 | 14 | python3 -m venv env 15 | source env/bin/activate 16 | 17 | Install our packages:: 18 | 19 | pip install django 20 | pip install djangorestframework # we need the serialization 21 | pip install djangogrpcframework 22 | pip install grpcio 23 | pip install grpcio-tools 24 | 25 | 26 | Project setup 27 | ------------- 28 | 29 | Let's create a new project to work with:: 30 | 31 | django-admin startproject tutorial 32 | cd tutorial 33 | 34 | Now we can create an app that we'll use to create a simple gRPC Service:: 35 | 36 | python manage.py startapp blog 37 | 38 | We'll need to add our new ``blog`` app and the ``django_grpc_framework`` app to 39 | ``INSTALLED_APPS``. Let's edit the ``tutorial/settings.py`` file:: 40 | 41 | INSTALLED_APPS = [ 42 | ... 43 | 'django_grpc_framework', 44 | 'blog', 45 | ] 46 | 47 | 48 | Create a model 49 | -------------- 50 | 51 | Now we're going to create a simple ``Post`` model that is used to store blog 52 | posts. Edit the ``blog/models.py`` file:: 53 | 54 | from django.db import models 55 | 56 | 57 | class Post(models.Model): 58 | title = models.CharField(max_length=100) 59 | content = models.TextField() 60 | created = models.DateTimeField(auto_now_add=True) 61 | 62 | class Meta: 63 | ordering = ['created'] 64 | 65 | We also need to create a migration for our post model, and sync the database:: 66 | 67 | python manage.py makemigrations blog 68 | python manage.py migrate 69 | 70 | 71 | Defining a service 72 | ------------------ 73 | 74 | Our first step is to define the gRPC service and messages, create a directory 75 | ``tutorial/protos`` that sits next to ``tutorial/manage.py``, create another 76 | directory ``protos/blog_proto`` and create the ``protos/blog_proto/post.proto`` 77 | file: 78 | 79 | .. code-block:: protobuf 80 | 81 | syntax = "proto3"; 82 | 83 | package blog_proto; 84 | 85 | import "google/protobuf/empty.proto"; 86 | 87 | service PostController { 88 | rpc List(PostListRequest) returns (stream Post) {} 89 | rpc Create(Post) returns (Post) {} 90 | rpc Retrieve(PostRetrieveRequest) returns (Post) {} 91 | rpc Update(Post) returns (Post) {} 92 | rpc Destroy(Post) returns (google.protobuf.Empty) {} 93 | } 94 | 95 | message Post { 96 | int32 id = 1; 97 | string title = 2; 98 | string content = 3; 99 | } 100 | 101 | message PostListRequest { 102 | } 103 | 104 | message PostRetrieveRequest { 105 | int32 id = 1; 106 | } 107 | 108 | For a model-backed service, you could also just run the model proto generator:: 109 | 110 | python manage.py generateproto --model blog.models.Post --fields=id,title,content --file protos/blog_proto/post.proto 111 | 112 | Then edit it as needed, here the package name can't be automatically inferred 113 | by the proto generator, change ``package post`` to ``package blog_proto``. 114 | 115 | Next we need to generate gRPC code, from the ``tutorial`` directory, run:: 116 | 117 | python -m grpc_tools.protoc --proto_path=./protos --python_out=./ --grpc_python_out=./ ./protos/blog_proto/post.proto 118 | 119 | 120 | Create a Serializer class 121 | ------------------------- 122 | 123 | Before we implement our gRPC service, we need to provide a way of serializing 124 | and deserializing the post instances into protocol buffer messages. We can 125 | do this by declaring serializers, create a file in the ``blog`` directory 126 | named ``serializers.py`` and add the following:: 127 | 128 | from django_grpc_framework import proto_serializerss 129 | from blog.models import Post 130 | from blog_proto import post_pb2 131 | 132 | 133 | class PostProtoSerializer(proto_serializers.ModelProtoSerializer): 134 | class Meta: 135 | model = Post 136 | proto_class = post_pb2.Post 137 | fields = ['id', 'title', 'content'] 138 | 139 | 140 | Write a service 141 | --------------- 142 | 143 | With our serializer class, we'll write a regular grpc service, create a file 144 | in the ``blog`` directory named ``services.py`` and add the following:: 145 | 146 | import grpc 147 | from google.protobuf import empty_pb2 148 | from django_grpc_framework.services import Service 149 | from blog.models import Post 150 | from blog.serializers import PostProtoSerializer 151 | 152 | 153 | class PostService(Service): 154 | def List(self, request, context): 155 | posts = Post.objects.all() 156 | serializer = PostProtoSerializer(posts, many=True) 157 | for msg in serializer.message: 158 | yield msg 159 | 160 | def Create(self, request, context): 161 | serializer = PostProtoSerializer(message=request) 162 | serializer.is_valid(raise_exception=True) 163 | serializer.save() 164 | return serializer.message 165 | 166 | def get_object(self, pk): 167 | try: 168 | return Post.objects.get(pk=pk) 169 | except Post.DoesNotExist: 170 | self.context.abort(grpc.StatusCode.NOT_FOUND, 'Post:%s not found!' % pk) 171 | 172 | def Retrieve(self, request, context): 173 | post = self.get_object(request.id) 174 | serializer = PostProtoSerializer(post) 175 | return serializer.message 176 | 177 | def Update(self, request, context): 178 | post = self.get_object(request.id) 179 | serializer = PostProtoSerializer(post, message=request) 180 | serializer.is_valid(raise_exception=True) 181 | serializer.save() 182 | return serializer.message 183 | 184 | def Destroy(self, request, context): 185 | post = self.get_object(request.id) 186 | post.delete() 187 | return empty_pb2.Empty() 188 | 189 | Finally we need to wire there services up, create ``blog/handlers.py`` file:: 190 | 191 | from blog._services import PostService 192 | from blog_proto import post_pb2_grpc 193 | 194 | 195 | def grpc_handlers(server): 196 | post_pb2_grpc.add_PostControllerServicer_to_server(PostService.as_servicer(), server) 197 | 198 | Also we need to wire up the root handlers conf, in ``tutorial/urls.py`` 199 | file, include our blog app's grpc handlers:: 200 | 201 | from blog.handlers import grpc_handlers as blog_grpc_handlers 202 | 203 | 204 | urlpatterns = [] 205 | 206 | 207 | def grpc_handlers(server): 208 | blog_grpc_handlers(server) 209 | 210 | 211 | Calling our service 212 | ------------------- 213 | 214 | Now we can start up a gRPC server so that clients can actually use our 215 | service:: 216 | 217 | python manage.py grpcrunserver --dev 218 | 219 | In another terminal window, we can test the server:: 220 | 221 | import grpc 222 | from blog_proto import post_pb2, post_pb2_grpc 223 | 224 | 225 | with grpc.insecure_channel('localhost:50051') as channel: 226 | stub = post_pb2_grpc.PostControllerStub(channel) 227 | print('----- Create -----') 228 | response = stub.Create(post_pb2.Post(title='t1', content='c1')) 229 | print(response, end='') 230 | print('----- List -----') 231 | for post in stub.List(post_pb2.PostListRequest()): 232 | print(post, end='') 233 | print('----- Retrieve -----') 234 | response = stub.Retrieve(post_pb2.PostRetrieveRequest(id=response.id)) 235 | print(response, end='') 236 | print('----- Update -----') 237 | response = stub.Update(post_pb2.Post(id=response.id, title='t2', content='c2')) 238 | print(response, end='') 239 | print('----- Delete -----') 240 | stub.Destroy(post_pb2.Post(id=response.id)) 241 | -------------------------------------------------------------------------------- /examples/demo/demo_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | import demo_pb2 as demo__pb2 5 | from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 6 | 7 | 8 | class UserControllerStub(object): 9 | """Missing associated documentation comment in .proto file""" 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.List = channel.unary_stream( 18 | '/demo.UserController/List', 19 | request_serializer=demo__pb2.UserListRequest.SerializeToString, 20 | response_deserializer=demo__pb2.User.FromString, 21 | ) 22 | self.Create = channel.unary_unary( 23 | '/demo.UserController/Create', 24 | request_serializer=demo__pb2.User.SerializeToString, 25 | response_deserializer=demo__pb2.User.FromString, 26 | ) 27 | self.Retrieve = channel.unary_unary( 28 | '/demo.UserController/Retrieve', 29 | request_serializer=demo__pb2.UserRetrieveRequest.SerializeToString, 30 | response_deserializer=demo__pb2.User.FromString, 31 | ) 32 | self.Update = channel.unary_unary( 33 | '/demo.UserController/Update', 34 | request_serializer=demo__pb2.User.SerializeToString, 35 | response_deserializer=demo__pb2.User.FromString, 36 | ) 37 | self.Destroy = channel.unary_unary( 38 | '/demo.UserController/Destroy', 39 | request_serializer=demo__pb2.User.SerializeToString, 40 | response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, 41 | ) 42 | 43 | 44 | class UserControllerServicer(object): 45 | """Missing associated documentation comment in .proto file""" 46 | 47 | def List(self, request, context): 48 | """Missing associated documentation comment in .proto file""" 49 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 50 | context.set_details('Method not implemented!') 51 | raise NotImplementedError('Method not implemented!') 52 | 53 | def Create(self, request, context): 54 | """Missing associated documentation comment in .proto file""" 55 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 56 | context.set_details('Method not implemented!') 57 | raise NotImplementedError('Method not implemented!') 58 | 59 | def Retrieve(self, request, context): 60 | """Missing associated documentation comment in .proto file""" 61 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 62 | context.set_details('Method not implemented!') 63 | raise NotImplementedError('Method not implemented!') 64 | 65 | def Update(self, request, context): 66 | """Missing associated documentation comment in .proto file""" 67 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 68 | context.set_details('Method not implemented!') 69 | raise NotImplementedError('Method not implemented!') 70 | 71 | def Destroy(self, request, context): 72 | """Missing associated documentation comment in .proto file""" 73 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 74 | context.set_details('Method not implemented!') 75 | raise NotImplementedError('Method not implemented!') 76 | 77 | 78 | def add_UserControllerServicer_to_server(servicer, server): 79 | rpc_method_handlers = { 80 | 'List': grpc.unary_stream_rpc_method_handler( 81 | servicer.List, 82 | request_deserializer=demo__pb2.UserListRequest.FromString, 83 | response_serializer=demo__pb2.User.SerializeToString, 84 | ), 85 | 'Create': grpc.unary_unary_rpc_method_handler( 86 | servicer.Create, 87 | request_deserializer=demo__pb2.User.FromString, 88 | response_serializer=demo__pb2.User.SerializeToString, 89 | ), 90 | 'Retrieve': grpc.unary_unary_rpc_method_handler( 91 | servicer.Retrieve, 92 | request_deserializer=demo__pb2.UserRetrieveRequest.FromString, 93 | response_serializer=demo__pb2.User.SerializeToString, 94 | ), 95 | 'Update': grpc.unary_unary_rpc_method_handler( 96 | servicer.Update, 97 | request_deserializer=demo__pb2.User.FromString, 98 | response_serializer=demo__pb2.User.SerializeToString, 99 | ), 100 | 'Destroy': grpc.unary_unary_rpc_method_handler( 101 | servicer.Destroy, 102 | request_deserializer=demo__pb2.User.FromString, 103 | response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, 104 | ), 105 | } 106 | generic_handler = grpc.method_handlers_generic_handler( 107 | 'demo.UserController', rpc_method_handlers) 108 | server.add_generic_rpc_handlers((generic_handler,)) 109 | 110 | 111 | # This class is part of an EXPERIMENTAL API. 112 | class UserController(object): 113 | """Missing associated documentation comment in .proto file""" 114 | 115 | @staticmethod 116 | def List(request, 117 | target, 118 | options=(), 119 | channel_credentials=None, 120 | call_credentials=None, 121 | compression=None, 122 | wait_for_ready=None, 123 | timeout=None, 124 | metadata=None): 125 | return grpc.experimental.unary_stream(request, target, '/demo.UserController/List', 126 | demo__pb2.UserListRequest.SerializeToString, 127 | demo__pb2.User.FromString, 128 | options, channel_credentials, 129 | call_credentials, compression, wait_for_ready, timeout, metadata) 130 | 131 | @staticmethod 132 | def Create(request, 133 | target, 134 | options=(), 135 | channel_credentials=None, 136 | call_credentials=None, 137 | compression=None, 138 | wait_for_ready=None, 139 | timeout=None, 140 | metadata=None): 141 | return grpc.experimental.unary_unary(request, target, '/demo.UserController/Create', 142 | demo__pb2.User.SerializeToString, 143 | demo__pb2.User.FromString, 144 | options, channel_credentials, 145 | call_credentials, compression, wait_for_ready, timeout, metadata) 146 | 147 | @staticmethod 148 | def Retrieve(request, 149 | target, 150 | options=(), 151 | channel_credentials=None, 152 | call_credentials=None, 153 | compression=None, 154 | wait_for_ready=None, 155 | timeout=None, 156 | metadata=None): 157 | return grpc.experimental.unary_unary(request, target, '/demo.UserController/Retrieve', 158 | demo__pb2.UserRetrieveRequest.SerializeToString, 159 | demo__pb2.User.FromString, 160 | options, channel_credentials, 161 | call_credentials, compression, wait_for_ready, timeout, metadata) 162 | 163 | @staticmethod 164 | def Update(request, 165 | target, 166 | options=(), 167 | channel_credentials=None, 168 | call_credentials=None, 169 | compression=None, 170 | wait_for_ready=None, 171 | timeout=None, 172 | metadata=None): 173 | return grpc.experimental.unary_unary(request, target, '/demo.UserController/Update', 174 | demo__pb2.User.SerializeToString, 175 | demo__pb2.User.FromString, 176 | options, channel_credentials, 177 | call_credentials, compression, wait_for_ready, timeout, metadata) 178 | 179 | @staticmethod 180 | def Destroy(request, 181 | target, 182 | options=(), 183 | channel_credentials=None, 184 | call_credentials=None, 185 | compression=None, 186 | wait_for_ready=None, 187 | timeout=None, 188 | metadata=None): 189 | return grpc.experimental.unary_unary(request, target, '/demo.UserController/Destroy', 190 | demo__pb2.User.SerializeToString, 191 | google_dot_protobuf_dot_empty__pb2.Empty.FromString, 192 | options, channel_credentials, 193 | call_credentials, compression, wait_for_ready, timeout, metadata) 194 | -------------------------------------------------------------------------------- /examples/quickstart/account_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | import account_pb2 as account__pb2 5 | from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 6 | 7 | 8 | class UserControllerStub(object): 9 | """Missing associated documentation comment in .proto file""" 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.List = channel.unary_stream( 18 | '/account.UserController/List', 19 | request_serializer=account__pb2.UserListRequest.SerializeToString, 20 | response_deserializer=account__pb2.User.FromString, 21 | ) 22 | self.Create = channel.unary_unary( 23 | '/account.UserController/Create', 24 | request_serializer=account__pb2.User.SerializeToString, 25 | response_deserializer=account__pb2.User.FromString, 26 | ) 27 | self.Retrieve = channel.unary_unary( 28 | '/account.UserController/Retrieve', 29 | request_serializer=account__pb2.UserRetrieveRequest.SerializeToString, 30 | response_deserializer=account__pb2.User.FromString, 31 | ) 32 | self.Update = channel.unary_unary( 33 | '/account.UserController/Update', 34 | request_serializer=account__pb2.User.SerializeToString, 35 | response_deserializer=account__pb2.User.FromString, 36 | ) 37 | self.Destroy = channel.unary_unary( 38 | '/account.UserController/Destroy', 39 | request_serializer=account__pb2.User.SerializeToString, 40 | response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, 41 | ) 42 | 43 | 44 | class UserControllerServicer(object): 45 | """Missing associated documentation comment in .proto file""" 46 | 47 | def List(self, request, context): 48 | """Missing associated documentation comment in .proto file""" 49 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 50 | context.set_details('Method not implemented!') 51 | raise NotImplementedError('Method not implemented!') 52 | 53 | def Create(self, request, context): 54 | """Missing associated documentation comment in .proto file""" 55 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 56 | context.set_details('Method not implemented!') 57 | raise NotImplementedError('Method not implemented!') 58 | 59 | def Retrieve(self, request, context): 60 | """Missing associated documentation comment in .proto file""" 61 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 62 | context.set_details('Method not implemented!') 63 | raise NotImplementedError('Method not implemented!') 64 | 65 | def Update(self, request, context): 66 | """Missing associated documentation comment in .proto file""" 67 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 68 | context.set_details('Method not implemented!') 69 | raise NotImplementedError('Method not implemented!') 70 | 71 | def Destroy(self, request, context): 72 | """Missing associated documentation comment in .proto file""" 73 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 74 | context.set_details('Method not implemented!') 75 | raise NotImplementedError('Method not implemented!') 76 | 77 | 78 | def add_UserControllerServicer_to_server(servicer, server): 79 | rpc_method_handlers = { 80 | 'List': grpc.unary_stream_rpc_method_handler( 81 | servicer.List, 82 | request_deserializer=account__pb2.UserListRequest.FromString, 83 | response_serializer=account__pb2.User.SerializeToString, 84 | ), 85 | 'Create': grpc.unary_unary_rpc_method_handler( 86 | servicer.Create, 87 | request_deserializer=account__pb2.User.FromString, 88 | response_serializer=account__pb2.User.SerializeToString, 89 | ), 90 | 'Retrieve': grpc.unary_unary_rpc_method_handler( 91 | servicer.Retrieve, 92 | request_deserializer=account__pb2.UserRetrieveRequest.FromString, 93 | response_serializer=account__pb2.User.SerializeToString, 94 | ), 95 | 'Update': grpc.unary_unary_rpc_method_handler( 96 | servicer.Update, 97 | request_deserializer=account__pb2.User.FromString, 98 | response_serializer=account__pb2.User.SerializeToString, 99 | ), 100 | 'Destroy': grpc.unary_unary_rpc_method_handler( 101 | servicer.Destroy, 102 | request_deserializer=account__pb2.User.FromString, 103 | response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, 104 | ), 105 | } 106 | generic_handler = grpc.method_handlers_generic_handler( 107 | 'account.UserController', rpc_method_handlers) 108 | server.add_generic_rpc_handlers((generic_handler,)) 109 | 110 | 111 | # This class is part of an EXPERIMENTAL API. 112 | class UserController(object): 113 | """Missing associated documentation comment in .proto file""" 114 | 115 | @staticmethod 116 | def List(request, 117 | target, 118 | options=(), 119 | channel_credentials=None, 120 | call_credentials=None, 121 | compression=None, 122 | wait_for_ready=None, 123 | timeout=None, 124 | metadata=None): 125 | return grpc.experimental.unary_stream(request, target, '/account.UserController/List', 126 | account__pb2.UserListRequest.SerializeToString, 127 | account__pb2.User.FromString, 128 | options, channel_credentials, 129 | call_credentials, compression, wait_for_ready, timeout, metadata) 130 | 131 | @staticmethod 132 | def Create(request, 133 | target, 134 | options=(), 135 | channel_credentials=None, 136 | call_credentials=None, 137 | compression=None, 138 | wait_for_ready=None, 139 | timeout=None, 140 | metadata=None): 141 | return grpc.experimental.unary_unary(request, target, '/account.UserController/Create', 142 | account__pb2.User.SerializeToString, 143 | account__pb2.User.FromString, 144 | options, channel_credentials, 145 | call_credentials, compression, wait_for_ready, timeout, metadata) 146 | 147 | @staticmethod 148 | def Retrieve(request, 149 | target, 150 | options=(), 151 | channel_credentials=None, 152 | call_credentials=None, 153 | compression=None, 154 | wait_for_ready=None, 155 | timeout=None, 156 | metadata=None): 157 | return grpc.experimental.unary_unary(request, target, '/account.UserController/Retrieve', 158 | account__pb2.UserRetrieveRequest.SerializeToString, 159 | account__pb2.User.FromString, 160 | options, channel_credentials, 161 | call_credentials, compression, wait_for_ready, timeout, metadata) 162 | 163 | @staticmethod 164 | def Update(request, 165 | target, 166 | options=(), 167 | channel_credentials=None, 168 | call_credentials=None, 169 | compression=None, 170 | wait_for_ready=None, 171 | timeout=None, 172 | metadata=None): 173 | return grpc.experimental.unary_unary(request, target, '/account.UserController/Update', 174 | account__pb2.User.SerializeToString, 175 | account__pb2.User.FromString, 176 | options, channel_credentials, 177 | call_credentials, compression, wait_for_ready, timeout, metadata) 178 | 179 | @staticmethod 180 | def Destroy(request, 181 | target, 182 | options=(), 183 | channel_credentials=None, 184 | call_credentials=None, 185 | compression=None, 186 | wait_for_ready=None, 187 | timeout=None, 188 | metadata=None): 189 | return grpc.experimental.unary_unary(request, target, '/account.UserController/Destroy', 190 | account__pb2.User.SerializeToString, 191 | google_dot_protobuf_dot_empty__pb2.Empty.FromString, 192 | options, channel_credentials, 193 | call_credentials, compression, wait_for_ready, timeout, metadata) 194 | -------------------------------------------------------------------------------- /examples/tutorial/blog_proto/post_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | from blog_proto import post_pb2 as blog__proto_dot_post__pb2 5 | from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 6 | 7 | 8 | class PostControllerStub(object): 9 | """Missing associated documentation comment in .proto file""" 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.List = channel.unary_stream( 18 | '/blog_proto.PostController/List', 19 | request_serializer=blog__proto_dot_post__pb2.PostListRequest.SerializeToString, 20 | response_deserializer=blog__proto_dot_post__pb2.Post.FromString, 21 | ) 22 | self.Create = channel.unary_unary( 23 | '/blog_proto.PostController/Create', 24 | request_serializer=blog__proto_dot_post__pb2.Post.SerializeToString, 25 | response_deserializer=blog__proto_dot_post__pb2.Post.FromString, 26 | ) 27 | self.Retrieve = channel.unary_unary( 28 | '/blog_proto.PostController/Retrieve', 29 | request_serializer=blog__proto_dot_post__pb2.PostRetrieveRequest.SerializeToString, 30 | response_deserializer=blog__proto_dot_post__pb2.Post.FromString, 31 | ) 32 | self.Update = channel.unary_unary( 33 | '/blog_proto.PostController/Update', 34 | request_serializer=blog__proto_dot_post__pb2.Post.SerializeToString, 35 | response_deserializer=blog__proto_dot_post__pb2.Post.FromString, 36 | ) 37 | self.Destroy = channel.unary_unary( 38 | '/blog_proto.PostController/Destroy', 39 | request_serializer=blog__proto_dot_post__pb2.Post.SerializeToString, 40 | response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, 41 | ) 42 | 43 | 44 | class PostControllerServicer(object): 45 | """Missing associated documentation comment in .proto file""" 46 | 47 | def List(self, request, context): 48 | """Missing associated documentation comment in .proto file""" 49 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 50 | context.set_details('Method not implemented!') 51 | raise NotImplementedError('Method not implemented!') 52 | 53 | def Create(self, request, context): 54 | """Missing associated documentation comment in .proto file""" 55 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 56 | context.set_details('Method not implemented!') 57 | raise NotImplementedError('Method not implemented!') 58 | 59 | def Retrieve(self, request, context): 60 | """Missing associated documentation comment in .proto file""" 61 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 62 | context.set_details('Method not implemented!') 63 | raise NotImplementedError('Method not implemented!') 64 | 65 | def Update(self, request, context): 66 | """Missing associated documentation comment in .proto file""" 67 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 68 | context.set_details('Method not implemented!') 69 | raise NotImplementedError('Method not implemented!') 70 | 71 | def Destroy(self, request, context): 72 | """Missing associated documentation comment in .proto file""" 73 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 74 | context.set_details('Method not implemented!') 75 | raise NotImplementedError('Method not implemented!') 76 | 77 | 78 | def add_PostControllerServicer_to_server(servicer, server): 79 | rpc_method_handlers = { 80 | 'List': grpc.unary_stream_rpc_method_handler( 81 | servicer.List, 82 | request_deserializer=blog__proto_dot_post__pb2.PostListRequest.FromString, 83 | response_serializer=blog__proto_dot_post__pb2.Post.SerializeToString, 84 | ), 85 | 'Create': grpc.unary_unary_rpc_method_handler( 86 | servicer.Create, 87 | request_deserializer=blog__proto_dot_post__pb2.Post.FromString, 88 | response_serializer=blog__proto_dot_post__pb2.Post.SerializeToString, 89 | ), 90 | 'Retrieve': grpc.unary_unary_rpc_method_handler( 91 | servicer.Retrieve, 92 | request_deserializer=blog__proto_dot_post__pb2.PostRetrieveRequest.FromString, 93 | response_serializer=blog__proto_dot_post__pb2.Post.SerializeToString, 94 | ), 95 | 'Update': grpc.unary_unary_rpc_method_handler( 96 | servicer.Update, 97 | request_deserializer=blog__proto_dot_post__pb2.Post.FromString, 98 | response_serializer=blog__proto_dot_post__pb2.Post.SerializeToString, 99 | ), 100 | 'Destroy': grpc.unary_unary_rpc_method_handler( 101 | servicer.Destroy, 102 | request_deserializer=blog__proto_dot_post__pb2.Post.FromString, 103 | response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, 104 | ), 105 | } 106 | generic_handler = grpc.method_handlers_generic_handler( 107 | 'blog_proto.PostController', rpc_method_handlers) 108 | server.add_generic_rpc_handlers((generic_handler,)) 109 | 110 | 111 | # This class is part of an EXPERIMENTAL API. 112 | class PostController(object): 113 | """Missing associated documentation comment in .proto file""" 114 | 115 | @staticmethod 116 | def List(request, 117 | target, 118 | options=(), 119 | channel_credentials=None, 120 | call_credentials=None, 121 | compression=None, 122 | wait_for_ready=None, 123 | timeout=None, 124 | metadata=None): 125 | return grpc.experimental.unary_stream(request, target, '/blog_proto.PostController/List', 126 | blog__proto_dot_post__pb2.PostListRequest.SerializeToString, 127 | blog__proto_dot_post__pb2.Post.FromString, 128 | options, channel_credentials, 129 | call_credentials, compression, wait_for_ready, timeout, metadata) 130 | 131 | @staticmethod 132 | def Create(request, 133 | target, 134 | options=(), 135 | channel_credentials=None, 136 | call_credentials=None, 137 | compression=None, 138 | wait_for_ready=None, 139 | timeout=None, 140 | metadata=None): 141 | return grpc.experimental.unary_unary(request, target, '/blog_proto.PostController/Create', 142 | blog__proto_dot_post__pb2.Post.SerializeToString, 143 | blog__proto_dot_post__pb2.Post.FromString, 144 | options, channel_credentials, 145 | call_credentials, compression, wait_for_ready, timeout, metadata) 146 | 147 | @staticmethod 148 | def Retrieve(request, 149 | target, 150 | options=(), 151 | channel_credentials=None, 152 | call_credentials=None, 153 | compression=None, 154 | wait_for_ready=None, 155 | timeout=None, 156 | metadata=None): 157 | return grpc.experimental.unary_unary(request, target, '/blog_proto.PostController/Retrieve', 158 | blog__proto_dot_post__pb2.PostRetrieveRequest.SerializeToString, 159 | blog__proto_dot_post__pb2.Post.FromString, 160 | options, channel_credentials, 161 | call_credentials, compression, wait_for_ready, timeout, metadata) 162 | 163 | @staticmethod 164 | def Update(request, 165 | target, 166 | options=(), 167 | channel_credentials=None, 168 | call_credentials=None, 169 | compression=None, 170 | wait_for_ready=None, 171 | timeout=None, 172 | metadata=None): 173 | return grpc.experimental.unary_unary(request, target, '/blog_proto.PostController/Update', 174 | blog__proto_dot_post__pb2.Post.SerializeToString, 175 | blog__proto_dot_post__pb2.Post.FromString, 176 | options, channel_credentials, 177 | call_credentials, compression, wait_for_ready, timeout, metadata) 178 | 179 | @staticmethod 180 | def Destroy(request, 181 | target, 182 | options=(), 183 | channel_credentials=None, 184 | call_credentials=None, 185 | compression=None, 186 | wait_for_ready=None, 187 | timeout=None, 188 | metadata=None): 189 | return grpc.experimental.unary_unary(request, target, '/blog_proto.PostController/Destroy', 190 | blog__proto_dot_post__pb2.Post.SerializeToString, 191 | google_dot_protobuf_dot_empty__pb2.Empty.FromString, 192 | options, channel_credentials, 193 | call_credentials, compression, wait_for_ready, timeout, metadata) 194 | --------------------------------------------------------------------------------