├── .gitignore ├── README.md ├── frontend ├── .env ├── .flaskenv ├── .gitignore ├── Dockerfile ├── README.md ├── application │ ├── __init__.py │ ├── frontend │ │ ├── __init__.py │ │ ├── api │ │ │ ├── OrderClient.py │ │ │ ├── ProductClient.py │ │ │ ├── UserClient.py │ │ │ └── __init__.py │ │ ├── forms.py │ │ └── views.py │ ├── static │ │ ├── css │ │ │ └── style.css │ │ └── images │ │ │ ├── product1.jpg │ │ │ ├── product2.jpg │ │ │ └── sample.jpg │ └── templates │ │ ├── _messages.html │ │ ├── admin │ │ └── index.html │ │ ├── base.html │ │ ├── base_col_1.html │ │ ├── base_col_2.html │ │ ├── home │ │ └── index.html │ │ ├── login │ │ └── index.html │ │ ├── macros │ │ ├── _macros_basket.html │ │ └── _macros_form.html │ │ ├── nav_header.html │ │ ├── order │ │ └── thankyou.html │ │ ├── product │ │ └── index.html │ │ └── register │ │ └── index.html ├── config.py ├── docker-compose.yml ├── requirements.txt └── run.py ├── order-service ├── .env ├── .flaskenv ├── .gitignore ├── Dockerfile ├── README.md ├── application │ ├── __init__.py │ ├── models.py │ └── order_api │ │ ├── __init__.py │ │ ├── api │ │ ├── UserClient.py │ │ └── __init__.py │ │ └── routes.py ├── config.py ├── docker-compose.yml ├── requirements.txt └── run.py ├── product-service ├── .env ├── .flaskenv ├── .gitignore ├── Dockerfile ├── README.md ├── application │ ├── __init__.py │ ├── models.py │ └── product_api │ │ ├── __init__.py │ │ └── routes.py ├── config.py ├── docker-compose.yml ├── requirements.txt └── run.py └── user-service ├── .env ├── .flaskenv ├── .gitignore ├── Dockerfile ├── README.md ├── application ├── __init__.py ├── models.py └── user_api │ ├── __init__.py │ └── routes.py ├── config.py ├── docker-compose.yml ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | migrations/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mastering Microservices with Python, Flask, and Docker 2 | Interested in microservices, and how they can be used for increased agility and scalability? 3 | 4 | Microservices is an architectural style and pattern that structures an application as a collection of coherent services. Each service is highly maintainable, testable, loosely coupled, independently deployable, and precisely focused. 5 | 6 | This [course](https://cloudacademy.com/course/mastering-microservices-with-python-flask-docker-1118) takes a hands-on look at microservices using Python, Flask, and Docker. You'll learn how Flask can be used to quickly prototype and build microservices, as well as how to use Docker to host and deploy them. 7 | 8 | :metal: 9 | 10 | ## Project Structure 11 | The Python Flask based microservices project is composed of the following 4 projects: 12 | * [frontend](https://github.com/cloudacademy/python-flask-microservices/tree/master/frontend) 13 | * [user-service](https://github.com/cloudacademy/python-flask-microservices/tree/master/user-service) 14 | * [product-service](https://github.com/cloudacademy/python-flask-microservices/tree/master/product-service) 15 | * [order-service](https://github.com/cloudacademy/python-flask-microservices/tree/master/order-service) 16 | 17 | ## Microservices Setup and Configuration 18 | To launch the end-to-end microservices application perform the following: 19 | 20 | ### Step 1. 21 | Navigate into the [frontend](https://github.com/cloudacademy/python-flask-microservices/tree/master/frontend) directory, and confirm the presence of the ```docker-compose.deploy.yml``` file: 22 | ``` 23 | cd frontend 24 | ls -la 25 | ``` 26 | 27 | ### Step 1. 28 | Create a new Docker network and name it ```micro_network```: 29 | ``` 30 | docker network create micro_network 31 | ``` 32 | 33 | ### Step 2. 34 | Build each of the microservice Docker container images: 35 | ``` 36 | docker-compose -f docker-compose.deploy.yml build 37 | docker images 38 | ``` 39 | 40 | ### Step 3. 41 | Launch the microservice environment: 42 | ``` 43 | docker-compose -f docker-compose.deploy.yml build 44 | docker ps -a 45 | ``` 46 | 47 | ### Step 4. 48 | Prepare each microservice mysql database: 49 | ``` 50 | for service in corder-service cproduct-service cuser-service; 51 | do 52 | docker exec -it $service flask db init 53 | docker exec -it $service flask db migrate 54 | docker exec -it $service flask db upgrade 55 | done 56 | ``` 57 | 58 | ### Step 5. 59 | Populate the product database: 60 | ``` 61 | curl -i -d "name=prod1&slug=prod1&image=product1.jpg&price=100" -X POST localhost:5002/api/product/create 62 | curl -i -d "name=prod2&slug=prod2&image=product2.jpg&price=200" -X POST localhost:5002/api/product/create 63 | ``` 64 | 65 | ### Step 6. 66 | Using your workstations browser - navigate to the following URL and register: 67 | ``` 68 | http://localhost:5000/register 69 | ``` 70 | 71 | ### Step 7. 72 | Back within your terminal, use a mysql client to confirm that a new user registration record was created: 73 | ``` 74 | mysql --host=127.0.0.1 --port=32000 --user=cloudacademy --password=pfm_2020 75 | mysql> show databases; 76 | mysql> use user; 77 | mysql> show tables; 78 | mysql> select * from user; 79 | mysql> exit 80 | ``` 81 | 82 | ### Step 8. 83 | Using your workstations browser - login, and add products into your cart, and then finally click the checkout option 84 | ``` 85 | http://localhost:5000/login 86 | ``` 87 | 88 | ### Step 9. 89 | Back within your terminal, use a mysql client to confirm that a new order has been created: 90 | ``` 91 | mysql --host=127.0.0.1 --port=32002 --user=cloudacademy --password=pfm_2020 92 | mysql> show databases; 93 | mysql> use order; 94 | mysql> show tables; 95 | mysql> select * from order.order; 96 | mysql> select * from order.order_item; 97 | mysql> exit 98 | ``` 99 | 100 | ## Microservices Teardown 101 | Perform the following steps to teardown the microservices environment: 102 | 103 | ### Step 1. 104 | Create a new Docker network and name it ```micro_network```: 105 | ``` 106 | for container in cuser-service cproduct-service corder-service cproduct_dbase cfrontend-app cuser_dbase corder_dbase; 107 | do 108 | docker stop $container 109 | docker rm $container 110 | done 111 | ``` 112 | 113 | ### Step 2. 114 | Remove the container volumes 115 | ``` 116 | for vol in frontend_orderdb_vol frontend_productdb_vol frontend_userdb_vol; 117 | do 118 | docker volume rm $vol 119 | done 120 | ``` 121 | 122 | ### Step 3. 123 | Remove the container network 124 | ``` 125 | docker network rm micro_network 126 | ``` 127 | 128 | ## Python extensions reference 129 | The following Python extensions were used: 130 | 131 | * Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/en/2.x/ 132 | * Flask-Login: https://flask-login.readthedocs.io/en/latest/ 133 | * Flask-Migrate: https://github.com/miguelgrinberg/flask-migrate/ 134 | * Requests: https://requests.readthedocs.io/en/master/ 135 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | #.env 2 | CONFIGURATION_SETUP="config.ProductionConfig" -------------------------------------------------------------------------------- /frontend/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /frontendapp/requirements.txt 4 | WORKDIR /frontendapp 5 | RUN pip install -r requirements.txt 6 | COPY . /frontendapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | ``` 4 | docker network ls 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t frontend-srv . 7 | docker run -p 5000:5000 --detach --name frontend-service --net=micro_network frontend-srv 8 | ``` -------------------------------------------------------------------------------- /frontend/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_bootstrap import Bootstrap 6 | from flask_login import LoginManager 7 | 8 | login_manager = LoginManager() 9 | bootstrap = Bootstrap() 10 | UPLOAD_FOLDER = 'application/static/images' 11 | 12 | 13 | def create_app(): 14 | app = Flask(__name__, static_folder='static') 15 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 16 | 17 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 18 | app.config.from_object(environment_configuration) 19 | 20 | login_manager.init_app(app) 21 | login_manager.login_message = "You must be login to access this page." 22 | login_manager.login_view = "frontend.login" 23 | 24 | bootstrap.init_app(app) 25 | 26 | with app.app_context(): 27 | from .frontend import frontend_blueprint 28 | app.register_blueprint(frontend_blueprint) 29 | 30 | return app 31 | -------------------------------------------------------------------------------- /frontend/application/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | # application/frontend/__init__.py 2 | from flask import Blueprint 3 | 4 | frontend_blueprint = Blueprint('frontend', __name__) 5 | 6 | from . import views 7 | -------------------------------------------------------------------------------- /frontend/application/frontend/api/OrderClient.py: -------------------------------------------------------------------------------- 1 | # application/frontend/api/OrderClient.py 2 | from flask import session 3 | import requests 4 | 5 | 6 | class OrderClient: 7 | @staticmethod 8 | def get_order(): 9 | headers = { 10 | 'Authorization': 'Basic ' + session['user_api_key'] 11 | } 12 | url = 'http://corder-service:5003/api/order' 13 | response = requests.request(method="GET", url=url, headers=headers) 14 | order = response.json() 15 | return order 16 | 17 | @staticmethod 18 | def post_add_to_cart(product_id, qty=1): 19 | payload = { 20 | 'product_id': product_id, 21 | 'qty': qty 22 | } 23 | url = 'http://corder-service:5003/api/order/add-item' 24 | 25 | headers = { 26 | 'Authorization': 'Basic ' + session['user_api_key'] 27 | } 28 | response = requests.request("POST", url=url, data=payload, headers=headers) 29 | if response: 30 | order = response.json() 31 | return order 32 | 33 | @staticmethod 34 | def post_checkout(): 35 | url = 'http://corder-service:5003/api/order/checkout' 36 | 37 | headers = { 38 | 'Authorization': 'Basic ' + session['user_api_key'] 39 | } 40 | response = requests.request("POST", url=url, headers=headers) 41 | order = response.json() 42 | return order 43 | 44 | @staticmethod 45 | def get_order_from_session(): 46 | default_order = { 47 | 'items': {}, 48 | 'total': 0, 49 | } 50 | return session.get('order', default_order) 51 | -------------------------------------------------------------------------------- /frontend/application/frontend/api/ProductClient.py: -------------------------------------------------------------------------------- 1 | # application/frontend/api/ProductClient.py 2 | import requests 3 | 4 | 5 | class ProductClient: 6 | 7 | @staticmethod 8 | def get_products(): 9 | r = requests.get('http://cproduct-service:5002/api/products') 10 | products = r.json() 11 | return products 12 | 13 | @staticmethod 14 | def get_product(slug): 15 | response = requests.request(method="GET", url='http://cproduct-service:5002/api/product/' + slug) 16 | product = response.json() 17 | return product 18 | -------------------------------------------------------------------------------- /frontend/application/frontend/api/UserClient.py: -------------------------------------------------------------------------------- 1 | # application/frontend/api/UserClient.py 2 | import requests 3 | from flask import session, request 4 | 5 | 6 | class UserClient: 7 | @staticmethod 8 | def post_login(form): 9 | api_key = False 10 | payload = { 11 | 'username': form.username.data, 12 | 'password': form.password.data 13 | } 14 | url = 'http://cuser-service:5001/api/user/login' 15 | response = requests.request("POST", url=url, data=payload) 16 | if response: 17 | d = response.json() 18 | print("This is response from user api: " + str(d)) 19 | if d['api_key'] is not None: 20 | api_key = d['api_key'] 21 | return api_key 22 | 23 | @staticmethod 24 | def get_user(): 25 | 26 | headers = { 27 | 'Authorization': 'Basic ' + session['user_api_key'] 28 | } 29 | url = 'http://cuser-service:5001/api/user' 30 | response = requests.request(method="GET", url=url, headers=headers) 31 | user = response.json() 32 | return user 33 | 34 | @staticmethod 35 | def post_user_create(form): 36 | user = False 37 | payload = { 38 | 'email': form.email.data, 39 | 'password': form.password.data, 40 | 'first_name': form.first_name.data, 41 | 'last_name': form.last_name.data, 42 | 'username': form.username.data 43 | } 44 | url = 'http://cuser-service:5001/api/user/create' 45 | response = requests.request("POST", url=url, data=payload) 46 | if response: 47 | user = response.json() 48 | return user 49 | 50 | @staticmethod 51 | def does_exist(username): 52 | url = 'http://cuser-service:5001/api/user/' + username + '/exists' 53 | response = requests.request("GET", url=url) 54 | return response.status_code == 200 55 | 56 | -------------------------------------------------------------------------------- /frontend/application/frontend/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/python-flask-microservices/6da3be279437e8975294cb59e02bb0ba3366bff7/frontend/application/frontend/api/__init__.py -------------------------------------------------------------------------------- /frontend/application/frontend/forms.py: -------------------------------------------------------------------------------- 1 | # application/frontend/forms.py 2 | from flask_wtf import FlaskForm 3 | from wtforms import StringField, PasswordField, SubmitField, HiddenField, IntegerField 4 | 5 | from wtforms.validators import DataRequired, Email 6 | 7 | 8 | class LoginForm(FlaskForm): 9 | username = StringField('Username', validators=[DataRequired()]) 10 | password = PasswordField('Password', validators=[DataRequired()]) 11 | submit = SubmitField('Login') 12 | 13 | 14 | class RegistrationForm(FlaskForm): 15 | username = StringField('Username', validators=[DataRequired()]) 16 | first_name = StringField('First name', validators=[DataRequired()]) 17 | last_name = StringField('Last name', validators=[DataRequired()]) 18 | email = StringField('Email address', validators=[DataRequired(), Email()]) 19 | password = PasswordField('Password', validators=[DataRequired()]) 20 | submit = SubmitField('Register') 21 | 22 | 23 | class OrderItemForm(FlaskForm): 24 | product_id = HiddenField(validators=[DataRequired()]) 25 | quantity = IntegerField(validators=[DataRequired()]) 26 | order_id = HiddenField() 27 | submit = SubmitField('Update') 28 | 29 | 30 | class ItemForm(FlaskForm): 31 | product_id = HiddenField(validators=[DataRequired()]) 32 | quantity = HiddenField(validators=[DataRequired()], default=1) 33 | -------------------------------------------------------------------------------- /frontend/application/frontend/views.py: -------------------------------------------------------------------------------- 1 | # application/frontend/views.py 2 | import requests 3 | from . import forms 4 | from . import frontend_blueprint 5 | from .. import login_manager 6 | from .api.UserClient import UserClient 7 | from .api.ProductClient import ProductClient 8 | from .api.OrderClient import OrderClient 9 | from flask import render_template, session, redirect, url_for, flash, request 10 | 11 | from flask_login import current_user 12 | 13 | 14 | @login_manager.user_loader 15 | def load_user(user_id): 16 | return None 17 | 18 | 19 | @frontend_blueprint.route('/', methods=['GET']) 20 | def home(): 21 | if current_user.is_authenticated: 22 | session['order'] = OrderClient.get_order_from_session() 23 | 24 | try: 25 | products = ProductClient.get_products() 26 | except requests.exceptions.ConnectionError: 27 | products = { 28 | 'results': [] 29 | } 30 | 31 | return render_template('home/index.html', products=products) 32 | 33 | 34 | @frontend_blueprint.route('/register', methods=['GET', 'POST']) 35 | def register(): 36 | form = forms.RegistrationForm(request.form) 37 | if request.method == "POST": 38 | if form.validate_on_submit(): 39 | username = form.username.data 40 | 41 | # Search for existing user 42 | user = UserClient.does_exist(username) 43 | if user: 44 | # Existing user found 45 | flash('Please try another username', 'error') 46 | return render_template('register/index.html', form=form) 47 | else: 48 | # Attempt to create new user 49 | user = UserClient.post_user_create(form) 50 | if user: 51 | flash('Thanks for registering, please login', 'success') 52 | return redirect(url_for('frontend.login')) 53 | 54 | else: 55 | flash('Errors found', 'error') 56 | 57 | return render_template('register/index.html', form=form) 58 | 59 | 60 | @frontend_blueprint.route('/login', methods=['GET', 'POST']) 61 | def login(): 62 | if current_user.is_authenticated: 63 | return redirect(url_for('frontend.home')) 64 | form = forms.LoginForm() 65 | if request.method == "POST": 66 | if form.validate_on_submit(): 67 | api_key = UserClient.post_login(form) 68 | if api_key: 69 | session['user_api_key'] = api_key 70 | user = UserClient.get_user() 71 | session['user'] = user['result'] 72 | 73 | order = OrderClient.get_order() 74 | if order.get('result', False): 75 | session['order'] = order['result'] 76 | 77 | flash('Welcome back, ' + user['result']['username'], 'success') 78 | return redirect(url_for('frontend.home')) 79 | else: 80 | flash('Cannot login', 'error') 81 | else: 82 | flash('Errors found', 'error') 83 | return render_template('login/index.html', form=form) 84 | 85 | 86 | @frontend_blueprint.route('/logout', methods=['GET']) 87 | def logout(): 88 | session.clear() 89 | return redirect(url_for('frontend.home')) 90 | 91 | 92 | @frontend_blueprint.route('/product/', methods=['GET', 'POST']) 93 | def product(slug): 94 | response = ProductClient.get_product(slug) 95 | item = response['result'] 96 | 97 | form = forms.ItemForm(product_id=item['id']) 98 | 99 | if request.method == "POST": 100 | if 'user' not in session: 101 | flash('Please login', 'error') 102 | return redirect(url_for('frontend.login')) 103 | order = OrderClient.post_add_to_cart(product_id=item['id'], qty=1) 104 | session['order'] = order['result'] 105 | flash('Order has been updated', 'success') 106 | return render_template('product/index.html', product=item, form=form) 107 | 108 | 109 | @frontend_blueprint.route('/checkout', methods=['GET']) 110 | def summary(): 111 | if 'user' not in session: 112 | flash('Please login', 'error') 113 | return redirect(url_for('frontend.login')) 114 | 115 | if 'order' not in session: 116 | flash('No order found', 'error') 117 | return redirect(url_for('frontend.home')) 118 | order = OrderClient.get_order() 119 | 120 | if len(order['result']['items']) == 0: 121 | flash('No order found', 'error') 122 | return redirect(url_for('frontend.home')) 123 | 124 | OrderClient.post_checkout() 125 | 126 | return redirect(url_for('frontend.thank_you')) 127 | 128 | @frontend_blueprint.route('/order/thank-you', methods=['GET']) 129 | def thank_you(): 130 | if 'user' not in session: 131 | flash('Please login', 'error') 132 | return redirect(url_for('frontend.login')) 133 | 134 | if 'order' not in session: 135 | flash('No order found', 'error') 136 | return redirect(url_for('frontend.home')) 137 | 138 | session.pop('order', None) 139 | flash('Thank you for your order', 'success') 140 | 141 | return render_template('order/thankyou.html') -------------------------------------------------------------------------------- /frontend/application/static/css/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | body, h1, h2, h3 { 7 | font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-weight: 700; 9 | } 10 | 11 | body{ 12 | background-color:#DDD; 13 | } 14 | 15 | a, .navbar-default .navbar-brand, .navbar-default .navbar-nav > li > a { 16 | color: #aec251; 17 | } 18 | 19 | a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav > li > a:hover { 20 | color: #687430; 21 | } 22 | 23 | footer { 24 | padding: 50px 0; 25 | background-color: #f8f8f8; 26 | } 27 | 28 | p.copyright { 29 | margin: 15px 0 0; 30 | } 31 | 32 | .alert-info { 33 | width: 50%; 34 | margin: auto; 35 | color: #687430; 36 | background-color: #e6ecca; 37 | border-color: #aec251; 38 | } 39 | 40 | .btn-default { 41 | border-color: #aec251; 42 | color: #aec251; 43 | } 44 | 45 | .btn-default:hover { 46 | background-color: #aec251; 47 | } 48 | 49 | .center { 50 | margin: auto; 51 | width: 50%; 52 | padding: 10px; 53 | } 54 | 55 | .content-section { 56 | padding: 50px 0; 57 | border-top: 1px solid #e7e7e7; 58 | } 59 | 60 | 61 | .img-block{ 62 | text-align:center; 63 | } 64 | .img-block img{ 65 | 66 | max-width:200px; 67 | max-height:200px; 68 | } 69 | -------------------------------------------------------------------------------- /frontend/application/static/images/product1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/python-flask-microservices/6da3be279437e8975294cb59e02bb0ba3366bff7/frontend/application/static/images/product1.jpg -------------------------------------------------------------------------------- /frontend/application/static/images/product2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/python-flask-microservices/6da3be279437e8975294cb59e02bb0ba3366bff7/frontend/application/static/images/product2.jpg -------------------------------------------------------------------------------- /frontend/application/static/images/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/python-flask-microservices/6da3be279437e8975294cb59e02bb0ba3366bff7/frontend/application/static/images/sample.jpg -------------------------------------------------------------------------------- /frontend/application/templates/_messages.html: -------------------------------------------------------------------------------- 1 | {% for error_message in get_flashed_messages(category_filter=["error"]) %} 2 |
{{ error_message }}
3 | {% endfor %} 4 | 5 | {% for success_message in get_flashed_messages(category_filter=["success"]) %} 6 |
{{ success_message }}
7 | {% endfor %} 8 | 9 | {% for info_message in get_flashed_messages(category_filter=["info"]) %} 10 |
{{ info_message }}
11 | {% endfor %} -------------------------------------------------------------------------------- /frontend/application/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Product page{% endblock %} 4 | 5 | {% block pageContent %} 6 | {{ message }} 7 |

Add Product

8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %} {% endblock %} 6 | 7 | 8 | {% block styles %} 9 | 10 | 11 | 12 | 15 | 16 | {% endblock %} 17 | 18 | 19 | 20 | 21 | {% block content %} 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /frontend/application/templates/base_col_1.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include 'nav_header.html' %} 5 |
6 | 7 | {% include '_messages.html' %} 8 | {% block pageContent %} {% endblock %} 9 | 10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/base_col_2.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block content %} 6 | {% include 'nav_header.html' %} 7 |
8 | 9 | {% include '_messages.html' %} 10 |
11 |
12 | 13 |
14 |
15 | {% block pageContent %} {% endblock %} 16 |
17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% block title %}Home Page{% endblock %} 3 | 4 | {% block pageContent %} 5 | 6 | {% if products | length > 0 %} 7 | {% for product in products.results %} 8 | 9 | {% set url = "/product/" + product.slug %} 10 | {% set imageSRC = "images/" + product.image %} 11 | 12 |
13 |
14 |
15 |

