├── .python-version ├── grpc_python_example ├── __init__.py ├── apis │ ├── __init__.py │ ├── http │ │ ├── controllers │ │ │ ├── __init__.py │ │ │ ├── main.py │ │ │ ├── health.py │ │ │ └── items.py │ │ ├── services.py │ │ ├── wsgi.py │ │ ├── settings.py │ │ ├── exceptions.py │ │ └── __init__.py │ ├── text │ │ ├── __main__.py │ │ ├── cli.py │ │ └── __init__.py │ └── grpc.py └── services │ ├── implementations │ ├── database │ │ ├── alembic │ │ │ ├── README │ │ │ ├── script.py.mako │ │ │ ├── versions │ │ │ │ └── 86998e2c2489_create_and_seed_item_table.py │ │ │ └── env.py │ │ ├── __init__.py │ │ ├── models.py │ │ └── cli.py │ ├── __init__.py │ ├── item_master.py │ └── health.py │ ├── definitions │ ├── __init__.py │ ├── health.proto │ └── items.proto │ ├── stubs │ ├── __init__.py │ ├── health_pb2.py │ └── items_pb2.py │ └── __init__.py ├── requirements ├── common.txt ├── dev.txt ├── grpc.txt ├── http.txt ├── docker.txt └── postgres.txt ├── .gitignore ├── Brewfile ├── img └── grpc_arch_diagram.png ├── docs ├── TODO.md ├── LICENSE.md └── CONTRIBUTING.md ├── .envrc.dev ├── scripts └── run-grpc-api.dev.sh ├── .pre-commit-config.yaml ├── Dockerfile ├── README.md ├── docker-compose.yml ├── Makefile ├── setup.py ├── alembic.ini └── .pylintrc /.python-version: -------------------------------------------------------------------------------- 1 | 3.5.2 2 | -------------------------------------------------------------------------------- /grpc_python_example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpc_python_example/apis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/common.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | build/ 4 | dist/ 5 | *.egg-info/ 6 | .envrc 7 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew 'direnv' 2 | brew 'pre-commit' 3 | brew 'pyenv' 4 | brew 'pyenv-virtualenv' 5 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r docker.txt 2 | pre-commit==0.9.2 3 | pycmd==1.2 4 | pylint==1.6.4 5 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/database/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /img/grpc_arch_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdzhang/grpc-python-example/HEAD/img/grpc_arch_diagram.png -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - Add tests 4 | - Docker mount database volume 5 | - Better logging 6 | - Fix lint violations 7 | -------------------------------------------------------------------------------- /requirements/grpc.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | Cython==0.24.1 3 | grpcio==1.0.1 4 | grpcio-tools==1.0.0 5 | protobuf==3.1.0.post1 6 | -------------------------------------------------------------------------------- /grpc_python_example/apis/text/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from grpc_python_example.apis.text.cli import cli 3 | 4 | cli() 5 | -------------------------------------------------------------------------------- /grpc_python_example/services/definitions/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines grpc_python_example's service interfaces using protocol buffers.""" 2 | -------------------------------------------------------------------------------- /requirements/http.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | gunicorn==19.6.0 3 | healthcheck==1.3.1 4 | Flask==0.11.1 5 | flask-ripozo==1.0.4 6 | ripozo==1.3.0 7 | -------------------------------------------------------------------------------- /requirements/docker.txt: -------------------------------------------------------------------------------- 1 | # Core service and all apis are built into docker container 2 | -r common.txt 3 | -r grpc.txt 4 | -r http.txt 5 | -r postgres.txt 6 | -------------------------------------------------------------------------------- /requirements/postgres.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | alembic==0.8.8 3 | psycopg2==2.6.2 4 | SQLAlchemy==1.1.2 5 | SQLAlchemy-Utils==0.32.9 6 | SQLAlchemy-Wrapper==1.7. 7 | -------------------------------------------------------------------------------- /.envrc.dev: -------------------------------------------------------------------------------- 1 | export DATABASE_URL=postgresql://postgres@localhost:5432/grpc_python_example 2 | export GRPC_API_URL=localhost:50051 3 | export HOST=localhost 4 | export PORT=50051 5 | -------------------------------------------------------------------------------- /grpc_python_example/services/stubs/__init__.py: -------------------------------------------------------------------------------- 1 | """Generated python grpc stubs for grpc_python_example services. 2 | 3 | Service interfaces are defined in grpc_python_example.services.definitions. 4 | 5 | Output by the `make protogen` command.""" 6 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/__init__.py: -------------------------------------------------------------------------------- 1 | from grpc_python_example.services.implementations.health import HealthServicer 2 | from grpc_python_example.services.implementations.item_master import ( 3 | ItemMasterServicer) 4 | 5 | __all__ = ['HealthServicer', 'ItemMasterServicer'] 6 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/database/__init__.py: -------------------------------------------------------------------------------- 1 | """Module that manages database connection.""" 2 | import os 3 | from sqlalchemy_wrapper import SQLAlchemy 4 | 5 | # pylint: disable=invalid-name 6 | # Connect to the database 7 | connection = SQLAlchemy(os.environ['DATABASE_URL']) 8 | -------------------------------------------------------------------------------- /scripts/run-grpc-api.dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | echo 'Creating database...' 5 | python -m grpc_python_example.services.implementations.database.cli create 6 | 7 | echo 'Running database migrations...' 8 | alembic upgrade head 9 | 10 | echo 'Starting grpc api...' 11 | make run-grpc-api 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: local 2 | hooks: 3 | - id: pylint 4 | name: pylint 5 | entry: pylint 6 | # Use system to account for pyenv-virtualenv 7 | # https://github.com/pre-commit/pre-commit/issues/178 8 | language: system 9 | files: \.py$ 10 | args: [] 11 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/controllers/main.py: -------------------------------------------------------------------------------- 1 | """Flask blueprint for / endpoint. 2 | 3 | Exposes the blueprint as a module-level variable named `main`. 4 | """ 5 | from flask import Blueprint, redirect 6 | 7 | # pylint: disable=invalid-name 8 | main = Blueprint('main', __name__) 9 | 10 | 11 | @main.route('/') 12 | def home(): 13 | """Redirects home to health check endpoint.""" 14 | return redirect('/health', code=302) 15 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/services.py: -------------------------------------------------------------------------------- 1 | """Module that setups grpc service connectors. 2 | 3 | Connectors are used throughout grpc_python_example.apis.http.""" 4 | from grpc_python_example.services.stubs import health_pb2, items_pb2 5 | from grpc_python_example.services import GrpcServiceConnector 6 | 7 | # pylint: disable=invalid-name 8 | health_conn = GrpcServiceConnector(health_pb2.HealthStub) 9 | items_conn = GrpcServiceConnector(items_pb2.ItemMasterStub) 10 | -------------------------------------------------------------------------------- /grpc_python_example/services/definitions/health.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package grpc_python_example; 4 | 5 | service Health { 6 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 7 | } 8 | 9 | message HealthCheckRequest { 10 | string service = 1; 11 | } 12 | 13 | message HealthCheckResponse { 14 | enum ServingStatus { 15 | UNKNOWN = 0; 16 | SERVING = 1; 17 | NOT_SERVING = 2; 18 | } 19 | ServingStatus status = 1; 20 | } 21 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/wsgi.py: -------------------------------------------------------------------------------- 1 | """WSGI config for grpc_python_example.apis.http. 2 | 3 | Exposes the WSGI callable as a module-level variable named `app`. 4 | """ 5 | import os 6 | from grpc_python_example.apis.http import create_app 7 | 8 | # pylint: disable=invalid-name 9 | env = os.environ.get('ENV', 'development') 10 | app = create_app('grpc_python_example.apis.http.settings.%sConfig' % env.capitalize()) 11 | 12 | 13 | if __name__ == '__main__': 14 | app.run() 15 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/settings.py: -------------------------------------------------------------------------------- 1 | """Per environment settings for grpc_python_example.apis.http.""" 2 | 3 | # pylint: disable=too-few-public-methods, missing-docstring 4 | 5 | 6 | class Config(object): 7 | DEBUG = True 8 | 9 | 10 | class DevelopmentConfig(Config): 11 | ENV = 'development' 12 | 13 | 14 | class ProductionConfig(Config): 15 | ENV = 'production' 16 | 17 | 18 | class StagingConfig(Config): 19 | ENV = 'staging' 20 | 21 | 22 | class TestConfig(Config): 23 | ENV = 'test' 24 | -------------------------------------------------------------------------------- /grpc_python_example/services/definitions/items.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package grpc_python_example; 4 | 5 | // Manages item data. 6 | service ItemMaster { 7 | // Get an item by id. 8 | rpc GetItem (GetItemRequest) returns (GetItemResponse) {} 9 | } 10 | 11 | message Item { 12 | int32 id = 1; 13 | string code = 2; 14 | string name = 3; 15 | } 16 | 17 | // The request message for [ItemMaster.GetItem]. 18 | message GetItemRequest { 19 | int32 id = 1; 20 | } 21 | 22 | // The response message for [ItemMaster.GetItem]. 23 | message GetItemResponse { 24 | Item item = 1; 25 | } 26 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/database/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | def upgrade(): 19 | ${upgrades if upgrades else "pass"} 20 | 21 | 22 | def downgrade(): 23 | ${downgrades if downgrades else "pass"} 24 | -------------------------------------------------------------------------------- /grpc_python_example/apis/text/cli.py: -------------------------------------------------------------------------------- 1 | """Module with common db cli commands. 2 | 3 | Exposes commands as a click group `basic_cmds`. 4 | """ 5 | 6 | import click 7 | from grpc_python_example.apis.text import TextClient 8 | 9 | __all__ = ['cli'] 10 | 11 | _client = TextClient() 12 | 13 | 14 | @click.command() 15 | def _check_health(): 16 | """Checks client health.""" 17 | click.echo(_client.check_health()) 18 | 19 | 20 | @click.command() 21 | @click.argument('item_id') 22 | def _get_item(item_id): 23 | """Get an item by id.""" 24 | click.echo(_client.get_item(int(item_id))) 25 | 26 | 27 | _cmds = { 28 | 'check_health': _check_health, 29 | 'get_item': _get_item 30 | } 31 | 32 | 33 | class _GrpcPythonExampleTextClientCli(click.MultiCommand): 34 | def list_commands(self, ctx): 35 | return sorted(list(_cmds.keys())) 36 | 37 | def get_command(self, ctx, name): 38 | return _cmds[name] 39 | 40 | 41 | cli = _GrpcPythonExampleTextClientCli(help='Text client commands') 42 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/item_master.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import grpc 3 | from grpc_python_example.services.stubs import items_pb2 4 | from grpc_python_example.services.implementations.database import ( 5 | connection as db, models) 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class ItemMasterServicer(items_pb2.ItemMasterServicer): 11 | """Implements ItemMaster protobuf service interface.""" 12 | 13 | def GetItem(self, request, context): 14 | """Retrieve a item from the database. 15 | 16 | Args: 17 | request: The request value for the RPC. 18 | context (grpc.ServicerContext) 19 | """ 20 | item = db.query(models.Item).get(request.id) 21 | 22 | if item: 23 | item_pb = items_pb2.Item(**item.to_dict()) 24 | return items_pb2.GetItemResponse(item=item_pb) 25 | else: 26 | context.set_code(grpc.StatusCode.NOT_FOUND) 27 | context.set_details('Item with id %s not found' % request.id) 28 | return items_pb2.GetItemResponse() 29 | -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | The MIT License (MIT) 4 | Copyright (c) 2016 Michelle D. Zhang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/database/models.py: -------------------------------------------------------------------------------- 1 | """Module with all models used by grpc_python_example.""" 2 | from sqlalchemy.inspection import inspect as _inspect 3 | from grpc_python_example.services.implementations.database import ( 4 | connection as db) 5 | 6 | 7 | __all__ = ['Item'] 8 | 9 | 10 | class _BaseMixin(object): 11 | """Encapsulating shared functionality of grpc_python_example models.""" 12 | id = db.Column(db.Integer, primary_key=True) 13 | 14 | def to_dict(self): 15 | """Returns model as dict of properties. 16 | 17 | Note: 18 | Removes SQLAlchemy fields included in self.__dict__ 19 | """ 20 | column_names = _inspect(self.__class__).columns.keys() 21 | return {k: self.__dict__[k] for k in column_names} 22 | 23 | 24 | class Item(_BaseMixin, db.Model): 25 | """Represents an item. 26 | 27 | Attributes: 28 | id (int) 29 | name (str): name of item 30 | """ 31 | __tablename__ = 'items' 32 | name = db.Column(db.String) 33 | 34 | def __repr__(self): 35 | return '' % (self.id, self.name) 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | 3 | MAINTAINER Michelle D. Zhang 4 | 5 | # Ensure that Python outputs everything that's printed inside 6 | # the application rather than buffering it. 7 | ENV PYTHONUNBUFFERED 1 8 | 9 | # Add dockerize, which will add a command we can use to wait for 10 | # dependent containers to finish setup (instead of just startup) 11 | RUN apt-get update && apt-get install -y wget 12 | RUN wget https://github.com/jwilder/dockerize/releases/download/v0.1.0/dockerize-linux-amd64-v0.1.0.tar.gz 13 | RUN tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.1.0.tar.gz 14 | 15 | RUN mkdir /opt/grpc_python_example/ 16 | WORKDIR /opt/grpc_python_example/ 17 | 18 | ARG GIT_COMMIT 19 | ENV GIT_COMMIT $GIT_COMMIT 20 | 21 | # Docker caches packages so that this line is only re-run 22 | # when requirements change 23 | RUN mkdir /opt/grpc_python_example/requirements 24 | ADD ./requirements/*.txt /opt/grpc_python_example/requirements/ 25 | RUN pip install -r requirements/dev.txt 26 | 27 | COPY . /opt/grpc_python_example 28 | 29 | # Server and clients are run from same container 30 | # so rely on docker compose to determine command 31 | CMD [] 32 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/database/alembic/versions/86998e2c2489_create_and_seed_item_table.py: -------------------------------------------------------------------------------- 1 | """create and seed item table 2 | 3 | Revision ID: 86998e2c2489 4 | Revises: 5 | Create Date: 2016-11-13 23:15:07.925884 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.sql import table 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '86998e2c2489' 15 | down_revision = None 16 | branch_labels = None 17 | depends_on = None 18 | 19 | def upgrade(): 20 | op.create_table( 21 | 'items', 22 | sa.Column('id', sa.Integer, primary_key=True), 23 | sa.Column('name', sa.String), 24 | ) 25 | 26 | items_table = table( 27 | 'items', 28 | sa.Column('id', sa.Integer, primary_key=True), 29 | sa.Column('name', sa.String), 30 | ) 31 | 32 | op.bulk_insert(items_table, 33 | [ 34 | { 35 | 'id': 1, 36 | 'name': 'Green Eggs' 37 | }, 38 | { 39 | 'id': 2, 40 | 'name': 'Ham' 41 | } 42 | ] 43 | ) 44 | 45 | def downgrade(): 46 | op.drop_table('items') 47 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/database/cli.py: -------------------------------------------------------------------------------- 1 | """Module with common database commands. 2 | 3 | Exposes commands as a click multicommand `cli`. 4 | """ 5 | 6 | import click 7 | from sqlalchemy_utils import database_exists, create_database 8 | from grpc_python_example.services.implementations.database import ( 9 | connection as db) 10 | 11 | 12 | @click.command() 13 | def _create(): 14 | """Create database.""" 15 | if not database_exists(db.engine.url): 16 | create_database(db.engine.url) 17 | 18 | 19 | @click.command() 20 | def _drop_tables(): 21 | """Drop all database tables.""" 22 | db.drop_all() 23 | 24 | 25 | @click.command() 26 | def _create_tables(): 27 | """Create all database tables.""" 28 | db.create_all() 29 | 30 | 31 | _cmds = { 32 | 'create': _create, 33 | 'drop_tables': _drop_tables, 34 | 'create_tables': _create_tables, 35 | } 36 | 37 | 38 | class _GrpcPythonExampleDatabaseCli(click.MultiCommand): 39 | def list_commands(self, ctx): 40 | return sorted(list(_cmds.keys())) 41 | 42 | def get_command(self, ctx, name): 43 | return _cmds[name] 44 | 45 | 46 | cli = _GrpcPythonExampleDatabaseCli(help='Database commands') 47 | 48 | 49 | if __name__ == '__main__': 50 | cli() 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grpc_python_example 2 | 3 | Demos the following: 4 | 5 | * a simple service that can return an `Item` from Postgres database using SQLAlchemy as an ORM, and Alembic for migrations 6 | * the following APIs to access the service: 7 | * grpc 8 | * RESTful, [JSON-API compliant](http://jsonapi.org/format/) API using Flask 9 | * text API with CLI using Click 10 | * example Dockerfile and docker-compose.yml file that dockerizes the service and its apis 11 | 12 | ## Table of Contents 13 | 14 | * [Description](#description) 15 | * [Contributing](docs/CONTRIBUTING.md) 16 | * [Todo](docs/TODO.md) 17 | * [License](docs/LICENSE.md) 18 | 19 | ## Description 20 | 21 | (AFAIK) 22 | 23 | ![grpc architecture diagram](img/grpc_arch_diagram.png) 24 | 25 | ### Pros 26 | 27 | #### gRPC server 28 | 29 | - Fast! Uses HTTP 2 and encodes requests in binary format 30 | 31 | #### Language agnostic stubs 32 | 33 | - Don't have to reimplement same client in multiple languages 34 | - Stubs are just plain old objects/modules so don't have to worry about it not working with one framework or another 35 | - Just have to generate stub from protobuf definitions in an e.g. organization level `.proto` repository 36 | 37 | ### Cons 38 | 39 | - Documentation is lacking 40 | - Few established best practices, conventions, code snippets etc. 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | grpc_api: 4 | command: dockerize -wait tcp://postgres.grpc_python_example.site:5432 ./scripts/run-grpc-api.dev.sh 5 | image: grpc_python_example 6 | depends_on: 7 | - postgres 8 | environment: 9 | # must bind to [::] for port forwarding to work properly 10 | - HOST=[::] 11 | - PORT=50051 12 | - DATABASE_URL=postgresql://postgres@postgres.grpc_python_example.site:5432/grpc_python_example 13 | networks: 14 | default: 15 | aliases: 16 | - grpc_api.grpc_python_example.site 17 | ports: 18 | - "50051:50051" 19 | volumes: 20 | - ./:/opt/grpc_python_example/ 21 | http_api: 22 | command: dockerize -wait tcp://grpc_api.grpc_python_example.site:50051 -timeout 30s make run-http-api 23 | image: grpc_python_example 24 | depends_on: 25 | - grpc_api 26 | environment: 27 | - GRPC_API_URL=grpc_api.grpc_python_example.site:50051 28 | networks: 29 | default: 30 | aliases: 31 | - http_api.grpc_python_example.site 32 | ports: 33 | - "4000:4000" 34 | volumes: 35 | - ./:/opt/grpc_python_example/ 36 | postgres: 37 | image: postgres:9.6 38 | networks: 39 | default: 40 | aliases: 41 | - postgres.grpc_python_example.site 42 | ports: 43 | - "5432:5432" 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_COMMIT=$(shell git rev-parse --verify HEAD) 2 | PROJECT_NAME=grpc_python_example 3 | SERVICE_DEFN_DIR=./$(PROJECT_NAME)/services/definitions 4 | SERVICE_STUB_DIR=./$(PROJECT_NAME)/services/stubs 5 | 6 | .PHONY: build 7 | build: 8 | docker build \ 9 | --build-arg GIT_COMMIT=${GIT_COMMIT} \ 10 | -t $(PROJECT_NAME):latest \ 11 | -t $(PROJECT_NAME):${GIT_COMMIT} \ 12 | . 13 | 14 | .PHONY: clean 15 | clean: 16 | py.cleanup -p . 17 | 18 | .PHONY: down 19 | down: 20 | docker-compose down 21 | 22 | .PHONY: install 23 | install: 24 | pip install -r requirements/dev.txt 25 | 26 | .PHONY: lint 27 | lint: 28 | pre-commit run pylint --all-files 29 | 30 | .PHONY: protogen 31 | protogen: 32 | python -m grpc.tools.protoc \ 33 | -I=$(SERVICE_DEFN_DIR) \ 34 | --python_out=$(SERVICE_STUB_DIR) \ 35 | --grpc_python_out=$(SERVICE_STUB_DIR) \ 36 | $(SERVICE_DEFN_DIR)/items.proto $(SERVICE_DEFN_DIR)/health.proto 37 | 38 | # Usage: make run-text-api ARGS="check_health" 39 | # make run-text-api ARGS="get_item 1" 40 | .PHONY: run-text-api 41 | run-text-api: 42 | python -m $(PROJECT_NAME).apis.text $(ARGS) 43 | 44 | .PHONY: run-http-api 45 | run-http-api: 46 | gunicorn -b 0.0.0.0:4000 $(PROJECT_NAME).apis.http.wsgi:app 47 | 48 | .PHONY: run-grpc-api 49 | run-grpc-api: 50 | python -m $(PROJECT_NAME).apis.grpc 51 | 52 | .PHONY: up 53 | up: 54 | docker-compose up -d 55 | -------------------------------------------------------------------------------- /grpc_python_example/services/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import grpc 3 | 4 | 5 | class GrpcServiceConnector(object): 6 | """Provides a simple wrapper around grpc service stubs. 7 | 8 | Args: 9 | service_class: stub class generated by grpc.tools.protoc 10 | 11 | Attributes: 12 | stub: an instance of self.service_class that has been connected to 13 | a grpc server 14 | 15 | Usage: 16 | GrpcServiceConnector(health_pb2.HealthStub).start() 17 | """ 18 | 19 | def __init__(self, service_class): 20 | self._grpc_api_address = os.environ['GRPC_API_URL'] 21 | self._channel = None 22 | self._stub = None 23 | self._service_class = service_class 24 | 25 | def start(self): 26 | """Connect to the grpc service corresponding to self.service_class. 27 | 28 | Note: self._stub will be created even if the grpc server is down, 29 | in which case any RPC calls will return with a 30 | grpc.StatusCode.UNAVAILABLE. 31 | """ 32 | self._channel = grpc.insecure_channel(self._grpc_api_address) 33 | self._stub = self._service_class(self._channel) 34 | 35 | @property 36 | def stub(self): 37 | """Getter for self._stub.""" 38 | if self._stub is None: 39 | service_class_name = self._service_class.__name__ 40 | raise AttributeError("stub '%s' is empty" % service_class_name) 41 | 42 | return self._stub 43 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/health.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import grpc 3 | from grpc_python_example.services.stubs import health_pb2 4 | 5 | 6 | class HealthServicer(health_pb2.HealthServicer): 7 | """Implements Health protobuf service interface. 8 | 9 | Notes: 10 | Taken from https://github.com/grpc/grpc/tree/master/src/python/grpcio_health_checking 11 | SHA: d953959e2bafd645b6ed674861a310daba5f80ae 12 | because of slow package setup speed 13 | """ 14 | 15 | def __init__(self): 16 | self._server_status_lock = threading.Lock() 17 | self._server_status = {} 18 | 19 | def Check(self, request, context): 20 | with self._server_status_lock: 21 | status = self._server_status.get(request.service) 22 | if status is None: 23 | context.set_code(grpc.StatusCode.NOT_FOUND) 24 | return health_pb2.HealthCheckResponse() 25 | else: 26 | return health_pb2.HealthCheckResponse(status=status) 27 | 28 | def set(self, service, status): 29 | """Sets the status of a service. 30 | Args: 31 | service: string, the name of the service. 32 | NOTE, '' must be set. 33 | status: HealthCheckResponse.status enum value indicating 34 | the status of the service 35 | """ 36 | with self._server_status_lock: 37 | self._server_status[service] = status 38 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/controllers/health.py: -------------------------------------------------------------------------------- 1 | """Flask blueprint for /health endpoint. 2 | 3 | Exposes the blueprint as a module-level variable named `health`. 4 | """ 5 | import json 6 | import grpc 7 | from flask import Blueprint 8 | from ripozo.adapters import JSONAPIAdapter 9 | from ripozo.exceptions import RestException 10 | from healthcheck import HealthCheck 11 | from grpc_python_example.services.stubs import health_pb2 12 | from grpc_python_example.apis.http.services import health_conn 13 | 14 | # pylint: disable=invalid-name 15 | health = Blueprint('health', __name__) 16 | 17 | 18 | def _grpc_available(): 19 | """Checks that the http client is connected and that the grpc server 20 | is available. 21 | 22 | Returns: 23 | A tuple of (bool, str or dict) where bool indicates whether the check 24 | passed, and the str or dict provides extra context about the check 25 | """ 26 | try: 27 | resp = health_conn.stub.Check(health_pb2.HealthCheckRequest()) 28 | check_passed = resp.status == health_pb2.HealthCheckResponse.SERVING 29 | return check_passed, 'grpc server OK' 30 | except grpc._channel._Rendezvous as exc: # pylint: disable=protected-access 31 | return False, {'code': str(exc.code())} 32 | except RestException as exc: 33 | response, _, _ = JSONAPIAdapter.format_exception(exc) 34 | return False, json.loads(response) 35 | 36 | 37 | _health_check = HealthCheck(health, '/health') 38 | _health_check.add_check(_grpc_available) 39 | -------------------------------------------------------------------------------- /grpc_python_example/apis/grpc.py: -------------------------------------------------------------------------------- 1 | """grpc server.""" 2 | 3 | import os 4 | import time 5 | import logging 6 | from concurrent import futures 7 | import grpc 8 | from grpc_python_example.services.stubs import items_pb2, health_pb2 9 | from grpc_python_example.services.implementations import ( 10 | HealthServicer, ItemMasterServicer) 11 | 12 | _ONE_DAY_IN_SECONDS = 60 * 60 * 24 13 | logging.basicConfig(level=logging.INFO) 14 | 15 | 16 | def serve(): 17 | """Start grpc server servicing FMS RPCs.""" 18 | # create grpc server 19 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 20 | 21 | # add services 22 | health_servicer = HealthServicer() 23 | item_master_servicer = ItemMasterServicer() 24 | 25 | items_pb2.add_ItemMasterServicer_to_server(item_master_servicer, 26 | server) 27 | health_pb2.add_HealthServicer_to_server(health_servicer, server) 28 | 29 | # start server 30 | address = '%s:%s' % (os.environ['HOST'], os.environ['PORT']) 31 | logging.info('Starting grpc server at %s', address) 32 | 33 | server.add_insecure_port(address) 34 | server.start() 35 | 36 | # mark server as healthy 37 | health_servicer.set('', health_pb2.HealthCheckResponse.SERVING) 38 | logging.info('grpc listening at %s', address) 39 | 40 | # start() does not block so sleep-loop 41 | try: 42 | while True: 43 | time.sleep(_ONE_DAY_IN_SECONDS) 44 | except KeyboardInterrupt: 45 | server.stop(0) 46 | 47 | 48 | if __name__ == '__main__': 49 | serve() 50 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/exceptions.py: -------------------------------------------------------------------------------- 1 | """Module with exceptions that grpc_python_example.apis.http explicitly uses.""" 2 | from ripozo.exceptions import RestException 3 | 4 | 5 | class GrpcServiceNotConnectedException(RestException): 6 | """Raised when trying to call methods on a grpc service stub 7 | that is not connected on either the HTTP client or grpc server side. 8 | """ 9 | def __init__(self, message=None, status_code='500', *args, **kwargs): 10 | super(GrpcServiceNotConnectedException, self).__init__(message, 11 | *args, 12 | **kwargs) 13 | self.status_code = status_code 14 | 15 | 16 | class UnauthorizedException(RestException): 17 | """Raised when authorizing a request fails.""" 18 | def __init__(self, message=None, status_code='401', *args, **kwargs): 19 | super(UnauthorizedException, self).__init__(message, *args, **kwargs) 20 | self.status_code = status_code 21 | 22 | 23 | class NotFoundException(RestException): 24 | """Raised when a requested resource does not exist.""" 25 | def __init__(self, message=None, status_code='404', *args, **kwargs): 26 | super(NotFoundException, self).__init__(message, *args, **kwargs) 27 | self.status_code = status_code 28 | 29 | 30 | class InternalServerException(RestException): 31 | """Raised when any unhandled exception arises.""" 32 | def __init__(self, message=None, status_code='500', *args, **kwargs): 33 | super(InternalServerException, self).__init__(message, *args, **kwargs) 34 | self.status_code = status_code 35 | -------------------------------------------------------------------------------- /grpc_python_example/apis/text/__init__.py: -------------------------------------------------------------------------------- 1 | """Module with text client for grpc_python_example's grpc server.""" 2 | 3 | from google.protobuf import text_format 4 | from grpc_python_example.services.stubs import health_pb2, items_pb2 5 | from grpc_python_example.services import GrpcServiceConnector 6 | 7 | 8 | class TextClient(object): 9 | """A text client for grpc_python_example's grpc server.""" 10 | def __init__(self): 11 | self.health_conn = GrpcServiceConnector(health_pb2.HealthStub) 12 | self.items_master_conn = GrpcServiceConnector(items_pb2.ItemMasterStub) 13 | 14 | self.health_conn.start() 15 | self.items_master_conn.start() 16 | 17 | def check_health(self): 18 | """Checks that the text client is connected and that the grpc server 19 | is available. 20 | 21 | Raises: 22 | grpc._channel._Rendezvous: When grpc server is down 23 | 24 | Returns: 25 | str: True or False depending on connection state 26 | """ 27 | res = self.health_conn.stub.Check(health_pb2.HealthCheckRequest()) 28 | return str(res.status == health_pb2.HealthCheckResponse.SERVING) 29 | 30 | def get_item(self, item_id): 31 | """Retrieve a item. 32 | 33 | Args: 34 | item_id (int): id of item to retrieve 35 | 36 | Raises: 37 | grpc._channel._Rendezvous: When grpc server is down 38 | 39 | Returns: 40 | str: True or False depending on connection state 41 | """ 42 | req = items_pb2.GetItemRequest(id=item_id) 43 | res = self.items_master_conn.stub.GetItem(req) 44 | return text_format.MessageToString(res) 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import os.path 4 | import sys 5 | from setuptools import setup, find_packages 6 | from pip.req import parse_requirements 7 | 8 | VERSION = '0.1.0' 9 | 10 | ROOT = os.path.realpath(os.path.join(os.path.dirname( 11 | sys.modules['__main__'].__file__))) 12 | 13 | 14 | def get_requirements(extra='common'): 15 | """Get requirements for given extra. 16 | 17 | Args: 18 | extra (str): name of file without extension in requirements/ 19 | 20 | Returns: 21 | (list): list of strings, each formatted as pip install compatible 22 | requirement specifier. 23 | """ 24 | file_name = '%s.txt' % extra 25 | requirements_path = os.path.join(ROOT, 'requirements', file_name) 26 | pip_reqs = parse_requirements(requirements_path, session='hack') 27 | reqs = [str(r.req) for r in pip_reqs] 28 | return reqs 29 | 30 | 31 | setup( 32 | name='grpc_python_example', 33 | version=VERSION, 34 | author='Michelle D. Zhang', 35 | author_email='zhang.michelle.d@gmail.com', 36 | url='', 37 | description='A grpc python example.', 38 | long_description=open(os.path.join(ROOT, 'README.md')).read(), 39 | packages=find_packages(), 40 | install_requires=get_requirements(), 41 | extras_require={ 42 | 'dev': get_requirements('dev'), 43 | 'docker': get_requirements('docker'), 44 | 'grpc': get_requirements('grpc'), 45 | 'http': get_requirements('http'), 46 | 'postgres': get_requirements('postgres'), 47 | }, 48 | license='MIT', 49 | classifiers=[ 50 | 'Intended Audience :: Developers', 51 | 'Operating System :: MacOS :: MacOS X', 52 | 'Programming Language :: Python :: 3.5', 53 | 'Topic :: Software Development' 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = grpc_python_example/services/implementations/database/alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # max length of characters to apply to the 11 | # "slug" field 12 | #truncate_slug_length = 40 13 | 14 | # set to 'true' to run the environment during 15 | # the 'revision' command, regardless of autogenerate 16 | # revision_environment = false 17 | 18 | # set to 'true' to allow .pyc and .pyo files without 19 | # a source .py file to be detected as revisions in the 20 | # versions/ directory 21 | # sourceless = false 22 | 23 | # version location specification; this defaults 24 | # to alembic/versions. When using multiple version 25 | # directories, initial revisions must be specified with --version-path 26 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 27 | 28 | # the output encoding used when revision files 29 | # are written from script.py.mako 30 | # output_encoding = utf-8 31 | 32 | # sqlalchemy.url = driver://user:pass@localhost/dbname 33 | 34 | 35 | # Logging configuration 36 | [loggers] 37 | keys = root,sqlalchemy,alembic 38 | 39 | [handlers] 40 | keys = console 41 | 42 | [formatters] 43 | keys = generic 44 | 45 | [logger_root] 46 | level = WARN 47 | handlers = console 48 | qualname = 49 | 50 | [logger_sqlalchemy] 51 | level = WARN 52 | handlers = 53 | qualname = sqlalchemy.engine 54 | 55 | [logger_alembic] 56 | level = INFO 57 | handlers = 58 | qualname = alembic 59 | 60 | [handler_console] 61 | class = StreamHandler 62 | args = (sys.stderr,) 63 | level = NOTSET 64 | formatter = generic 65 | 66 | [formatter_generic] 67 | format = %(levelname)-5.5s [%(name)s] %(message)s 68 | datefmt = %H:%M:%S 69 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """RESTful, JSON-API compliant wrapper around grpc_python_example' services. 4 | 5 | Interfaces with grpc stubs which in turn interface with core service logic. 6 | """ 7 | import grpc 8 | from flask import Flask, Response 9 | from ripozo.adapters import JSONAPIAdapter 10 | from ripozo.exceptions import RestException 11 | from grpc_python_example.apis.http.controllers.main import main 12 | from grpc_python_example.apis.http.controllers.health import health 13 | from grpc_python_example.apis.http.controllers.items import items 14 | from grpc_python_example.apis.http.services import health_conn, items_conn 15 | from grpc_python_example.apis.http.exceptions import ( 16 | InternalServerException, NotFoundException) 17 | 18 | 19 | def create_app(object_name): 20 | """A flask application factory for grpc_python_example. 21 | Args: 22 | object_name: the python path of the config object, 23 | e.g. grpc_python_example.apis.http.settings.DevelopmentConfig 24 | """ 25 | app = Flask(__name__) 26 | 27 | app.config.from_object(object_name) 28 | 29 | # initialize connection to grpc services 30 | health_conn.start() 31 | items_conn.start() 32 | 33 | # register our blueprints 34 | app.register_blueprint(main) 35 | app.register_blueprint(health) 36 | app.register_blueprint(items) 37 | 38 | # pylint: disable=unused-variable 39 | @app.errorhandler(Exception) 40 | def all_exception_handler(exc): 41 | """Default, JSON-API compliant exception handler.""" 42 | # pylint: disable=redefined-variable-type 43 | if isinstance(exc, RestException): 44 | rest_exc = exc 45 | elif (isinstance(exc, grpc._channel._Rendezvous) # pylint: disable=protected-access 46 | and exc.code() == grpc.StatusCode.NOT_FOUND): 47 | rest_exc = NotFoundException(message=exc.details()) 48 | else: 49 | rest_exc = InternalServerException(message=str(exc)) 50 | 51 | response, content_type, status_code = \ 52 | JSONAPIAdapter.format_exception(rest_exc) 53 | 54 | return Response(response=response, 55 | content_type=content_type, 56 | status=status_code) 57 | 58 | return app 59 | -------------------------------------------------------------------------------- /grpc_python_example/services/implementations/database/alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import os 3 | from alembic import context 4 | from sqlalchemy import create_engine 5 | from logging.config import fileConfig 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | 15 | # add your model's MetaData object here 16 | # for 'autogenerate' support 17 | # from myapp import mymodel 18 | # target_metadata = mymodel.Base.metadata 19 | target_metadata = None 20 | 21 | # other values from the config, defined by the needs of env.py, 22 | # can be acquired: 23 | # my_important_option = config.get_main_option("my_important_option") 24 | # ... etc. 25 | 26 | 27 | def get_database_url(): 28 | """Get database url from environment variable. 29 | Replaces alembic's default of using `sqlalchemy.url` in alembic.ini. 30 | """ 31 | return os.environ['DATABASE_URL'] 32 | 33 | 34 | def run_migrations_offline(): 35 | """Run migrations in 'offline' mode. 36 | 37 | This configures the context with just a URL 38 | and not an Engine, though an Engine is acceptable 39 | here as well. By skipping the Engine creation 40 | we don't even need a DBAPI to be available. 41 | 42 | Calls to context.execute() here emit the given string to the 43 | script output. 44 | 45 | """ 46 | url = get_database_url() 47 | context.configure( 48 | url=url, target_metadata=target_metadata, literal_binds=True) 49 | 50 | with context.begin_transaction(): 51 | context.run_migrations() 52 | 53 | 54 | def run_migrations_online(): 55 | """Run migrations in 'online' mode. 56 | 57 | In this scenario we need to create an Engine 58 | and associate a connection with the context. 59 | 60 | """ 61 | connectable = create_engine(get_database_url()) 62 | 63 | with connectable.connect() as connection: 64 | context.configure( 65 | connection=connection, 66 | target_metadata=target_metadata 67 | ) 68 | 69 | with context.begin_transaction(): 70 | context.run_migrations() 71 | 72 | if context.is_offline_mode(): 73 | run_migrations_offline() 74 | else: 75 | run_migrations_online() 76 | -------------------------------------------------------------------------------- /grpc_python_example/apis/http/controllers/items.py: -------------------------------------------------------------------------------- 1 | """Flask blueprint for /items* endpoint. 2 | 3 | Exposes the blueprint as a module-level variable named `main`. 4 | """ 5 | from flask import Blueprint 6 | from flask_ripozo import FlaskDispatcher 7 | from ripozo import restmixins 8 | from ripozo.manager_base import BaseManager 9 | from ripozo.adapters import JSONAPIAdapter 10 | from ripozo.resources.fields.common import StringField, IntegerField 11 | from grpc_python_example.services.stubs import items_pb2 12 | from grpc_python_example.apis.http.services import items_conn 13 | 14 | # pylint: disable=invalid-name 15 | items = Blueprint('items', __name__) 16 | 17 | 18 | class ItemManager(BaseManager): 19 | """Manager that interops between ripozo and grpc. 20 | 21 | Provides convience functions primarily for basic CRUD. 22 | """ 23 | fields = ('id', 'name',) 24 | field_validators = [ 25 | IntegerField('id', required=True), 26 | StringField('name', required=True), 27 | ] 28 | 29 | def create(self, values, *args, **kwargs): 30 | raise NotImplementedError() 31 | 32 | def delete(self, lookup_keys, *args, **kwargs): 33 | raise NotImplementedError() 34 | 35 | def retrieve(self, lookup_keys, *args, **kwargs): 36 | """Retrieve a single item and nothing more as a dictionary. 37 | 38 | Args: 39 | lookup_keys (dict): Taken from url_params on flask request. Used to 40 | lookup a item and its associated values. 41 | 42 | Returns: 43 | dict or None: Properties of the retrieved item or None if no 44 | such item found. 45 | """ 46 | resp = items_conn.stub.GetItem( 47 | items_pb2.GetItemRequest(id=lookup_keys['id'])) 48 | item = resp.item 49 | 50 | if item: 51 | return dict(id=item.id, name=item.name) 52 | 53 | def retrieve_list(self, filters, *args, **kwargs): 54 | raise NotImplementedError() 55 | 56 | def update(self, lookup_keys, updates, *args, **kwargs): 57 | raise NotImplementedError() 58 | 59 | 60 | class ItemResource(restmixins.Retrieve): 61 | """Standard ripozo resource that can be used by ripozo adapters. Handles 62 | requests and constructs resources to return as a request. 63 | """ 64 | manager = ItemManager() 65 | pks = 'id', 66 | resource_name = 'items' 67 | 68 | 69 | # register resources and valid response types 70 | _dispatcher = FlaskDispatcher(items) 71 | _dispatcher.register_adapters(JSONAPIAdapter) 72 | _dispatcher.register_resources(ItemResource) 73 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Table of Contents 4 | 5 | * [Requirements](#requirements) 6 | * [Installation](#installation) 7 | * [Running](#running) 8 | * [Testing](#testing) 9 | * [Style](#style) 10 | 11 | ## Requirements 12 | 13 | 1. [Homebrew](http://brew.sh) for managing software packages on OS X 14 | ``` 15 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 16 | ``` 17 | 18 | 2. [git](https://git-scm.com) for version control 19 | ``` 20 | brew install git 21 | ``` 22 | 23 | 3. [Homebrew Bundle](https://github.com/Homebrew/homebrew-bundle) for bundling packages with Homebrew 24 | ``` 25 | brew tap Homebrew/bundle 26 | ``` 27 | 28 | 4. [Docker for Mac](https://docs.docker.com/docker-for-mac/) 29 | 30 | ## Installation 31 | 32 | 1. Clone this repo 33 | ``` 34 | git clone git@github.com:mdzhang/grpc-python-example.git 35 | cd grpc-python-example 36 | ``` 37 | 38 | 2. Install Homebrew packages 39 | ``` 40 | brew bundle 41 | ``` 42 | 43 | 3. Add `pyenv init` to your shell to enable shims and autocompletion 44 | ``` 45 | # ~/.bashrc 46 | 47 | if which pyenv > /dev/null; then 48 | eval "$(pyenv init -)" 49 | fi 50 | 51 | if which pyenv-virtualenv > /dev/null; then 52 | eval "$(pyenv virtualenv-init -)"; 53 | export PYENV_VIRTUALENV_DISABLE_PROMPT=1 54 | fi 55 | 56 | if which direnv > /dev/null; then 57 | eval "$(direnv hook bash)" 58 | fi 59 | ``` 60 | 61 | 4. Restart shell so that changes take effect 62 | ``` 63 | source ~/.bashrc 64 | ``` 65 | 66 | 3. Install Python 67 | ``` 68 | pyenv install -s $(cat ./.python-version) 69 | ``` 70 | 71 | 4. Create virtual environment 72 | ``` 73 | pyenv virtualenv grpc_python_example 74 | pyenv activate grpc_python_example 75 | ``` 76 | 77 | 5. Install Python packages 78 | ``` 79 | make install 80 | ``` 81 | 82 | 6. Install git hooks 83 | ``` 84 | pre-commit install 85 | ``` 86 | 87 | 7. Install development environment variables 88 | ``` 89 | cp .envrc.dev .envrc 90 | direnv allow 91 | ``` 92 | 93 | ## Running 94 | 95 | 1. Build the project containers at least once 96 | ``` 97 | make build 98 | ``` 99 | 100 | 2. Bring up database, grpc and http apis as docker containers 101 | ``` 102 | make up 103 | ``` 104 | 105 | 3. Run text api from host 106 | ``` 107 | make run-text-api ARGS="check_health" 108 | ``` 109 | 110 | 4. View http api from browser 111 | ``` 112 | open http://localhost:4000 113 | ``` 114 | 115 | ## Testing 116 | 117 | TODO 118 | 119 | ## Style 120 | 121 | * Must pass `pylint` 122 | * [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) 123 | * [Hitchhiker's Guide to Python](http://docs.python-guide.org/en/latest/) 124 | -------------------------------------------------------------------------------- /grpc_python_example/services/stubs/health_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: health.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='health.proto', 20 | package='grpc_python_example', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x0chealth.proto\x12\x13grpc_python_example\"%\n\x12HealthCheckRequest\x12\x0f\n\x07service\x18\x01 \x01(\t\"\x99\x01\n\x13HealthCheckResponse\x12\x46\n\x06status\x18\x01 \x01(\x0e\x32\x36.grpc_python_example.HealthCheckResponse.ServingStatus\":\n\rServingStatus\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07SERVING\x10\x01\x12\x0f\n\x0bNOT_SERVING\x10\x02\x32\x64\n\x06Health\x12Z\n\x05\x43heck\x12\'.grpc_python_example.HealthCheckRequest\x1a(.grpc_python_example.HealthCheckResponseb\x06proto3') 23 | ) 24 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 25 | 26 | 27 | 28 | _HEALTHCHECKRESPONSE_SERVINGSTATUS = _descriptor.EnumDescriptor( 29 | name='ServingStatus', 30 | full_name='grpc_python_example.HealthCheckResponse.ServingStatus', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | values=[ 34 | _descriptor.EnumValueDescriptor( 35 | name='UNKNOWN', index=0, number=0, 36 | options=None, 37 | type=None), 38 | _descriptor.EnumValueDescriptor( 39 | name='SERVING', index=1, number=1, 40 | options=None, 41 | type=None), 42 | _descriptor.EnumValueDescriptor( 43 | name='NOT_SERVING', index=2, number=2, 44 | options=None, 45 | type=None), 46 | ], 47 | containing_type=None, 48 | options=None, 49 | serialized_start=172, 50 | serialized_end=230, 51 | ) 52 | _sym_db.RegisterEnumDescriptor(_HEALTHCHECKRESPONSE_SERVINGSTATUS) 53 | 54 | 55 | _HEALTHCHECKREQUEST = _descriptor.Descriptor( 56 | name='HealthCheckRequest', 57 | full_name='grpc_python_example.HealthCheckRequest', 58 | filename=None, 59 | file=DESCRIPTOR, 60 | containing_type=None, 61 | fields=[ 62 | _descriptor.FieldDescriptor( 63 | name='service', full_name='grpc_python_example.HealthCheckRequest.service', index=0, 64 | number=1, type=9, cpp_type=9, label=1, 65 | has_default_value=False, default_value=_b("").decode('utf-8'), 66 | message_type=None, enum_type=None, containing_type=None, 67 | is_extension=False, extension_scope=None, 68 | options=None), 69 | ], 70 | extensions=[ 71 | ], 72 | nested_types=[], 73 | enum_types=[ 74 | ], 75 | options=None, 76 | is_extendable=False, 77 | syntax='proto3', 78 | extension_ranges=[], 79 | oneofs=[ 80 | ], 81 | serialized_start=37, 82 | serialized_end=74, 83 | ) 84 | 85 | 86 | _HEALTHCHECKRESPONSE = _descriptor.Descriptor( 87 | name='HealthCheckResponse', 88 | full_name='grpc_python_example.HealthCheckResponse', 89 | filename=None, 90 | file=DESCRIPTOR, 91 | containing_type=None, 92 | fields=[ 93 | _descriptor.FieldDescriptor( 94 | name='status', full_name='grpc_python_example.HealthCheckResponse.status', index=0, 95 | number=1, type=14, cpp_type=8, label=1, 96 | has_default_value=False, default_value=0, 97 | message_type=None, enum_type=None, containing_type=None, 98 | is_extension=False, extension_scope=None, 99 | options=None), 100 | ], 101 | extensions=[ 102 | ], 103 | nested_types=[], 104 | enum_types=[ 105 | _HEALTHCHECKRESPONSE_SERVINGSTATUS, 106 | ], 107 | options=None, 108 | is_extendable=False, 109 | syntax='proto3', 110 | extension_ranges=[], 111 | oneofs=[ 112 | ], 113 | serialized_start=77, 114 | serialized_end=230, 115 | ) 116 | 117 | _HEALTHCHECKRESPONSE.fields_by_name['status'].enum_type = _HEALTHCHECKRESPONSE_SERVINGSTATUS 118 | _HEALTHCHECKRESPONSE_SERVINGSTATUS.containing_type = _HEALTHCHECKRESPONSE 119 | DESCRIPTOR.message_types_by_name['HealthCheckRequest'] = _HEALTHCHECKREQUEST 120 | DESCRIPTOR.message_types_by_name['HealthCheckResponse'] = _HEALTHCHECKRESPONSE 121 | 122 | HealthCheckRequest = _reflection.GeneratedProtocolMessageType('HealthCheckRequest', (_message.Message,), dict( 123 | DESCRIPTOR = _HEALTHCHECKREQUEST, 124 | __module__ = 'health_pb2' 125 | # @@protoc_insertion_point(class_scope:grpc_python_example.HealthCheckRequest) 126 | )) 127 | _sym_db.RegisterMessage(HealthCheckRequest) 128 | 129 | HealthCheckResponse = _reflection.GeneratedProtocolMessageType('HealthCheckResponse', (_message.Message,), dict( 130 | DESCRIPTOR = _HEALTHCHECKRESPONSE, 131 | __module__ = 'health_pb2' 132 | # @@protoc_insertion_point(class_scope:grpc_python_example.HealthCheckResponse) 133 | )) 134 | _sym_db.RegisterMessage(HealthCheckResponse) 135 | 136 | 137 | import grpc 138 | from grpc.beta import implementations as beta_implementations 139 | from grpc.beta import interfaces as beta_interfaces 140 | from grpc.framework.common import cardinality 141 | from grpc.framework.interfaces.face import utilities as face_utilities 142 | 143 | 144 | class HealthStub(object): 145 | 146 | def __init__(self, channel): 147 | """Constructor. 148 | 149 | Args: 150 | channel: A grpc.Channel. 151 | """ 152 | self.Check = channel.unary_unary( 153 | '/grpc_python_example.Health/Check', 154 | request_serializer=HealthCheckRequest.SerializeToString, 155 | response_deserializer=HealthCheckResponse.FromString, 156 | ) 157 | 158 | 159 | class HealthServicer(object): 160 | 161 | def Check(self, request, context): 162 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 163 | context.set_details('Method not implemented!') 164 | raise NotImplementedError('Method not implemented!') 165 | 166 | 167 | def add_HealthServicer_to_server(servicer, server): 168 | rpc_method_handlers = { 169 | 'Check': grpc.unary_unary_rpc_method_handler( 170 | servicer.Check, 171 | request_deserializer=HealthCheckRequest.FromString, 172 | response_serializer=HealthCheckResponse.SerializeToString, 173 | ), 174 | } 175 | generic_handler = grpc.method_handlers_generic_handler( 176 | 'grpc_python_example.Health', rpc_method_handlers) 177 | server.add_generic_rpc_handlers((generic_handler,)) 178 | 179 | 180 | class BetaHealthServicer(object): 181 | def Check(self, request, context): 182 | context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) 183 | 184 | 185 | class BetaHealthStub(object): 186 | def Check(self, request, timeout, metadata=None, with_call=False, protocol_options=None): 187 | raise NotImplementedError() 188 | Check.future = None 189 | 190 | 191 | def beta_create_Health_server(servicer, pool=None, pool_size=None, default_timeout=None, maximum_timeout=None): 192 | request_deserializers = { 193 | ('grpc_python_example.Health', 'Check'): HealthCheckRequest.FromString, 194 | } 195 | response_serializers = { 196 | ('grpc_python_example.Health', 'Check'): HealthCheckResponse.SerializeToString, 197 | } 198 | method_implementations = { 199 | ('grpc_python_example.Health', 'Check'): face_utilities.unary_unary_inline(servicer.Check), 200 | } 201 | server_options = beta_implementations.server_options(request_deserializers=request_deserializers, response_serializers=response_serializers, thread_pool=pool, thread_pool_size=pool_size, default_timeout=default_timeout, maximum_timeout=maximum_timeout) 202 | return beta_implementations.server(method_implementations, options=server_options) 203 | 204 | 205 | def beta_create_Health_stub(channel, host=None, metadata_transformer=None, pool=None, pool_size=None): 206 | request_serializers = { 207 | ('grpc_python_example.Health', 'Check'): HealthCheckRequest.SerializeToString, 208 | } 209 | response_deserializers = { 210 | ('grpc_python_example.Health', 'Check'): HealthCheckResponse.FromString, 211 | } 212 | cardinalities = { 213 | 'Check': cardinality.Cardinality.UNARY_UNARY, 214 | } 215 | stub_options = beta_implementations.stub_options(host=host, metadata_transformer=metadata_transformer, request_serializers=request_serializers, response_deserializers=response_deserializers, thread_pool=pool, thread_pool_size=pool_size) 216 | return beta_implementations.dynamic_stub(channel, 'grpc_python_example.Health', cardinalities, options=stub_options) 217 | # @@protoc_insertion_point(module_scope) 218 | -------------------------------------------------------------------------------- /grpc_python_example/services/stubs/items_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: items.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='items.proto', 20 | package='grpc_python_example', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x0bitems.proto\x12\x13grpc_python_example\".\n\x04Item\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\"\x1c\n\x0eGetItemRequest\x12\n\n\x02id\x18\x01 \x01(\x05\":\n\x0fGetItemResponse\x12\'\n\x04item\x18\x01 \x01(\x0b\x32\x19.grpc_python_example.Item2d\n\nItemMaster\x12V\n\x07GetItem\x12#.grpc_python_example.GetItemRequest\x1a$.grpc_python_example.GetItemResponse\"\x00\x62\x06proto3') 23 | ) 24 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 25 | 26 | 27 | 28 | 29 | _ITEM = _descriptor.Descriptor( 30 | name='Item', 31 | full_name='grpc_python_example.Item', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='id', full_name='grpc_python_example.Item.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 | options=None), 43 | _descriptor.FieldDescriptor( 44 | name='code', full_name='grpc_python_example.Item.code', 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 | options=None), 50 | _descriptor.FieldDescriptor( 51 | name='name', full_name='grpc_python_example.Item.name', 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 | options=None), 57 | ], 58 | extensions=[ 59 | ], 60 | nested_types=[], 61 | enum_types=[ 62 | ], 63 | options=None, 64 | is_extendable=False, 65 | syntax='proto3', 66 | extension_ranges=[], 67 | oneofs=[ 68 | ], 69 | serialized_start=36, 70 | serialized_end=82, 71 | ) 72 | 73 | 74 | _GETITEMREQUEST = _descriptor.Descriptor( 75 | name='GetItemRequest', 76 | full_name='grpc_python_example.GetItemRequest', 77 | filename=None, 78 | file=DESCRIPTOR, 79 | containing_type=None, 80 | fields=[ 81 | _descriptor.FieldDescriptor( 82 | name='id', full_name='grpc_python_example.GetItemRequest.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 | options=None), 88 | ], 89 | extensions=[ 90 | ], 91 | nested_types=[], 92 | enum_types=[ 93 | ], 94 | options=None, 95 | is_extendable=False, 96 | syntax='proto3', 97 | extension_ranges=[], 98 | oneofs=[ 99 | ], 100 | serialized_start=84, 101 | serialized_end=112, 102 | ) 103 | 104 | 105 | _GETITEMRESPONSE = _descriptor.Descriptor( 106 | name='GetItemResponse', 107 | full_name='grpc_python_example.GetItemResponse', 108 | filename=None, 109 | file=DESCRIPTOR, 110 | containing_type=None, 111 | fields=[ 112 | _descriptor.FieldDescriptor( 113 | name='item', full_name='grpc_python_example.GetItemResponse.item', index=0, 114 | number=1, type=11, cpp_type=10, label=1, 115 | has_default_value=False, default_value=None, 116 | message_type=None, enum_type=None, containing_type=None, 117 | is_extension=False, extension_scope=None, 118 | options=None), 119 | ], 120 | extensions=[ 121 | ], 122 | nested_types=[], 123 | enum_types=[ 124 | ], 125 | options=None, 126 | is_extendable=False, 127 | syntax='proto3', 128 | extension_ranges=[], 129 | oneofs=[ 130 | ], 131 | serialized_start=114, 132 | serialized_end=172, 133 | ) 134 | 135 | _GETITEMRESPONSE.fields_by_name['item'].message_type = _ITEM 136 | DESCRIPTOR.message_types_by_name['Item'] = _ITEM 137 | DESCRIPTOR.message_types_by_name['GetItemRequest'] = _GETITEMREQUEST 138 | DESCRIPTOR.message_types_by_name['GetItemResponse'] = _GETITEMRESPONSE 139 | 140 | Item = _reflection.GeneratedProtocolMessageType('Item', (_message.Message,), dict( 141 | DESCRIPTOR = _ITEM, 142 | __module__ = 'items_pb2' 143 | # @@protoc_insertion_point(class_scope:grpc_python_example.Item) 144 | )) 145 | _sym_db.RegisterMessage(Item) 146 | 147 | GetItemRequest = _reflection.GeneratedProtocolMessageType('GetItemRequest', (_message.Message,), dict( 148 | DESCRIPTOR = _GETITEMREQUEST, 149 | __module__ = 'items_pb2' 150 | # @@protoc_insertion_point(class_scope:grpc_python_example.GetItemRequest) 151 | )) 152 | _sym_db.RegisterMessage(GetItemRequest) 153 | 154 | GetItemResponse = _reflection.GeneratedProtocolMessageType('GetItemResponse', (_message.Message,), dict( 155 | DESCRIPTOR = _GETITEMRESPONSE, 156 | __module__ = 'items_pb2' 157 | # @@protoc_insertion_point(class_scope:grpc_python_example.GetItemResponse) 158 | )) 159 | _sym_db.RegisterMessage(GetItemResponse) 160 | 161 | 162 | import grpc 163 | from grpc.beta import implementations as beta_implementations 164 | from grpc.beta import interfaces as beta_interfaces 165 | from grpc.framework.common import cardinality 166 | from grpc.framework.interfaces.face import utilities as face_utilities 167 | 168 | 169 | class ItemMasterStub(object): 170 | """Manages item data. 171 | """ 172 | 173 | def __init__(self, channel): 174 | """Constructor. 175 | 176 | Args: 177 | channel: A grpc.Channel. 178 | """ 179 | self.GetItem = channel.unary_unary( 180 | '/grpc_python_example.ItemMaster/GetItem', 181 | request_serializer=GetItemRequest.SerializeToString, 182 | response_deserializer=GetItemResponse.FromString, 183 | ) 184 | 185 | 186 | class ItemMasterServicer(object): 187 | """Manages item data. 188 | """ 189 | 190 | def GetItem(self, request, context): 191 | """Get an item by id. 192 | """ 193 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 194 | context.set_details('Method not implemented!') 195 | raise NotImplementedError('Method not implemented!') 196 | 197 | 198 | def add_ItemMasterServicer_to_server(servicer, server): 199 | rpc_method_handlers = { 200 | 'GetItem': grpc.unary_unary_rpc_method_handler( 201 | servicer.GetItem, 202 | request_deserializer=GetItemRequest.FromString, 203 | response_serializer=GetItemResponse.SerializeToString, 204 | ), 205 | } 206 | generic_handler = grpc.method_handlers_generic_handler( 207 | 'grpc_python_example.ItemMaster', rpc_method_handlers) 208 | server.add_generic_rpc_handlers((generic_handler,)) 209 | 210 | 211 | class BetaItemMasterServicer(object): 212 | """Manages item data. 213 | """ 214 | def GetItem(self, request, context): 215 | """Get an item by id. 216 | """ 217 | context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) 218 | 219 | 220 | class BetaItemMasterStub(object): 221 | """Manages item data. 222 | """ 223 | def GetItem(self, request, timeout, metadata=None, with_call=False, protocol_options=None): 224 | """Get an item by id. 225 | """ 226 | raise NotImplementedError() 227 | GetItem.future = None 228 | 229 | 230 | def beta_create_ItemMaster_server(servicer, pool=None, pool_size=None, default_timeout=None, maximum_timeout=None): 231 | request_deserializers = { 232 | ('grpc_python_example.ItemMaster', 'GetItem'): GetItemRequest.FromString, 233 | } 234 | response_serializers = { 235 | ('grpc_python_example.ItemMaster', 'GetItem'): GetItemResponse.SerializeToString, 236 | } 237 | method_implementations = { 238 | ('grpc_python_example.ItemMaster', 'GetItem'): face_utilities.unary_unary_inline(servicer.GetItem), 239 | } 240 | server_options = beta_implementations.server_options(request_deserializers=request_deserializers, response_serializers=response_serializers, thread_pool=pool, thread_pool_size=pool_size, default_timeout=default_timeout, maximum_timeout=maximum_timeout) 241 | return beta_implementations.server(method_implementations, options=server_options) 242 | 243 | 244 | def beta_create_ItemMaster_stub(channel, host=None, metadata_transformer=None, pool=None, pool_size=None): 245 | request_serializers = { 246 | ('grpc_python_example.ItemMaster', 'GetItem'): GetItemRequest.SerializeToString, 247 | } 248 | response_deserializers = { 249 | ('grpc_python_example.ItemMaster', 'GetItem'): GetItemResponse.FromString, 250 | } 251 | cardinalities = { 252 | 'GetItem': cardinality.Cardinality.UNARY_UNARY, 253 | } 254 | stub_options = beta_implementations.stub_options(host=host, metadata_transformer=metadata_transformer, request_serializers=request_serializers, response_deserializers=response_deserializers, thread_pool=pool, thread_pool_size=pool_size) 255 | return beta_implementations.dynamic_stub(channel, 'grpc_python_example.ItemMaster', cardinalities, options=stub_options) 256 | # @@protoc_insertion_point(module_scope) 257 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=1 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | disable=input-builtin,old-octal-literal,coerce-method,coerce-builtin,cmp-builtin,round-builtin,intern-builtin,dict-iter-method,import-star-module-level,no-absolute-import,suppressed-message,long-suffix,reduce-builtin,indexing-exception,parameter-unpacking,unichr-builtin,setslice-method,execfile-builtin,using-cmp-argument,old-division,file-builtin,nonzero-method,next-method-called,dict-view-method,basestring-builtin,cmp-method,oct-method,old-ne-operator,backtick,raising-string,hex-method,buffer-builtin,delslice-method,useless-suppression,apply-builtin,zip-builtin-not-iterating,unpacking-in-except,old-raise-syntax,getslice-method,long-builtin,xrange-builtin,unicode-builtin,raw_input-builtin,standarderror-builtin,metaclass-assignment,filter-builtin-not-iterating,print-statement,range-builtin-not-iterating,map-builtin-not-iterating,reload-builtin 69 | 70 | 71 | [REPORTS] 72 | 73 | # Set the output format. Available formats are text, parseable, colorized, msvs 74 | # (visual studio) and html. You can also give a reporter class, eg 75 | # mypackage.mymodule.MyReporterClass. 76 | output-format=text 77 | 78 | # Put messages in a separate file for each module / package specified on the 79 | # command line instead of printing them on stdout. Reports (if any) will be 80 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 81 | # and it will be removed in Pylint 2.0. 82 | files-output=no 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=yes 86 | 87 | # Python expression which should return a note less than 10 (10 is the highest 88 | # note). You have access to the variables errors warning, statement which 89 | # respectively contain the number of errors / warnings messages and the total 90 | # number of statements analyzed. This is used by the global evaluation report 91 | # (RP0004). 92 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 93 | 94 | # Template used to display messages. This is a python new-style format string 95 | # used to format the message information. See doc for all details 96 | #msg-template= 97 | 98 | 99 | [BASIC] 100 | 101 | # Good variable names which should always be accepted, separated by a comma 102 | good-names=i,j,k,ex,Run,_ 103 | 104 | # Bad variable names which should always be refused, separated by a comma 105 | bad-names=foo,bar,baz,toto,tutu,tata 106 | 107 | # Colon-delimited sets of names that determine each other's naming style when 108 | # the name regexes allow several styles. 109 | name-group= 110 | 111 | # Include a hint for the correct naming format with invalid-name 112 | include-naming-hint=no 113 | 114 | # List of decorators that produce properties, such as abc.abstractproperty. Add 115 | # to this list to register other decorators that produce valid properties. 116 | property-classes=abc.abstractproperty 117 | 118 | # Regular expression matching correct module names 119 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 120 | 121 | # Naming hint for module names 122 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 123 | 124 | # Regular expression matching correct class attribute names 125 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 126 | 127 | # Naming hint for class attribute names 128 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 129 | 130 | # Regular expression matching correct inline iteration names 131 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 132 | 133 | # Naming hint for inline iteration names 134 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 135 | 136 | # Regular expression matching correct attribute names 137 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 138 | 139 | # Naming hint for attribute names 140 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 141 | 142 | # Regular expression matching correct argument names 143 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 144 | 145 | # Naming hint for argument names 146 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 147 | 148 | # Regular expression matching correct method names 149 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 150 | 151 | # Naming hint for method names 152 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 153 | 154 | # Regular expression matching correct class names 155 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 156 | 157 | # Naming hint for class names 158 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 159 | 160 | # Regular expression matching correct function names 161 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 162 | 163 | # Naming hint for function names 164 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 165 | 166 | # Regular expression matching correct constant names 167 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 168 | 169 | # Naming hint for constant names 170 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 171 | 172 | # Regular expression matching correct variable names 173 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 174 | 175 | # Naming hint for variable names 176 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 177 | 178 | # Regular expression which should only match function or class names that do 179 | # not require a docstring. 180 | no-docstring-rgx=^_ 181 | 182 | # Minimum line length for functions/classes that require docstrings, shorter 183 | # ones are exempt. 184 | docstring-min-length=-1 185 | 186 | 187 | [ELIF] 188 | 189 | # Maximum number of nested blocks for function / method body 190 | max-nested-blocks=5 191 | 192 | 193 | [FORMAT] 194 | 195 | # Maximum number of characters on a single line. 196 | max-line-length=100 197 | 198 | # Regexp for a line that is allowed to be longer than the limit. 199 | ignore-long-lines=^\s*(# )??$ 200 | 201 | # Allow the body of an if to be on the same line as the test if there is no 202 | # else. 203 | single-line-if-stmt=no 204 | 205 | # List of optional constructs for which whitespace checking is disabled. `dict- 206 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 207 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 208 | # `empty-line` allows space-only lines. 209 | no-space-check=trailing-comma,dict-separator 210 | 211 | # Maximum number of lines in a module 212 | max-module-lines=1000 213 | 214 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 215 | # tab). 216 | indent-string=' ' 217 | 218 | # Number of spaces of indent required inside a hanging or continued line. 219 | indent-after-paren=4 220 | 221 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 222 | expected-line-ending-format= 223 | 224 | 225 | [LOGGING] 226 | 227 | # Logging modules to check that the string format arguments are in logging 228 | # function parameter format 229 | logging-modules=logging 230 | 231 | 232 | [MISCELLANEOUS] 233 | 234 | # List of note tags to take in consideration, separated by a comma. 235 | notes=FIXME,XXX,TODO 236 | 237 | 238 | [SIMILARITIES] 239 | 240 | # Minimum lines number of a similarity. 241 | min-similarity-lines=4 242 | 243 | # Ignore comments when computing similarities. 244 | ignore-comments=yes 245 | 246 | # Ignore docstrings when computing similarities. 247 | ignore-docstrings=yes 248 | 249 | # Ignore imports when computing similarities. 250 | ignore-imports=no 251 | 252 | 253 | [SPELLING] 254 | 255 | # Spelling dictionary name. Available dictionaries: none. To make it working 256 | # install python-enchant package. 257 | spelling-dict= 258 | 259 | # List of comma separated words that should not be checked. 260 | spelling-ignore-words= 261 | 262 | # A path to a file that contains private dictionary; one word per line. 263 | spelling-private-dict-file= 264 | 265 | # Tells whether to store unknown words to indicated private dictionary in 266 | # --spelling-private-dict-file option instead of raising a message. 267 | spelling-store-unknown-words=no 268 | 269 | 270 | [TYPECHECK] 271 | 272 | # Tells whether missing members accessed in mixin class should be ignored. A 273 | # mixin class is detected if its name ends with "mixin" (case insensitive). 274 | ignore-mixin-members=yes 275 | 276 | # List of module names for which member attributes should not be checked 277 | # (useful for modules/projects where namespaces are manipulated during runtime 278 | # and thus existing member attributes cannot be deduced by static analysis. It 279 | # supports qualified module names, as well as Unix pattern matching. 280 | ignored-modules= 281 | 282 | # List of class names for which member attributes should not be checked (useful 283 | # for classes with dynamically set attributes). This supports the use of 284 | # qualified names. 285 | ignored-classes=optparse.Values,thread._local,_thread._local 286 | 287 | # List of members which are set dynamically and missed by pylint inference 288 | # system, and so shouldn't trigger E1101 when accessed. Python regular 289 | # expressions are accepted. 290 | generated-members= 291 | 292 | # List of decorators that produce context managers, such as 293 | # contextlib.contextmanager. Add to this list to register other decorators that 294 | # produce valid context managers. 295 | contextmanager-decorators=contextlib.contextmanager 296 | 297 | 298 | [VARIABLES] 299 | 300 | # Tells whether we should check for unused import in __init__ files. 301 | init-import=no 302 | 303 | # A regular expression matching the name of dummy variables (i.e. expectedly 304 | # not used). 305 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 306 | 307 | # List of additional names supposed to be defined in builtins. Remember that 308 | # you should avoid to define new builtins when possible. 309 | additional-builtins= 310 | 311 | # List of strings which can identify a callback function by name. A callback 312 | # name must start or end with one of those strings. 313 | callbacks=cb_,_cb 314 | 315 | # List of qualified module names which can have objects that can redefine 316 | # builtins. 317 | redefining-builtins-modules=six.moves,future.builtins 318 | 319 | 320 | [CLASSES] 321 | 322 | # List of method names used to declare (i.e. assign) instance attributes. 323 | defining-attr-methods=__init__,__new__,setUp 324 | 325 | # List of valid names for the first argument in a class method. 326 | valid-classmethod-first-arg=cls 327 | 328 | # List of valid names for the first argument in a metaclass class method. 329 | valid-metaclass-classmethod-first-arg=mcs 330 | 331 | # List of member names, which should be excluded from the protected access 332 | # warning. 333 | exclude-protected=_asdict,_fields,_replace,_source,_make 334 | 335 | 336 | [DESIGN] 337 | 338 | # Maximum number of arguments for function / method 339 | max-args=5 340 | 341 | # Argument names that match this expression will be ignored. Default to name 342 | # with leading underscore 343 | ignored-argument-names=_.* 344 | 345 | # Maximum number of locals for function / method body 346 | max-locals=15 347 | 348 | # Maximum number of return / yield for function / method body 349 | max-returns=6 350 | 351 | # Maximum number of branch for function / method body 352 | max-branches=12 353 | 354 | # Maximum number of statements in function / method body 355 | max-statements=50 356 | 357 | # Maximum number of parents for a class (see R0901). 358 | max-parents=7 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=7 362 | 363 | # Minimum number of public methods for a class (see R0903). 364 | min-public-methods=2 365 | 366 | # Maximum number of public methods for a class (see R0904). 367 | max-public-methods=20 368 | 369 | # Maximum number of boolean expressions in a if statement 370 | max-bool-expr=5 371 | 372 | 373 | [IMPORTS] 374 | 375 | # Deprecated modules which should not be used, separated by a comma 376 | deprecated-modules=optparse 377 | 378 | # Create a graph of every (i.e. internal and external) dependencies in the 379 | # given file (report RP0402 must not be disabled) 380 | import-graph= 381 | 382 | # Create a graph of external dependencies in the given file (report RP0402 must 383 | # not be disabled) 384 | ext-import-graph= 385 | 386 | # Create a graph of internal dependencies in the given file (report RP0402 must 387 | # not be disabled) 388 | int-import-graph= 389 | 390 | # Force import order to recognize a module as part of the standard 391 | # compatibility libraries. 392 | known-standard-library= 393 | 394 | # Force import order to recognize a module as part of a third party library. 395 | known-third-party=enchant 396 | 397 | # Analyse import fallback blocks. This can be used to support both Python 2 and 398 | # 3 compatible code, which means that the block might have code that exists 399 | # only in one or another interpreter, leading to false positives when analysed. 400 | analyse-fallback-blocks=no 401 | 402 | 403 | [EXCEPTIONS] 404 | 405 | # Exceptions that will emit a warning when being caught. Defaults to 406 | # "Exception" 407 | overgeneral-exceptions=Exception 408 | --------------------------------------------------------------------------------