├── api ├── __init__.py ├── subscriber_api.py ├── total_covid_cases_api.py └── covid_api.py ├── screenshots ├── mutation.png ├── thumbnail.png └── all_queries.png ├── graphql_queries ├── mutation_query.graphql └── all_queries.graphql ├── run.py ├── config.py ├── LICENSE ├── models.py ├── schema.py └── README.md /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/python-graphql-microservices/main/screenshots/mutation.png -------------------------------------------------------------------------------- /screenshots/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/python-graphql-microservices/main/screenshots/thumbnail.png -------------------------------------------------------------------------------- /screenshots/all_queries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/python-graphql-microservices/main/screenshots/all_queries.png -------------------------------------------------------------------------------- /graphql_queries/mutation_query.graphql: -------------------------------------------------------------------------------- 1 | mutation { 2 | fetchByCountryName(country: "India") { 3 | statistics { 4 | code 5 | country 6 | flag 7 | coordinates 8 | confirmed 9 | deaths 10 | recovered 11 | states { 12 | edges { 13 | node { 14 | name 15 | address 16 | latitude 17 | longitude 18 | confirmed 19 | deaths 20 | recovered 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from api.subscriber_api import subscriber_bp 2 | from api.total_covid_cases_api import total_covid_cases_bp 3 | from api.covid_api import covid_bp 4 | from flask import Flask 5 | from flask_graphql import GraphQLView 6 | from config import Config 7 | from schema import schema 8 | 9 | app = Flask(__name__) 10 | app.debug = True 11 | 12 | app.register_blueprint(covid_bp) 13 | app.register_blueprint(total_covid_cases_bp) 14 | app.register_blueprint(subscriber_bp) 15 | 16 | app.add_url_rule( 17 | Config.GRAPTH_QL, 18 | view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True) 19 | ) 20 | 21 | if __name__ == '__main__': 22 | Config.connect_db() 23 | app.run() -------------------------------------------------------------------------------- /graphql_queries/all_queries.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allStatistics { 3 | edges { 4 | node { 5 | code 6 | country 7 | flag 8 | coordinates 9 | confirmed 10 | deaths 11 | recovered 12 | states { 13 | edges { 14 | node { 15 | name 16 | address 17 | latitude 18 | longitude 19 | confirmed 20 | deaths 21 | recovered 22 | } 23 | } 24 | } 25 | } 26 | } 27 | }, 28 | allTotalCases { 29 | edges { 30 | node { 31 | totalConfirmed 32 | totalDeaths 33 | totalRecovered 34 | } 35 | } 36 | }, 37 | allSubscriber { 38 | edges { 39 | node { 40 | email 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from mongoengine import connect 2 | 3 | class Config: 4 | # API Configs 5 | API_PATH = '/api/' 6 | API_VERSION = 'v1' 7 | BASE_URL = 'http://127.0.0.1:5000' 8 | 9 | # GraphQL Endpoint 10 | GRAPTH_QL = API_PATH+API_VERSION+'/graphql' 11 | 12 | # Total Covid cases Endpoint 13 | ADD_TOTAL_COVID_CASES = API_PATH+API_VERSION+'/add_total_cases' 14 | FETCH_TOTAL_COVID_CASES = API_PATH+API_VERSION+'/fetch_total_cases' 15 | 16 | # Statistics Endpoint 17 | ADD_STATISTICS = API_PATH+API_VERSION+'/add_statistics' 18 | FETCH_STATISTICS = API_PATH+API_VERSION+'/fetch_statistics' 19 | 20 | # Covid News Subscription Endpoint 21 | SUBSCRIBE_TO_COVID_ALERT = API_PATH+API_VERSION+'/subscribe' 22 | FETCH_SUBSCRIBERS_LIST = API_PATH+API_VERSION+'/fetch_subscribers' 23 | 24 | def connect_db(): 25 | connect('covid-19', host='mongodb://127.0.0.1:27017', alias='default') -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anshuman Pattnaik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from mongoengine import Document 2 | from mongoengine.fields import ( 3 | ListField, DecimalField, IntField,ReferenceField, StringField, 4 | ) 5 | 6 | class States(Document): 7 | meta = {'collection': 'states'} 8 | key = StringField() 9 | name = StringField() 10 | address = StringField() 11 | latitude = DecimalField() 12 | longitude = DecimalField() 13 | confirmed = IntField() 14 | deaths = IntField() 15 | recovered = IntField() 16 | 17 | class Statistics(Document): 18 | meta = {'collection': 'statistics'} 19 | country = StringField() 20 | code = StringField() 21 | flag = StringField() 22 | coordinates = ListField(DecimalField()) 23 | confirmed = IntField() 24 | deaths = IntField() 25 | recovered = IntField() 26 | states = ListField(ReferenceField(States)) 27 | 28 | class TotalCases(Document): 29 | meta = {'collection': 'total_cases'} 30 | total_confirmed = IntField() 31 | total_deaths = IntField() 32 | total_recovered = IntField() 33 | last_date_updated = StringField() 34 | 35 | class Subscriber(Document): 36 | meta = {'collection': 'subscriber'} 37 | email = StringField() -------------------------------------------------------------------------------- /api/subscriber_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import Blueprint, make_response, jsonify, request 3 | from werkzeug.exceptions import abort 4 | from models import Subscriber 5 | from config import Config 6 | 7 | subscriber_bp = Blueprint('subscriber', __name__) 8 | 9 | 10 | @subscriber_bp.route(Config.FETCH_SUBSCRIBERS_LIST, methods=['GET']) 11 | def fetch_subscribers(): 12 | subscriber = [json.loads(res.to_json()) for res in Subscriber.objects] 13 | return make_response(jsonify(subscriber), 200) 14 | 15 | 16 | @subscriber_bp.route(Config.SUBSCRIBE_TO_COVID_ALERT, methods=['POST']) 17 | def subscribe(): 18 | try: 19 | email = request.json['email'] 20 | try: 21 | if Subscriber.objects.get(email=email): 22 | return make_response(jsonify({"email": email+' is already subscribed'}), 400) 23 | except Subscriber.DoesNotExist: 24 | pass 25 | 26 | subscribe = Subscriber(email=email) 27 | subscribe.save() 28 | 29 | return make_response(jsonify({ 30 | "success": "Thank you for subscribing to COVID-19 alerts" 31 | }), 201) 32 | 33 | except KeyError: 34 | abort(400) 35 | 36 | 37 | @subscriber_bp.errorhandler(400) 38 | def invalid_request(error): 39 | return make_response(jsonify({'error': 'Invalid Request '+error}), 400) 40 | -------------------------------------------------------------------------------- /api/total_covid_cases_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import Blueprint, make_response, jsonify, request 3 | from werkzeug.exceptions import abort 4 | from models import TotalCases 5 | from config import Config 6 | 7 | total_covid_cases_bp = Blueprint('total_covid_cases', __name__) 8 | 9 | 10 | @total_covid_cases_bp.route(Config.FETCH_TOTAL_COVID_CASES, methods=['GET']) 11 | def fetch_total_covid_cases(): 12 | total_cases = [json.loads(res.to_json()) for res in TotalCases.objects] 13 | return make_response(jsonify(total_cases), 200) 14 | 15 | 16 | @total_covid_cases_bp.route(Config.ADD_TOTAL_COVID_CASES, methods=['POST']) 17 | def add_total_covid_cases(): 18 | try: 19 | total_confirmed = request.json['total_confirmed'] 20 | total_deaths = request.json['total_deaths'] 21 | total_recovered = request.json['total_recovered'] 22 | last_date_updated = request.json['last_date_updated'] 23 | 24 | total_cases = TotalCases(total_confirmed=total_confirmed, 25 | total_deaths=total_deaths, 26 | total_recovered=total_recovered, 27 | last_date_updated=last_date_updated) 28 | total_cases.save() 29 | 30 | return make_response(jsonify({ 31 | "success": request.json 32 | }), 201) 33 | 34 | except KeyError: 35 | abort(400) 36 | 37 | 38 | @total_covid_cases_bp.errorhandler(400) 39 | def invalid_request(error): 40 | return make_response(jsonify({'error': 'Invalid Request '+error}), 400) 41 | -------------------------------------------------------------------------------- /schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.relay import Node 3 | from graphene_mongo import MongoengineConnectionField, MongoengineObjectType 4 | from models import States as StatesModel 5 | from models import Statistics as StatisticsModel 6 | from models import TotalCases as TotalCasesModel 7 | from models import Subscriber as SubscriberModel 8 | 9 | 10 | class States(MongoengineObjectType): 11 | 12 | class Meta: 13 | model = StatesModel 14 | interfaces = (Node,) 15 | 16 | 17 | class Statistics(MongoengineObjectType): 18 | 19 | class Meta: 20 | model = StatisticsModel 21 | interfaces = (Node,) 22 | 23 | 24 | class TotalCases(MongoengineObjectType): 25 | 26 | class Meta: 27 | model = TotalCasesModel 28 | interfaces = (Node,) 29 | 30 | 31 | class Subscriber(MongoengineObjectType): 32 | 33 | class Meta: 34 | model = SubscriberModel 35 | interfaces = (Node,) 36 | 37 | class Query(graphene.ObjectType): 38 | node = Node.Field() 39 | all_statistics = MongoengineConnectionField(Statistics) 40 | all_states = MongoengineConnectionField(States) 41 | all_total_cases = MongoengineConnectionField(TotalCases) 42 | all_subscriber = MongoengineConnectionField(Subscriber) 43 | 44 | 45 | class fetchByCountryName(graphene.Mutation): 46 | class Arguments: 47 | country = graphene.String(required=True) 48 | 49 | statistics = graphene.Field(lambda: Statistics) 50 | 51 | def mutate(self, info, country): 52 | country_statistics = StatisticsModel.objects.get(country=country) 53 | return fetchByCountryName(statistics=country_statistics) 54 | 55 | 56 | class Mutation(graphene.ObjectType): 57 | fetch_by_country_name = fetchByCountryName.Field() 58 | 59 | 60 | schema = graphene.Schema( 61 | query=Query, types=[States, Statistics, TotalCases, Subscriber], mutation=Mutation) 62 | -------------------------------------------------------------------------------- /api/covid_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import Blueprint, make_response, jsonify, request 3 | from werkzeug.exceptions import abort 4 | from models import States, Statistics 5 | from config import Config 6 | 7 | covid_bp = Blueprint('covid', __name__) 8 | 9 | @covid_bp.route(Config.FETCH_STATISTICS, methods=['GET']) 10 | def fetch_statistics(): 11 | statistics = [json.loads(res.to_json()) for res in Statistics.objects] 12 | return make_response(jsonify({'success': statistics}), 200) 13 | 14 | @covid_bp.route(Config.ADD_STATISTICS, methods=['POST']) 15 | def add_statistics(): 16 | try: 17 | for data in request.json['country_statistics']: 18 | country = data['country'] 19 | code = data['code'] 20 | flag = data['flag'] 21 | coordinates = data['coordinates'] 22 | confirmed = data['confirmed'] 23 | deaths = data['deaths'] 24 | recovered = data['recovered'] 25 | 26 | all_states = [] 27 | for state in data['states']: 28 | states = States(key=state['key'], name=state['name'], 29 | address=state['address'], 30 | latitude=state['latitude'], 31 | longitude=state['longitude'], 32 | confirmed=state['confirmed'], 33 | deaths=state['deaths'], 34 | recovered=state['recovered']) 35 | states.save() 36 | all_states.append(states) 37 | 38 | statistics = Statistics(country=country, 39 | code=code, 40 | flag=flag, 41 | coordinates=coordinates, 42 | confirmed=confirmed, 43 | deaths=deaths, 44 | recovered=recovered, 45 | states=all_states) 46 | statistics.save() 47 | 48 | return make_response(jsonify({ 49 | "success": request.json 50 | }), 201) 51 | 52 | except KeyError: 53 | abort(400) 54 | 55 | 56 | @covid_bp.errorhandler(400) 57 | def invalid_request(error): 58 | return make_response(jsonify({'error': 'Invalid Request '+error}), 400) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Python GraphQL Microservices 2 | The idea behind this project is to demonstrate using [GraphQL](https://graphql.org/) how we can query large sets of datasets by combining different sets of API endpoints? and GraphQL is a very popular data query and manipulation language developed by [Facebook](https://developers.facebook.com/docs/graph-api/) and using GraphQL we can populate large sets of data using a single query in a very faster and efficient way. To demonstrate the proof of concept I have taken the datasets from [COVID-19](https://github.com/anshumanpattnaik/covid19-full-stack-application) project and those datasets used for analysis in this project. 3 | 4 | 5 | 6 | ## Technical Overview 7 | The proof of concept written using python and to implement the GraphQL it uses [graphene](https://pypi.org/project/graphene/) python module and the microservices designed and implemented using [flask](https://flask.palletsprojects.com/en/1.1.x/) framework and to store the data it uses mongodb database for the storage. 8 | 9 | There are three different types of microservices designed in this project 10 | 11 | 1. Statistics 12 | 2. Total Cases 13 | 3. Subscriptions 14 | 15 | ## Installation 16 | ````````````````````````````````````````````````````````````````````````` 17 | git clone https://github.com/anshumanpattnaik/python-graphql-microservices 18 | cd python-graphql-microservices 19 | pip install -r requirements.txt 20 | source venv/bin/activate 21 | python3 run.py 22 | ````````````````````````````````````````````````````````````````````````` 23 | 24 | ## COVID-19 Datasets 25 | Under `covid19_dummy_data/` folder you will find the statistics [data.json](https://github.com/anshumanpattnaik/python-graphql-microservices/blob/main/covid19_dummy_data/data.json) file, which you can use while adding data using `http://127.0.0.1:5000/api/v1/add_statistics` endpoint. 26 | 27 | ## GraphQL (All Queries/Mutation) 28 | After the installation and importing the data into mongodb database, you can open `http://127.0.0.1:5000/api/v1/graphql` to view in the browser. 29 | 30 | To refer the queries you can follow `graphql_queries/` folder for [all_queries.graphql](https://github.com/anshumanpattnaik/python-graphql-microservices/blob/main/graphql_queries/all_queries.graphql) and [mutation_query.graphql](https://github.com/anshumanpattnaik/python-graphql-microservices/blob/main/graphql_queries/mutation_query.graphql) which you can use to analyze the data. 31 | 32 | ### All Queries 33 | Using all queries it will populate three different sets of data. 34 | 35 | 1. Statistics of all counties - (allStatistics) 36 | 2. Total Cases around the world - (allTotalCases) 37 | 3. List of COVID-19 email subscribers - (allSubscriber) 38 | 39 | `````````````````````````````````````````````````````````````````````````````` 40 | { 41 | allStatistics { 42 | edges { 43 | node { 44 | code 45 | country 46 | flag 47 | coordinates 48 | confirmed 49 | deaths 50 | recovered 51 | states { 52 | edges { 53 | node { 54 | name 55 | address 56 | latitude 57 | longitude 58 | confirmed 59 | deaths 60 | recovered 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | allTotalCases { 68 | edges { 69 | node { 70 | totalConfirmed 71 | totalDeaths 72 | totalRecovered 73 | } 74 | } 75 | }, 76 | allSubscriber { 77 | edges { 78 | node { 79 | email 80 | } 81 | } 82 | } 83 | } 84 | `````````````````````````````````````````````````````````````````````````````` 85 | 86 | 87 | 88 | ### Mutation Query 89 | Using mutation query you can populate the statistics of the individual country by passing the country name. 90 | 91 | ```````````````````````````````````````````````````````````````````` 92 | mutation { 93 | fetchByCountryName(country: "India") { 94 | statistics { 95 | code 96 | country 97 | flag 98 | coordinates 99 | confirmed 100 | deaths 101 | recovered 102 | states { 103 | edges { 104 | node { 105 | name 106 | address 107 | latitude 108 | longitude 109 | confirmed 110 | deaths 111 | recovered 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | ```````````````````````````````````````````````````````````````````` 119 | 120 | 121 | 122 | ### Note 123 | To understand more in-depth I'll highly recommend following the official documentation of [GraphQL](https://graphql.org/code/#python) and [Graphene](https://docs.graphene-python.org/en/latest/quickstart/) 124 | 125 | ### License 126 | This project is licensed under the [MIT License](LICENSE) 127 | --------------------------------------------------------------------------------