├── 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 |
--------------------------------------------------------------------------------