├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── __init__.py ├── add_products.py ├── app.py ├── models.py ├── node_modules │ └── .gitkeep ├── package.json ├── product_api │ ├── __init__.py │ └── routes.py ├── requirements.txt ├── setup.py ├── swagger.json └── test_service.py ├── docker-compose.yml ├── docs ├── api │ └── .gitignore └── install │ ├── .gitignore │ └── install.md └── 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 | gcc \ 12 | libc-dev \ 13 | mariadb-dev \ 14 | nodejs \ 15 | npm \ 16 | && pip install --upgrade pip \ 17 | && pip install -r requirements.txt \ 18 | && rm -rf /var/cache/apk/* 19 | 20 | COPY ./app/package.json /app/package.json 21 | 22 | RUN npm install 23 | 24 | COPY ./app /app 25 | 26 | CMD ["python", "app.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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-Product-Service/81aaf189118e4276135f823dfc83438ef8ff4726/README.md -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Product-Service/81aaf189118e4276135f823dfc83438ef8ff4726/app/__init__.py -------------------------------------------------------------------------------- /app/add_products.py: -------------------------------------------------------------------------------- 1 | from models import db, Product 2 | import setup 3 | 4 | app = setup.create_app() 5 | 6 | items = [ 7 | { 8 | 'name': 'Product 1', 9 | 'slug': 'product-1', 10 | 'image': 'apple.png', 11 | 'price': 1 12 | }, 13 | { 14 | 'name': 'Product 2', 15 | 'slug': 'product-2', 16 | 'image': 'banana.png', 17 | 'price': 2 18 | }, 19 | { 20 | 'name': 'Product 3', 21 | 'slug': 'product-3', 22 | 'image': 'coffee.png', 23 | 'price': 3 24 | }, 25 | { 26 | 'name': 'Product 4', 27 | 'slug': 'product-4', 28 | 'image': 'rubber_duck.png', 29 | 'price': 4 30 | }, 31 | { 32 | 'name': 'Product 5', 33 | 'slug': 'product-5', 34 | 'image': 'tomato.png', 35 | 'price': 1 36 | }, 37 | { 38 | 'name': 'Product 6', 39 | 'slug': 'product-6', 40 | 'image': 'Fidget_spinner_in_blue.png', 41 | 'price': 3 42 | }, 43 | ] 44 | 45 | for item in items: 46 | 47 | record = Product.query.filter_by(slug=item['slug']).first() 48 | 49 | if record is None: 50 | 51 | print("Adding product " + item['slug'] + "\n") 52 | 53 | record = Product() 54 | record.name = item['name'] 55 | record.slug = item['slug'] 56 | record.image = item['image'] 57 | record.price = item['price'] 58 | 59 | db.session.add(record) 60 | db.session.commit() 61 | else: 62 | print("product " + item['slug'] + " has already been added ...... Skipping \n") 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | import setup 2 | 3 | app = setup.create_app() 4 | 5 | if __name__ == '__main__': 6 | app.run(debug=True, host='0.0.0.0') 7 | -------------------------------------------------------------------------------- /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 Product(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | name = db.Column(db.String(255), unique=True, nullable=False) 23 | slug = db.Column(db.String(255), unique=True, nullable=False) 24 | price = db.Column(db.Integer, nullable=False) 25 | image = db.Column(db.String(255), unique=False, nullable=True) 26 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 27 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 28 | 29 | def to_json(self): 30 | return { 31 | 'id' : self.id, 32 | 'name': self.name, 33 | 'slug': self.slug, 34 | 'price': self.price, 35 | 'image': self.image 36 | } -------------------------------------------------------------------------------- /app/node_modules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Product-Service/81aaf189118e4276135f823dfc83438ef8ff4726/app/node_modules/.gitkeep -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "dependencies": { 4 | "swagger-ui-dist": "~3.5.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/product_api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | product_api_blueprint = Blueprint('product_api', __name__) 4 | 5 | 6 | from . import routes -------------------------------------------------------------------------------- /app/product_api/routes.py: -------------------------------------------------------------------------------- 1 | from flask import json, jsonify, request 2 | from . import product_api_blueprint 3 | from models import db, Product 4 | 5 | 6 | @product_api_blueprint.route("/api/product/docs.json", methods=['GET']) 7 | def swagger_api_docs_yml(): 8 | with open('swagger.json') as fd: 9 | json_data = json.load(fd) 10 | 11 | return jsonify(json_data) 12 | 13 | 14 | @product_api_blueprint.route('/api/products', methods=['GET']) 15 | def products(): 16 | 17 | items = [] 18 | for row in Product.query.all(): 19 | items.append(row.to_json()) 20 | 21 | response = jsonify({ 22 | 'results' : items 23 | }) 24 | 25 | return response 26 | 27 | 28 | @product_api_blueprint.route('/api/product/', methods=['GET']) 29 | def product(slug): 30 | item = Product.query.filter_by(slug=slug).first() 31 | if item is not None: 32 | response = jsonify({'result' : item.to_json() }) 33 | else: 34 | response = jsonify({'message': 'Cannot find product'}), 404 35 | 36 | return response 37 | 38 | 39 | @product_api_blueprint.route('/api/product/create', methods=['POST']) 40 | def post_create(): 41 | 42 | name = request.form['name'] 43 | slug = request.form['slug'] 44 | image = request.form['image'] 45 | price = request.form['price'] 46 | 47 | item = Product() 48 | item.name = name 49 | item.slug = slug 50 | item.image = image 51 | item.price = price 52 | 53 | db.session.add(item) 54 | db.session.commit() 55 | 56 | response = jsonify({'message': 'Product added', 'product': item.to_json()}) 57 | 58 | return response 59 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | atomicwrites==1.2.1 2 | attrs==18.2.0 3 | certifi==2018.11.29 4 | chardet==3.0.4 5 | Click==7.0 6 | Flask==1.0.2 7 | Flask-SQLAlchemy==2.3.2 8 | flask-swagger-ui==3.18.0 9 | idna==2.7 10 | itsdangerous==1.1.0 11 | Jinja2==2.10 12 | MarkupSafe==1.1.1 13 | more-itertools==4.3.0 14 | mysql-connector==2.2.9 15 | pluggy==0.8.0 16 | py==1.7.0 17 | pytest==3.9.1 18 | requests==2.20.0 19 | six==1.11.0 20 | SQLAlchemy==1.2.8 21 | urllib3==1.24.2 22 | Werkzeug==0.16.1 -------------------------------------------------------------------------------- /app/setup.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from product_api import product_api_blueprint 3 | from flask_swagger_ui import get_swaggerui_blueprint 4 | import models 5 | 6 | SWAGGER_URL = '/api/docs' 7 | API_URL = '/api/product/docs.json' 8 | 9 | 10 | def create_app(): 11 | app = Flask(__name__) 12 | 13 | app.config.update(dict( 14 | SECRET_KEY="powerful secretkey", 15 | WTF_CSRF_SECRET_KEY="a csrf secret key", 16 | SQLALCHEMY_DATABASE_URI='mysql+mysqlconnector://root:test@product_db/product', 17 | SQLALCHEMY_TRACK_MODIFICATIONS=False 18 | )) 19 | 20 | models.init_app(app) 21 | models.create_tables(app) 22 | 23 | app.register_blueprint(product_api_blueprint) 24 | 25 | swaggerui_blueprint = get_swaggerui_blueprint( 26 | SWAGGER_URL, 27 | API_URL, 28 | ) 29 | app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) 30 | 31 | return app 32 | -------------------------------------------------------------------------------- /app/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Product service for order management system micro service", 5 | "version": "1.0.0", 6 | "title": "Product Service" 7 | }, 8 | "host": "192.168.99.100:8081", 9 | "basePath": "/api", 10 | "schemes": [ 11 | "http" 12 | ], 13 | "paths": { 14 | "/products": { 15 | "get": { 16 | "tags": [ 17 | "Product" 18 | ], 19 | "summary": "Get all the products", 20 | "description": "", 21 | "produces": [ 22 | "application/json" 23 | ], 24 | "responses": { 25 | "200": { 26 | "description": "successful operation" 27 | } 28 | } 29 | } 30 | }, 31 | "/product/{slug}": { 32 | "get": { 33 | "tags": [ 34 | "Product" 35 | ], 36 | "summary": "Finds a product by it's slug", 37 | "produces": [ 38 | "application/json" 39 | ], 40 | "parameters": [ 41 | { 42 | "name": "slug", 43 | "in": "path", 44 | "description": "Product slug", 45 | "required": true, 46 | "type": "string" 47 | } 48 | ], 49 | "responses": { 50 | "200": { 51 | "description": "successful operation" 52 | }, 53 | "404": { 54 | "description": "Product not found" 55 | } 56 | } 57 | } 58 | }, 59 | "/product/create": { 60 | "post": { 61 | "tags": [ 62 | "Product" 63 | ], 64 | "summary": "Creates a product", 65 | "description": "", 66 | "consumes": [ 67 | "multipart/form-data" 68 | ], 69 | "produces": [ 70 | "application/json" 71 | ], 72 | "parameters": [ 73 | { 74 | "name": "name", 75 | "in": "formData", 76 | "description": "Additional data to pass to server", 77 | "required": true, 78 | "type": "string" 79 | }, 80 | { 81 | "name": "slug", 82 | "in": "formData", 83 | "description": "Product slug", 84 | "required": true, 85 | "type": "string" 86 | }, 87 | { 88 | "name": "image", 89 | "in": "formData", 90 | "description": "Image path", 91 | "required": false, 92 | "type": "string" 93 | }, 94 | { 95 | "name": "price", 96 | "in": "formData", 97 | "description": "Image path", 98 | "required": false, 99 | "type": "integer" 100 | } 101 | ], 102 | "responses": { 103 | "200": { 104 | "description": "successful operation" 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /app/test_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import setup 3 | import json 4 | 5 | 6 | class ProductServiceTestCase(unittest.TestCase): 7 | 8 | def setUp(self): 9 | """Define test variables and initialize app.""" 10 | self.app = setup.create_app() 11 | self.client = self.app.test_client 12 | 13 | def test_products(self): 14 | """Test API can get the products (Get request)""" 15 | res = self.client().get('/api/products') 16 | self.assertEqual(res.status_code, 200) 17 | response = { 18 | "results": [ 19 | { 20 | "id": 1, 21 | "image": "banana.png", 22 | "name": "Product 1", 23 | "price": 2, 24 | "slug": "product-1" 25 | }, 26 | { 27 | "id": 2, 28 | "image": "coffee.png", 29 | "name": "Coffee", 30 | "price": 5, 31 | "slug": "product-2" 32 | }, 33 | { 34 | "id": 3, 35 | "image": "rubber_duck.png", 36 | "name": "Rubber Duck", 37 | "price": 2, 38 | "slug": "product-3" 39 | } 40 | ] 41 | } 42 | 43 | data = json.loads(res.get_data(as_text=True)) 44 | 45 | self.assertEqual(data, response) 46 | 47 | def test_product(self): 48 | """Test API can get a product (Get request)""" 49 | res = self.client().get('/api/product/product-1') 50 | self.assertEqual(res.status_code, 200) 51 | 52 | response = { 53 | "result": { 54 | "id": 1, 55 | "image": "banana.png", 56 | "name": "Product 1", 57 | "price": 2, 58 | "slug": "product-1" 59 | } 60 | } 61 | 62 | data = json.loads(res.get_data(as_text=True)) 63 | 64 | self.assertEqual(data, response) 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | product-db-data: 5 | 6 | services: 7 | product: 8 | build: 9 | context: . 10 | ports: 11 | - 80:5000 12 | links: 13 | - product_db 14 | depends_on: 15 | - product_db 16 | 17 | product_db: 18 | image: mysql:5.7.22 19 | volumes: 20 | - product-db-data:/var/lib/mysql 21 | environment: 22 | - MYSQL_ROOT_PASSWORD=test 23 | - MYSQL_DATABASE=product 24 | restart: always -------------------------------------------------------------------------------- /docs/api/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Product-Service/81aaf189118e4276135f823dfc83438ef8ff4726/docs/api/.gitignore -------------------------------------------------------------------------------- /docs/install/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Hands-on-Microservices-with-Python-Product-Service/81aaf189118e4276135f823dfc83438ef8ff4726/docs/install/.gitignore -------------------------------------------------------------------------------- /docs/install/install.md: -------------------------------------------------------------------------------- 1 | # Product Service 2 | 3 | 4 | ## Add products to the database 5 | 6 | Run the following against the product container 7 | ``` 8 | $ docker exec -i python add_products.py 9 | ``` 10 | 11 | To find the container: 12 | ``` 13 | $ docker ps -a 14 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 15 | 063bfdb6abf6 frontendgit_user "python app.py" About an hour ago Up About an hour 0.0.0.0:8082->5000/tcp frontendgit_user_1 16 | 6210faec3385 frontendgit_product "python app.py" About an hour ago Up About an hour 0.0.0.0:8081->5000/tcp frontendgit_product_1 17 | 9ac86d9588ad frontendgit_order "python app.py" About an hour ago Up About an hour 0.0.0.0:8083->5000/tcp frontendgit_order_1 18 | 5ce04e5859d8 frontendgit_frontend "/bin/sh -c 'python …" About an hour ago Up About an hour 0.0.0.0:80->5000/tcp frontendgit_frontend_1 19 | c451c131818b mysql:5.7.22 "docker-entrypoint.s…" About an hour ago Up About an hour 3306/tcp frontendgit_user_db_1 20 | a280b689942f mysql:5.7.22 "docker-entrypoint.s…" About an hour ago Up About an hour 3306/tcp frontendgit_product_db_1 21 | b86246a5b652 mysql:5.7.22 "docker-entrypoint.s…" About an hour ago Up About an hour 3306/tcp frontendgit_order_db_1 22 | 23 | ``` 24 | 25 | In this case the container name is ``frontendgit_product_1`` -------------------------------------------------------------------------------- /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) --------------------------------------------------------------------------------