{{ product.name }}

16 |
17 |
18 |
19 | 20 |
21 |
22 | 28 |
29 |
30 | {% endfor %} 31 | 32 | {% else %} 33 | No products found. 34 | {% endif %} 35 | 36 | 37 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/login/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Login{% endblock %} 4 | 5 | {% block pageContent %} 6 |

Login

7 | {{ message }} 8 | 9 |
10 | {{ form.hidden_tag() }} 11 | {{ render_field(form.username) }} 12 | {{ render_field(form.password) }} 13 | {{ form.submit(class_="btn btn-success pull-right") }} 14 | 15 | 16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/macros/_macros_basket.html: -------------------------------------------------------------------------------- 1 | {{ session['order'] | pprint }} 2 | {% macro count_items()%} 3 | {% set counter = namespace(a=0) %} 4 | {% if session['order'] %} 5 | {% for item in session['order']['items'] %} 6 | {% set counter.a = counter.a + item['quantity'] %} 7 | {% endfor %} 8 | {% endif %} 9 | {{ counter.a }} 10 | {% endmacro %} -------------------------------------------------------------------------------- /frontend/application/templates/macros/_macros_form.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |
3 | {{ field.label(class_="form-label") }}: 4 | {% if field.errors %} 5 | 6 | {% for error in field.errors %} 7 | {{ error }} 8 | {% endfor %} 9 | 10 | {% endif %} 11 | 12 | 13 | {{ field(class_="form-control") }} 14 |
15 | {% endmacro %} 16 | 17 | 18 | 19 | {% macro render_field_without_label(field) %} 20 |
{{ field(**kwargs)|safe }} 21 | {% if field.errors %} 22 | 23 | {% for error in field.errors %} 24 | {{ error }} 25 | {% endfor %} 26 | 27 | {% endif %} 28 |
29 | {% endmacro %} 30 | 31 | 32 | {% macro render_boolean_field(field) %} 33 |
{{ field(**kwargs)|safe }} {{ field.label }} 34 | {% if field.errors %} 35 | 36 | {% for error in field.errors %} 37 | {{ error }} 38 | {% endfor %} 39 | 40 | {% endif %} 41 |
42 | {% endmacro %} 43 | 44 | 45 | {% macro render_errors(field) %} 46 | {% if field.errors %} 47 | 48 | {% for error in field.errors %} 49 | {{ error }} 50 | {% endfor %} 51 | 52 | {% endif %} 53 | {% endmacro %} 54 | -------------------------------------------------------------------------------- /frontend/application/templates/nav_header.html: -------------------------------------------------------------------------------- 1 | {% from "macros/_macros_basket.html" import count_items %} 2 |
3 | 24 |
-------------------------------------------------------------------------------- /frontend/application/templates/order/thankyou.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% block title %}Thank you{% endblock %} 3 | 4 | {% block pageContent %} 5 |

