├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── __init__.py ├── app.py ├── models.py ├── node_modules │ └── .gitkeep ├── order_api │ ├── __init__.py │ ├── api │ │ ├── UserClient.py │ │ └── __init__.py │ └── routes.py ├── package.json ├── requirements.txt └── swagger.json ├── bin └── .gitignore ├── docker-compose.yml ├── docs ├── api │ └── .gitignore └── install │ └── .gitignore └── tests ├── conftest.py └── test_endpoints.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.idea 2 | venv/* 3 | env/ 4 | **/*.pytest_cache 5 | **/.DS_Store 6 | app/node_modules/* 7 | !app/node_modules/.gitkeep -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | MAINTAINER Peter Fisher 4 | 5 | COPY ./app/requirements.txt /app/requirements.txt 6 | 7 | WORKDIR /app 8 | 9 | RUN apk add --update \ 10 | build-base \ 11 | bash \ 12 | curl \ 13 | gcc \ 14 | libc-dev \ 15 | mariadb-dev \ 16 | nodejs \ 17 | npm \ 18 | && pip install --upgrade pip \ 19 | && pip install -r requirements.txt \ 20 | && rm -rf /var/cache/apk/* 21 | 22 | COPY ./app/package.json /app/package.json 23 | RUN npm install 24 | 25 | COPY ./app /app 26 | 27 | CMD ["python", "app.py"] 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Order-Service/a5dede8a2c04ed9b748a65f33d183b96499e9c94/README.md -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Order-Service/a5dede8a2c04ed9b748a65f33d183b96499e9c94/app/__init__.py -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from order_api import order_api_blueprint 3 | from flask_swagger_ui import get_swaggerui_blueprint 4 | import models 5 | 6 | app = Flask(__name__) 7 | 8 | app.config.update(dict( 9 | SECRET_KEY="powerful secretkey", 10 | WTF_CSRF_SECRET_KEY="a csrf secret key", 11 | SQLALCHEMY_DATABASE_URI='mysql+mysqlconnector://root:test@order_db/order', 12 | )) 13 | 14 | models.init_app(app) 15 | models.create_tables(app) 16 | 17 | app.register_blueprint(order_api_blueprint) 18 | SWAGGER_URL = '/api/docs' 19 | API_URL = '/api/order/docs.json' 20 | 21 | swaggerui_blueprint = get_swaggerui_blueprint( 22 | SWAGGER_URL, 23 | API_URL, 24 | ) 25 | app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) 26 | 27 | if __name__ == '__main__': 28 | app.run(debug=True, host='0.0.0.0') 29 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from flask_sqlalchemy import SQLAlchemy 3 | from sqlalchemy import create_engine 4 | 5 | db = SQLAlchemy() 6 | 7 | 8 | def init_app(app): 9 | db.app = app 10 | db.init_app(app) 11 | return db 12 | 13 | 14 | def create_tables(app): 15 | engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI']) 16 | db.metadata.create_all(engine) 17 | return engine 18 | 19 | 20 | class Order(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | user_id = db.Column(db.Integer) 23 | items = db.relationship('OrderItem', backref='orderItem') 24 | is_open = db.Column(db.Boolean, default=True) 25 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 26 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 27 | 28 | def create(self, user_id): 29 | self.user_id = user_id 30 | self.is_open = True 31 | return self 32 | 33 | def to_json(self): 34 | items = [] 35 | for i in self.items: 36 | items.append(i.to_json()) 37 | 38 | return { 39 | 'items': items, 40 | 'is_open': self.is_open, 41 | 'user_id': self.user_id 42 | } 43 | 44 | 45 | class OrderItem(db.Model): 46 | id = db.Column(db.Integer, primary_key=True) 47 | order_id = db.Column(db.Integer, db.ForeignKey('order.id')) 48 | product_id = db.Column(db.Integer) 49 | quantity = db.Column(db.Integer, default=1) 50 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 51 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 52 | 53 | def __init__(self, product_id, quantity): 54 | self.product_id = product_id 55 | self.quantity = quantity 56 | 57 | def to_json(self): 58 | return { 59 | 'product': self.product_id, 60 | 'quantity': self.quantity, 61 | } 62 | -------------------------------------------------------------------------------- /app/node_modules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Order-Service/a5dede8a2c04ed9b748a65f33d183b96499e9c94/app/node_modules/.gitkeep -------------------------------------------------------------------------------- /app/order_api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | order_api_blueprint = Blueprint('order_api', __name__) 4 | 5 | 6 | from . import routes -------------------------------------------------------------------------------- /app/order_api/api/UserClient.py: -------------------------------------------------------------------------------- 1 | from flask import session 2 | import requests 3 | 4 | 5 | class UserClient: 6 | 7 | @staticmethod 8 | def get_user(api_key): 9 | headers = { 10 | 'Authorization': api_key 11 | } 12 | 13 | response = requests.request(method="GET", url='http://user:5000/api/user', headers=headers) 14 | if response.status_code == 401: 15 | return False 16 | 17 | user = response.json() 18 | return user 19 | -------------------------------------------------------------------------------- /app/order_api/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Order-Service/a5dede8a2c04ed9b748a65f33d183b96499e9c94/app/order_api/api/__init__.py -------------------------------------------------------------------------------- /app/order_api/routes.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify,json, request, make_response 2 | from . import order_api_blueprint 3 | from models import db, Order, OrderItem 4 | from .api.UserClient import UserClient 5 | 6 | 7 | @order_api_blueprint.route("/api/order/docs.json", methods=['GET']) 8 | def swagger_api_docs_yml(): 9 | with open('swagger.json') as fd: 10 | json_data = json.load(fd) 11 | 12 | return jsonify(json_data) 13 | 14 | 15 | @order_api_blueprint.route('/api/orders', methods=['GET']) 16 | def orders(): 17 | 18 | items = [] 19 | for row in Order.query.all(): 20 | items.append(row.to_json()) 21 | 22 | response = jsonify(items) 23 | 24 | return response 25 | 26 | 27 | @order_api_blueprint.route('/api/order/add-item', methods=['POST']) 28 | def order_add_item(): 29 | 30 | api_key = request.headers.get('Authorization') 31 | response = UserClient.get_user(api_key) 32 | 33 | if not response: 34 | return make_response(jsonify({'message': 'Not logged in'}), 401) 35 | 36 | user = response['result'] 37 | 38 | p_id = int(request.form['product_id']) 39 | qty = int(request.form['qty']) 40 | u_id = int(user['id']) 41 | 42 | # Find open order 43 | known_order = Order.query.filter_by(user_id=u_id, is_open=1).first() 44 | 45 | if known_order is None: 46 | # Create the order 47 | known_order = Order() 48 | known_order.is_open = True 49 | known_order.user_id = u_id 50 | 51 | order_item = OrderItem(p_id, qty) 52 | known_order.items.append(order_item) 53 | 54 | else: 55 | found = False 56 | # Check if we already have an order item with that product 57 | for item in known_order.items: 58 | 59 | if item.product_id == p_id: 60 | found = True 61 | item.quantity += qty 62 | 63 | if found is False: 64 | order_item = OrderItem(p_id, qty) 65 | known_order.items.append(order_item) 66 | 67 | db.session.add(known_order) 68 | db.session.commit() 69 | 70 | response = jsonify({'result': known_order.to_json()}) 71 | 72 | return response 73 | 74 | 75 | @order_api_blueprint.route('/api/order', methods=['GET']) 76 | def order(): 77 | api_key = request.headers.get('Authorization') 78 | response = UserClient.get_user(api_key) 79 | 80 | if not response: 81 | return make_response(jsonify({'message': 'Not logged in'}), 401) 82 | 83 | user = response['result'] 84 | 85 | open_order = Order.query.filter_by(user_id=user['id'], is_open=1).first() 86 | 87 | if open_order is None: 88 | response = jsonify({'message': 'No order found'}) 89 | else: 90 | response = jsonify({'result': open_order.to_json()}) 91 | 92 | return response 93 | 94 | 95 | @order_api_blueprint.route('/api/order/checkout', methods=['POST']) 96 | def checkout(): 97 | api_key = request.headers.get('Authorization') 98 | response = UserClient.get_user(api_key) 99 | 100 | if not response: 101 | return make_response(jsonify({'message': 'Not logged in'}), 401) 102 | 103 | user = response['result'] 104 | 105 | order_model = Order.query.filter_by(user_id=user['id'], is_open=1).first() 106 | order_model.is_open = 0 107 | 108 | db.session.add(order_model) 109 | db.session.commit() 110 | 111 | response = jsonify({'result': order_model.to_json()}) 112 | 113 | return response 114 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "dependencies": { 4 | "swagger-ui-dist": "~3.5.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | requests==2.20.0 3 | SQLAlchemy==1.2.8 4 | Flask-SQLAlchemy==2.3.2 5 | mysql-connector==2.2.9 6 | flask-swagger-ui==3.18.0 7 | blinker==1.4 8 | pytest==3.9.1 -------------------------------------------------------------------------------- /app/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Order service for order management system micro service", 5 | "version": "1.0.0", 6 | "title": "Order Service" 7 | }, 8 | "host": "192.168.99.102:8083", 9 | "basePath": "/api", 10 | "schemes": [ 11 | "http" 12 | ], 13 | "paths": { 14 | "/orders": { 15 | "get": { 16 | "tags": [ 17 | "Order" 18 | ], 19 | "summary": "Get all the orders", 20 | "description": "", 21 | "produces": [ 22 | "application/json" 23 | ], 24 | "responses": { 25 | "200": { 26 | "description": "successful operation" 27 | } 28 | } 29 | } 30 | }, 31 | "/order/add-item": { 32 | "get": { 33 | "tags": [ 34 | "Order" 35 | ], 36 | "summary": "Adds an item to the order", 37 | "produces": [ 38 | "application/json" 39 | ], 40 | "parameters": [ 41 | { 42 | "name": "p_id", 43 | "in": "formData", 44 | "description": "Product ID", 45 | "required": true, 46 | "type": "integer" 47 | }, 48 | { 49 | "name": "qty", 50 | "in": "formData", 51 | "description": "Product quantity", 52 | "required": true, 53 | "type": "integer" 54 | }, 55 | { 56 | "name": "u_id", 57 | "in": "formData", 58 | "description": "User ID", 59 | "required": true, 60 | "type": "integer" 61 | } 62 | ], 63 | "responses": { 64 | "200": { 65 | "description": "Updated order" 66 | }, 67 | "401": { 68 | "description": "Not logged in" 69 | } 70 | } 71 | } 72 | }, 73 | "/order": { 74 | "post": { 75 | "tags": [ 76 | "Order" 77 | ], 78 | "summary": "Get an order", 79 | "description": "", 80 | "consumes": [ 81 | "multipart/form-data" 82 | ], 83 | "produces": [ 84 | "application/json" 85 | ], 86 | "responses": { 87 | "401": { 88 | "description": "Not logged in" 89 | }, 90 | "404": { 91 | "description": "No order found'" 92 | }, 93 | "200": { 94 | "description": "Users order'" 95 | } 96 | } 97 | } 98 | }, 99 | "/order/checkout": { 100 | "get": { 101 | "tags": [ 102 | "Order" 103 | ], 104 | "summary": "Processes the order", 105 | "description": "", 106 | "produces": [ 107 | "application/json" 108 | ], 109 | "responses": { 110 | "401": { 111 | "description": "Not logged in" 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Order-Service/a5dede8a2c04ed9b748a65f33d183b96499e9c94/bin/.gitignore -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | order-db-data: 5 | 6 | services: 7 | order: 8 | build: 9 | context: . 10 | ports: 11 | - 80:5000 12 | volumes: 13 | - ./app:/app 14 | links: 15 | - order_db 16 | depends_on: 17 | - order_db 18 | 19 | order_db: 20 | image: mysql:5.7.22 21 | volumes: 22 | - order-db-data:/var/lib/mysql 23 | environment: 24 | - MYSQL_ROOT_PASSWORD=test 25 | - MYSQL_DATABASE=order 26 | restart: always 27 | -------------------------------------------------------------------------------- /docs/api/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Order-Service/a5dede8a2c04ed9b748a65f33d183b96499e9c94/docs/api/.gitignore -------------------------------------------------------------------------------- /docs/install/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Order-Service/a5dede8a2c04ed9b748a65f33d183b96499e9c94/docs/install/.gitignore -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from app import app 5 | from app.models import db as _db 6 | 7 | 8 | TEST_DB = 'test_project.db' 9 | TEST_DB_PATH = "/opt/project/data/{}".format(TEST_DB) 10 | TEST_DATABASE_URI = 'sqlite:///' + TEST_DB_PATH 11 | 12 | 13 | @pytest.fixture(scope='session') 14 | def app(): 15 | """Session-wide test `Flask` application.""" 16 | 17 | app.config.update(dict( 18 | TESTING=True, 19 | SQLALCHEMY_DATABASE_URI=TEST_DATABASE_URI, 20 | )) 21 | 22 | return app 23 | 24 | 25 | @pytest.fixture(scope='session') 26 | def db(app, request): 27 | """Session-wide test database.""" 28 | if os.path.exists(TEST_DB_PATH): 29 | os.unlink(TEST_DB_PATH) 30 | 31 | def teardown(): 32 | _db.drop_all() 33 | os.unlink(TEST_DB_PATH) 34 | 35 | _db.app = app 36 | _db.create_all() 37 | 38 | request.addfinalizer(teardown) 39 | return _db 40 | 41 | 42 | @pytest.fixture(scope='function') 43 | def session(db, request): 44 | """Creates a new database session for a test.""" 45 | connection = db.engine.connect() 46 | transaction = connection.begin() 47 | 48 | options = dict(bind=connection, binds={}) 49 | session = db.create_scoped_session(options=options) 50 | 51 | db.session = session 52 | 53 | def teardown(): 54 | transaction.rollback() 55 | connection.close() 56 | session.remove() 57 | 58 | request.addfinalizer(teardown) 59 | return session -------------------------------------------------------------------------------- /tests/test_endpoints.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import requests 4 | 5 | 6 | class TestFlaskApiUsingRequests(TestCase): 7 | def test_products(self): 8 | response = requests.get('http://192.168.99.100:8081/api/products') 9 | self.assertEqual(response.status_code, 200) 10 | 11 | def test_create(self): 12 | payload = { 13 | "product": { 14 | "image": "banana.png", 15 | "name": "Product 1", 16 | "price": 2, 17 | "slug": "product-1" 18 | } 19 | } 20 | response = requests.post('http://192.168.99.100:8081/api/product/create', payload) 21 | self.assertEqual(response.status_code, 200) 22 | 23 | def test_product(self): 24 | response = requests.get('http://192.168.99.100:8081/api/product/product-1') 25 | self.assertEqual(response.status_code, 200) --------------------------------------------------------------------------------