Thank you

6 |

your order is being processed

7 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/product/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Product page{% endblock %} 4 | 5 | {% block pageContent %} 6 | 7 | {% set url = "/product/" + product.slug %} 8 | {% set imageSRC = "images/" + product.image %} 9 |
10 |
11 |
12 |

{{ product.name }}

13 |
14 |
15 |
16 | 17 |
18 |
19 | 28 |
29 |
30 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/register/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Register{% endblock %} 4 | 5 | {% block pageContent %} 6 |

Register

7 | {{ message }} 8 | 9 |
10 | {{ form.hidden_tag() }} 11 | {{ render_field(form.username) }} 12 | {{ render_field(form.first_name) }} 13 | {{ render_field(form.last_name) }} 14 | {{ render_field(form.email) }} 15 | {{ render_field(form.password) }} 16 | {{ form.submit(class_="btn btn-success pull-right") }} 17 | 18 |
19 | 20 | {% endblock %} -------------------------------------------------------------------------------- /frontend/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | 7 | if os.path.exists(dotenv_path): 8 | load_dotenv(dotenv_path) 9 | 10 | 11 | class Config: 12 | SECRET_KEY = 'y2BH8xD9pyZhDT5qkyZZRgjcJCMHdQ' 13 | WTF_CSRF_SECRET_KEY = 'VyOyqv5Fm3Hs3qB1AmNeeuvPpdRqTJbTs5wKvWCS' 14 | 15 | 16 | class DevelopmentConfig(Config): 17 | ENV = "development" 18 | DEBUG = True 19 | 20 | 21 | class ProductionConfig(Config): 22 | ENV = "production" 23 | DEBUG = False 24 | 25 | -------------------------------------------------------------------------------- /frontend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.deploy.yml 2 | version: '3.8' 3 | 4 | volumes: 5 | userdb_vol: 6 | productdb_vol: 7 | orderdb_vol: 8 | 9 | networks: 10 | micro_network: 11 | name: micro_network 12 | 13 | services: 14 | user-api: 15 | container_name: cuser-service 16 | build: 17 | context: ../user-service 18 | ports: 19 | - "5001:5001" 20 | depends_on: 21 | - user-db 22 | networks: 23 | - micro_network 24 | restart: always 25 | 26 | user-db: 27 | container_name: cuser_dbase 28 | image: mysql:8 29 | ports: 30 | - "32000:3306" 31 | environment: 32 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 33 | MYSQL_DATABASE: user 34 | MYSQL_USER: cloudacademy 35 | MYSQL_PASSWORD: pfm_2020 36 | networks: 37 | - micro_network 38 | volumes: 39 | - userdb_vol:/var/lib/mysql 40 | 41 | product-api: 42 | container_name: cproduct-service 43 | build: 44 | context: ../product-service 45 | ports: 46 | - "5002:5002" 47 | depends_on: 48 | - product-db 49 | networks: 50 | - micro_network 51 | restart: always 52 | 53 | product-db: 54 | container_name: cproduct_dbase 55 | image: mysql:8 56 | ports: 57 | - "32001:3306" 58 | environment: 59 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 60 | MYSQL_DATABASE: product 61 | MYSQL_USER: cloudacademy 62 | MYSQL_PASSWORD: pfm_2020 63 | networks: 64 | - micro_network 65 | volumes: 66 | - productdb_vol:/var/lib/mysql 67 | 68 | order-api: 69 | container_name: corder-service 70 | build: 71 | context: ../order-service 72 | ports: 73 | - "5003:5003" 74 | depends_on: 75 | - order-db 76 | networks: 77 | - micro_network 78 | restart: always 79 | 80 | order-db: 81 | container_name: corder_dbase 82 | image: mysql:8 83 | ports: 84 | - "32002:3306" 85 | environment: 86 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 87 | MYSQL_DATABASE: order 88 | MYSQL_USER: cloudacademy 89 | MYSQL_PASSWORD: pfm_2020 90 | networks: 91 | - micro_network 92 | volumes: 93 | - orderdb_vol:/var/lib/mysql 94 | 95 | frontend-app: 96 | container_name: cfrontend-app 97 | build: 98 | context: . 99 | ports: 100 | - "5000:5000" 101 | networks: 102 | - micro_network 103 | restart: always -------------------------------------------------------------------------------- /frontend/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | idna==2.9 20 | itsdangerous==1.1.0 21 | Jinja2==2.11.1 22 | Mako==1.1.2 23 | MarkupSafe==1.1.1 24 | marshmallow==3.5.1 25 | passlib==1.7.2 26 | protobuf==3.6.1 27 | pycparser==2.20 28 | PyMySQL==0.9.3 29 | python-dateutil==2.8.1 30 | python-dotenv==0.12.0 31 | python-editor==1.0.4 32 | requests==2.23.0 33 | six==1.14.0 34 | SQLAlchemy==1.3.15 35 | urllib3==1.25.8 36 | visitor==0.1.3 37 | Werkzeug==1.0.1 38 | WTForms==2.2.1 39 | -------------------------------------------------------------------------------- /frontend/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app 3 | 4 | app = create_app() 5 | 6 | if __name__ == '__main__': 7 | app.run(host='0.0.0.0', port=5000) -------------------------------------------------------------------------------- /order-service/.env: -------------------------------------------------------------------------------- 1 | # .env 2 | CONFIGURATION_SETUP="config.ProductionConfig" -------------------------------------------------------------------------------- /order-service/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /order-service/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /order-service/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /orderapp/requirements.txt 4 | WORKDIR /orderapp 5 | RUN pip install -r requirements.txt 6 | COPY . /orderapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /order-service/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | ``` 4 | docker network ls 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t order-srv . 7 | docker run -p 5003:5003 --detach --name order-service --net=micro_network order-srv 8 | ``` -------------------------------------------------------------------------------- /order-service/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | db = SQLAlchemy() 8 | 9 | 10 | def create_app(): 11 | app = Flask(__name__) 12 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 13 | app.config.from_object(environment_configuration) 14 | 15 | db.init_app(app) 16 | 17 | with app.app_context(): 18 | from .order_api import order_api_blueprint 19 | app.register_blueprint(order_api_blueprint) 20 | return app 21 | -------------------------------------------------------------------------------- /order-service/application/models.py: -------------------------------------------------------------------------------- 1 | # application/models.py 2 | from . import db 3 | from datetime import datetime 4 | 5 | class Order(db.Model): 6 | id = db.Column(db.Integer, primary_key=True) 7 | user_id = db.Column(db.Integer) 8 | items = db.relationship('OrderItem', backref='orderItem') 9 | is_open = db.Column(db.Boolean, default=True) 10 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 11 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 12 | 13 | def create(self, user_id): 14 | self.user_id = user_id 15 | self.is_open = True 16 | return self 17 | 18 | def to_json(self): 19 | items = [] 20 | for i in self.items: 21 | items.append(i.to_json()) 22 | 23 | return { 24 | 'items': items, 25 | 'is_open': self.is_open, 26 | 'user_id': self.user_id 27 | } 28 | 29 | 30 | class OrderItem(db.Model): 31 | id = db.Column(db.Integer, primary_key=True) 32 | order_id = db.Column(db.Integer, db.ForeignKey('order.id')) 33 | product_id = db.Column(db.Integer) 34 | quantity = db.Column(db.Integer, default=1) 35 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 36 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 37 | 38 | def __init__(self, product_id, quantity): 39 | self.product_id = product_id 40 | self.quantity = quantity 41 | 42 | def to_json(self): 43 | return { 44 | 'product': self.product_id, 45 | 'quantity': self.quantity 46 | } -------------------------------------------------------------------------------- /order-service/application/order_api/__init__.py: -------------------------------------------------------------------------------- 1 | # application/order_api/__init__.py 2 | from flask import Blueprint 3 | 4 | order_api_blueprint = Blueprint('order_api', __name__) 5 | 6 | from . import routes -------------------------------------------------------------------------------- /order-service/application/order_api/api/UserClient.py: -------------------------------------------------------------------------------- 1 | # application/order_api/api/UserClient.py 2 | import requests 3 | 4 | 5 | class UserClient: 6 | @staticmethod 7 | def get_user(api_key): 8 | headers = { 9 | 'Authorization': api_key 10 | } 11 | response = requests.request(method="GET", url='http://cuser-service:5001/api/user', headers=headers) 12 | if response.status_code == 401: 13 | return False 14 | user = response.json() 15 | return user 16 | -------------------------------------------------------------------------------- /order-service/application/order_api/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/python-flask-microservices/6da3be279437e8975294cb59e02bb0ba3366bff7/order-service/application/order_api/api/__init__.py -------------------------------------------------------------------------------- /order-service/application/order_api/routes.py: -------------------------------------------------------------------------------- 1 | # application/order_api/routes.py 2 | from flask import jsonify, request, make_response 3 | from . import order_api_blueprint 4 | from .. import db 5 | from ..models import Order, OrderItem 6 | from .api.UserClient import UserClient 7 | 8 | 9 | @order_api_blueprint.route('/api/orders', methods=['GET']) 10 | def orders(): 11 | items = [] 12 | for row in Order.query.all(): 13 | items.append(row.to_json()) 14 | 15 | response = jsonify(items) 16 | return response 17 | 18 | 19 | @order_api_blueprint.route('/api/order/add-item', methods=['POST']) 20 | def order_add_item(): 21 | api_key = request.headers.get('Authorization') 22 | response = UserClient.get_user(api_key) 23 | 24 | if not response: 25 | return make_response(jsonify({'message': 'Not logged in'}), 401) 26 | 27 | user = response['result'] 28 | p_id = int(request.form['product_id']) 29 | qty = int(request.form['qty']) 30 | u_id = int(user['id']) 31 | 32 | known_order = Order.query.filter_by(user_id=u_id, is_open=1).first() 33 | 34 | if known_order is None: 35 | known_order = Order() 36 | known_order.is_open = True 37 | known_order.user_id = u_id 38 | 39 | order_item = OrderItem(p_id, qty) 40 | known_order.items.append(order_item) 41 | else: 42 | found = False 43 | 44 | for item in known_order.items: 45 | if item.product_id == p_id: 46 | found = True 47 | item.quantity += qty 48 | 49 | if found is False: 50 | order_item = OrderItem(p_id, qty) 51 | known_order.items.append(order_item) 52 | 53 | db.session.add(known_order) 54 | db.session.commit() 55 | response = jsonify({'result': known_order.to_json()}) 56 | return response 57 | 58 | 59 | @order_api_blueprint.route('/api/order', methods=['GET']) 60 | def order(): 61 | api_key = request.headers.get('Authorization') 62 | 63 | response = UserClient.get_user(api_key) 64 | 65 | if not response: 66 | return make_response(jsonify({'message': 'Not logged in'}), 401) 67 | 68 | user = response['result'] 69 | open_order = Order.query.filter_by(user_id=user['id'], is_open=1).first() 70 | 71 | if open_order is None: 72 | response = jsonify({'message': 'No order found'}) 73 | else: 74 | response = jsonify({'result': open_order.to_json()}) 75 | return response 76 | 77 | 78 | @order_api_blueprint.route('/api/order/checkout', methods=['POST']) 79 | def checkout(): 80 | api_key = request.headers.get('Authorization') 81 | 82 | response = UserClient.get_user(api_key) 83 | 84 | if not response: 85 | return make_response(jsonify({'message': 'Not logged in'}), 401) 86 | 87 | user = response['result'] 88 | 89 | order_model = Order.query.filter_by(user_id=user['id'], is_open=1).first() 90 | order_model.is_open = 0 91 | 92 | db.session.add(order_model) 93 | db.session.commit() 94 | 95 | response = jsonify({'result': order_model.to_json()}) 96 | return response 97 | -------------------------------------------------------------------------------- /order-service/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | if os.path.exists(dotenv_path): 7 | load_dotenv(dotenv_path) 8 | 9 | 10 | class Config: 11 | SQLALCHEMY_TRACK_MODIFICATIONS = False 12 | 13 | 14 | class DevelopmentConfig(Config): 15 | ENV = "development" 16 | DEBUG = True 17 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@host.docker.internal:3306/order_dev' 18 | SQLALCHEMY_ECHO = True 19 | 20 | 21 | class ProductionConfig(Config): 22 | ENV = "production" 23 | DEBUG = False 24 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@order-db:3306/order' 25 | SQLALCHEMY_ECHO = False 26 | 27 | 28 | -------------------------------------------------------------------------------- /order-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | volumes: 4 | orderdb_vol: 5 | 6 | networks: 7 | micro_network: 8 | external: 9 | name: micro_network 10 | 11 | services: 12 | order-api: 13 | container_name: corder-service 14 | build: 15 | context: . 16 | ports: 17 | - "5003:5003" 18 | depends_on: 19 | - order-db 20 | networks: 21 | - micro_network 22 | restart: always 23 | 24 | order-db: 25 | container_name: corder_dbase 26 | image: mysql:8 27 | ports: 28 | - "32002:3306" 29 | environment: 30 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 31 | MYSQL_DATABASE: order 32 | MYSQL_USER: cloudacademy 33 | MYSQL_PASSWORD: pfm_2020 34 | networks: 35 | - micro_network 36 | volumes: 37 | - orderdb_vol:/var/lib/mysql -------------------------------------------------------------------------------- /order-service/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | idna==2.9 20 | itsdangerous==1.1.0 21 | Jinja2==2.11.1 22 | Mako==1.1.2 23 | MarkupSafe==1.1.1 24 | marshmallow==3.5.1 25 | passlib==1.7.2 26 | protobuf==3.6.1 27 | pycparser==2.20 28 | PyMySQL==0.9.3 29 | python-dateutil==2.8.1 30 | python-dotenv==0.12.0 31 | python-editor==1.0.4 32 | requests==2.23.0 33 | six==1.14.0 34 | SQLAlchemy==1.3.15 35 | urllib3==1.25.8 36 | visitor==0.1.3 37 | Werkzeug==1.0.1 38 | WTForms==2.2.1 39 | -------------------------------------------------------------------------------- /order-service/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app, db 3 | from flask_migrate import Migrate 4 | from application import models 5 | 6 | app = create_app() 7 | migrate = Migrate(app, db) 8 | 9 | if __name__ == '__main__': 10 | app.run(host='0.0.0.0', port=5003) 11 | -------------------------------------------------------------------------------- /product-service/.env: -------------------------------------------------------------------------------- 1 | #.env 2 | CONFIGURATION_SETUP="config.ProductionConfig" -------------------------------------------------------------------------------- /product-service/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /product-service/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /product-service/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /productapp/requirements.txt 4 | WORKDIR /productapp 5 | RUN pip install -r requirements.txt 6 | COPY . /productapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /product-service/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | ``` 4 | docker network ls 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t product-srv . 7 | docker run -p 5002:5002 --detach --name product-service --net=micro_network product-srv 8 | ``` -------------------------------------------------------------------------------- /product-service/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | db = SQLAlchemy() 8 | 9 | 10 | def create_app(): 11 | app = Flask(__name__) 12 | 13 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 14 | app.config.from_object(environment_configuration) 15 | 16 | db.init_app(app) 17 | 18 | with app.app_context(): 19 | from .product_api import product_api_blueprint 20 | app.register_blueprint(product_api_blueprint) 21 | return app 22 | -------------------------------------------------------------------------------- /product-service/application/models.py: -------------------------------------------------------------------------------- 1 | # application/models.py 2 | from . import db 3 | from datetime import datetime 4 | 5 | 6 | class Product(db.Model): 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(255), unique=True, nullable=False) 9 | slug = db.Column(db.String(255), unique=True, nullable=False) 10 | price = db.Column(db.Integer, nullable=False) 11 | image = db.Column(db.String(255), unique=False, nullable=True) 12 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 13 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 14 | 15 | def to_json(self): 16 | return { 17 | 'id': self.id, 18 | 'name': self.name, 19 | 'slug': self.slug, 20 | 'price': self.price, 21 | 'image': self.image 22 | } 23 | -------------------------------------------------------------------------------- /product-service/application/product_api/__init__.py: -------------------------------------------------------------------------------- 1 | # application/product_api/__init__.py 2 | from flask import Blueprint 3 | 4 | product_api_blueprint = Blueprint('product_api', __name__) 5 | 6 | from . import routes 7 | -------------------------------------------------------------------------------- /product-service/application/product_api/routes.py: -------------------------------------------------------------------------------- 1 | # application/product_api/routes.py 2 | from . import product_api_blueprint 3 | from .. import db 4 | from ..models import Product 5 | from flask import jsonify, request 6 | 7 | 8 | @product_api_blueprint.route('/api/products', methods=['GET']) 9 | def products(): 10 | items = [] 11 | for row in Product.query.all(): 12 | items.append(row.to_json()) 13 | 14 | response = jsonify({'results': items}) 15 | return response 16 | 17 | 18 | @product_api_blueprint.route('/api/product/create', methods=['POST']) 19 | def post_create(): 20 | name = request.form['name'] 21 | slug = request.form['slug'] 22 | image = request.form['image'] 23 | price = request.form['price'] 24 | 25 | item = Product() 26 | item.name = name 27 | item.slug = slug 28 | item.image = image 29 | item.price = price 30 | 31 | db.session.add(item) 32 | db.session.commit() 33 | 34 | response = jsonify({'message': 'Product added', 'product': item.to_json()}) 35 | return response 36 | 37 | 38 | @product_api_blueprint.route('/api/product/', methods=['GET']) 39 | def product(slug): 40 | item = Product.query.filter_by(slug=slug).first() 41 | if item is not None: 42 | response = jsonify({'result': item.to_json()}) 43 | else: 44 | response = jsonify({'message': 'Cannot find product'}), 404 45 | return response 46 | -------------------------------------------------------------------------------- /product-service/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | if os.path.exists(dotenv_path): 7 | load_dotenv(dotenv_path) 8 | 9 | 10 | class Config: 11 | SQLALCHEMY_TRACK_MODIFICATIONS = False 12 | 13 | 14 | class DevelopmentConfig(Config): 15 | ENV = "development" 16 | DEBUG = True 17 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@localhost:3306/product_dev' 18 | 19 | 20 | class ProductionConfig(Config): 21 | ENV = "production" 22 | DEBUG = False 23 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@product-db:3306/product' 24 | -------------------------------------------------------------------------------- /product-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | 4 | volumes: 5 | productdb_vol: 6 | 7 | networks: 8 | micro_network: 9 | external: 10 | name: micro_network 11 | 12 | services: 13 | product-api: 14 | container_name: cproduct-service 15 | build: 16 | context: . 17 | ports: 18 | - "5002:5002" 19 | depends_on: 20 | - product-db 21 | networks: 22 | - micro_network 23 | restart: always 24 | 25 | product-db: 26 | container_name: cproduct_dbase 27 | image: mysql:8 28 | ports: 29 | - "32001:3306" 30 | environment: 31 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 32 | MYSQL_DATABASE: product 33 | MYSQL_USER: cloudacademy 34 | MYSQL_PASSWORD: pfm_2020 35 | networks: 36 | - micro_network 37 | volumes: 38 | - productdb_vol:/var/lib/mysql -------------------------------------------------------------------------------- /product-service/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | idna==2.9 20 | itsdangerous==1.1.0 21 | Jinja2==2.11.1 22 | Mako==1.1.2 23 | MarkupSafe==1.1.1 24 | marshmallow==3.5.1 25 | passlib==1.7.2 26 | protobuf==3.6.1 27 | pycparser==2.20 28 | PyMySQL==0.9.3 29 | python-dateutil==2.8.1 30 | python-dotenv==0.12.0 31 | python-editor==1.0.4 32 | requests==2.23.0 33 | six==1.14.0 34 | SQLAlchemy==1.3.15 35 | urllib3==1.25.8 36 | visitor==0.1.3 37 | Werkzeug==1.0.1 38 | WTForms==2.2.1 39 | -------------------------------------------------------------------------------- /product-service/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app, db 3 | from application import models 4 | from flask_migrate import Migrate 5 | 6 | app = create_app() 7 | migrate = Migrate(app, db) 8 | 9 | if __name__ == '__main__': 10 | app.run(host='0.0.0.0', port=5002) 11 | -------------------------------------------------------------------------------- /user-service/.env: -------------------------------------------------------------------------------- 1 | # .env 2 | CONFIGURATION_SETUP="config.ProductionConfig" -------------------------------------------------------------------------------- /user-service/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /user-service/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /user-service/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /userapp/requirements.txt 4 | WORKDIR /userapp 5 | RUN pip install -r requirements.txt 6 | COPY . /userapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /user-service/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | 4 | ``` 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t user-srv . 7 | docker run -p 5001:5001 --detach --name user-service --net=micro_network user-srv 8 | ``` 9 | 10 | ## Using 'flask shell' to access flask application 11 | ``` 12 | $ flask shell 13 | from application.models import User 14 | from application import db 15 | admin = User(username="foo", email="foo@admin.com",first_name="foo", last_name="bar", password="admin2020",is_admin=True) 16 | db.session.add(admin) 17 | db.session.commit() 18 | ``` -------------------------------------------------------------------------------- /user-service/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_login import LoginManager 6 | from flask_sqlalchemy import SQLAlchemy 7 | 8 | db = SQLAlchemy() 9 | login_manager = LoginManager() 10 | 11 | 12 | def create_app(): 13 | app = Flask(__name__) 14 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 15 | app.config.from_object(environment_configuration) 16 | 17 | db.init_app(app) 18 | login_manager.init_app(app) 19 | 20 | with app.app_context(): 21 | # Register blueprints 22 | from .user_api import user_api_blueprint 23 | app.register_blueprint(user_api_blueprint) 24 | return app 25 | -------------------------------------------------------------------------------- /user-service/application/models.py: -------------------------------------------------------------------------------- 1 | # application/models.py 2 | from . import db 3 | from datetime import datetime 4 | from flask_login import UserMixin 5 | from passlib.hash import sha256_crypt 6 | 7 | 8 | class User(UserMixin, db.Model): 9 | id = db.Column(db.Integer, primary_key=True) 10 | username = db.Column(db.String(255), unique=True, nullable=False) 11 | email = db.Column(db.String(255), unique=True, nullable=False) 12 | first_name = db.Column(db.String(255), unique=False, nullable=True) 13 | last_name = db.Column(db.String(255), unique=False, nullable=True) 14 | password = db.Column(db.String(255), unique=False, nullable=False) 15 | is_admin = db.Column(db.Boolean, default=False) 16 | authenticated = db.Column(db.Boolean, default=False) 17 | api_key = db.Column(db.String(255), unique=True, nullable=True) 18 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 19 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 20 | 21 | def encode_api_key(self): 22 | self.api_key = sha256_crypt.hash(self.username + str(datetime.utcnow)) 23 | 24 | def encode_password(self): 25 | self.password = sha256_crypt.hash(self.password) 26 | 27 | def __repr__(self): 28 | return '' % (self.username) 29 | 30 | def to_json(self): 31 | return { 32 | 'first_name': self.first_name, 33 | 'last_name': self.last_name, 34 | 'username': self.username, 35 | 'email': self.email, 36 | 'id': self.id, 37 | 'api_key': self.api_key, 38 | 'is_active': True, 39 | 'is_admin': self.is_admin 40 | } -------------------------------------------------------------------------------- /user-service/application/user_api/__init__.py: -------------------------------------------------------------------------------- 1 | # application/user_api/__init__.py 2 | from flask import Blueprint 3 | 4 | user_api_blueprint = Blueprint('user_api', __name__) 5 | 6 | from . import routes 7 | -------------------------------------------------------------------------------- /user-service/application/user_api/routes.py: -------------------------------------------------------------------------------- 1 | # application/user_api/routes.py 2 | from . import user_api_blueprint 3 | from .. import db, login_manager 4 | from ..models import User 5 | from flask import make_response, request, jsonify 6 | from flask_login import current_user, login_user, logout_user, login_required 7 | 8 | from passlib.hash import sha256_crypt 9 | 10 | 11 | @login_manager.user_loader 12 | def load_user(user_id): 13 | return User.query.filter_by(id=user_id).first() 14 | 15 | 16 | @login_manager.request_loader 17 | def load_user_from_request(request): 18 | api_key = request.headers.get('Authorization') 19 | if api_key: 20 | api_key = api_key.replace('Basic ', '', 1) 21 | user = User.query.filter_by(api_key=api_key).first() 22 | if user: 23 | return user 24 | return None 25 | 26 | 27 | @user_api_blueprint.route('/api/users', methods=['GET']) 28 | def get_users(): 29 | data = [] 30 | for row in User.query.all(): 31 | data.append(row.to_json()) 32 | 33 | response = jsonify(data) 34 | return response 35 | 36 | @user_api_blueprint.route('/api/user/create', methods=['POST']) 37 | def post_register(): 38 | first_name = request.form['first_name'] 39 | last_name = request.form['last_name'] 40 | email = request.form['email'] 41 | username = request.form['username'] 42 | 43 | password = sha256_crypt.hash((str(request.form['password']))) 44 | 45 | user = User() 46 | user.email = email 47 | user.first_name = first_name 48 | user.last_name = last_name 49 | user.password = password 50 | user.username = username 51 | user.authenticated = True 52 | 53 | db.session.add(user) 54 | db.session.commit() 55 | 56 | response = jsonify({'message': 'User added', 'result': user.to_json()}) 57 | 58 | return response 59 | 60 | 61 | @user_api_blueprint.route('/api/user/login', methods=['POST']) 62 | def post_login(): 63 | username = request.form['username'] 64 | user = User.query.filter_by(username=username).first() 65 | if user: 66 | if sha256_crypt.verify(str(request.form['password']), user.password): 67 | user.encode_api_key() 68 | db.session.commit() 69 | login_user(user) 70 | 71 | return make_response(jsonify({'message': 'Logged in', 'api_key': user.api_key})) 72 | 73 | return make_response(jsonify({'message': 'Not logged in'}), 401) 74 | 75 | 76 | @user_api_blueprint.route('/api/user/logout', methods=['POST']) 77 | def post_logout(): 78 | if current_user.is_authenticated: 79 | logout_user() 80 | return make_response(jsonify({'message': 'You are logged out'})) 81 | return make_response(jsonify({'message': 'You are not logged in'})) 82 | 83 | 84 | @user_api_blueprint.route('/api/user//exists', methods=['GET']) 85 | def get_username(username): 86 | item = User.query.filter_by(username=username).first() 87 | if item is not None: 88 | response = jsonify({'result': True}) 89 | else: 90 | response = jsonify({'message': 'Cannot find username'}), 404 91 | return response 92 | 93 | 94 | @login_required 95 | @user_api_blueprint.route('/api/user', methods=['GET']) 96 | def get_user(): 97 | if current_user.is_authenticated: 98 | return make_response(jsonify({'result': current_user.to_json()})) 99 | 100 | return make_response(jsonify({'message': 'Not logged in'})), 401 -------------------------------------------------------------------------------- /user-service/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | if os.path.exists(dotenv_path): 7 | load_dotenv(dotenv_path) 8 | 9 | 10 | class Config: 11 | SECRET_KEY = "mrfrIMEngCl0pAKqIIBS_g" 12 | SQLALCHEMY_TRACK_MODIFICATIONS = False 13 | 14 | 15 | class DevelopmentConfig(Config): 16 | ENV = "development" 17 | DEBUG = True 18 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@host.docker.internal:3306/user_dev' 19 | SQLALCHEMY_ECHO = True 20 | 21 | 22 | class ProductionConfig(Config): 23 | ENV = "production" 24 | DEBUG = False 25 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@user-db:3306/user' 26 | SQLALCHEMY_ECHO = False 27 | 28 | -------------------------------------------------------------------------------- /user-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | 4 | volumes: 5 | userdb_vol: 6 | 7 | networks: 8 | micro_network: 9 | external: 10 | name: micro_network 11 | 12 | services: 13 | user-api: 14 | container_name: cuser-service 15 | build: 16 | context: . 17 | ports: 18 | - "5001:5001" 19 | depends_on: 20 | - user-db 21 | networks: 22 | - micro_network 23 | restart: always 24 | 25 | user-db: 26 | container_name: cuser_dbase 27 | image: mysql:8 28 | ports: 29 | - "32000:3306" 30 | environment: 31 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 32 | MYSQL_DATABASE: user 33 | MYSQL_USER: cloudacademy 34 | MYSQL_PASSWORD: pfm_2020 35 | networks: 36 | - micro_network 37 | volumes: 38 | - userdb_vol:/var/lib/mysql -------------------------------------------------------------------------------- /user-service/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | gunicorn==20.0.4 20 | idna==2.9 21 | itsdangerous==1.1.0 22 | Jinja2==2.11.1 23 | Mako==1.1.2 24 | MarkupSafe==1.1.1 25 | marshmallow==3.5.1 26 | passlib==1.7.2 27 | protobuf==3.6.1 28 | pycparser==2.20 29 | PyMySQL==0.9.3 30 | python-dateutil==2.8.1 31 | python-dotenv==0.12.0 32 | python-editor==1.0.4 33 | requests==2.23.0 34 | six==1.14.0 35 | SQLAlchemy==1.3.15 36 | urllib3==1.25.8 37 | visitor==0.1.3 38 | Werkzeug==1.0.1 39 | WTForms==2.2.1 40 | -------------------------------------------------------------------------------- /user-service/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app, db 3 | from application import models 4 | from flask_migrate import Migrate 5 | 6 | app = create_app() 7 | migrate = Migrate(app, db) 8 | 9 | from flask import g 10 | from flask.sessions import SecureCookieSessionInterface 11 | from flask_login import user_loaded_from_header 12 | 13 | 14 | class CustomSessionInterface(SecureCookieSessionInterface): 15 | """Prevent creating session from API requests.""" 16 | 17 | def save_session(self, *args, **kwargs): 18 | if g.get('login_via_header'): 19 | return 20 | return super(CustomSessionInterface, self).save_session(*args, 21 | **kwargs) 22 | 23 | 24 | app.session_interface = CustomSessionInterface() 25 | 26 | 27 | @user_loaded_from_header.connect 28 | def user_loaded_from_header(self, user=None): 29 | g.login_via_header = True 30 | 31 | 32 | if __name__ == '__main__': 33 | app.run(host='0.0.0.0', port=5001) 34 | --------------------------------------------------------------------------------