diff --git a/.gitignore b/.gitignore
index 894a44c..a050e84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,7 +57,7 @@ local_settings.py
db.sqlite3
# Flask stuff:
-instance/
+#instance/
.webassets-cache
# Scrapy stuff:
@@ -102,3 +102,6 @@ venv.bak/
# mypy
.mypy_cache/
+
+# VS code settings
+.vscode/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..c6d5171
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,31 @@
+env:
+ global:
+ - CC_TEST_REPORTER_ID=18007e474cdfcf70da58a682805aff9f88216339b0dce34454dc0bfcc988ea91
+ - APP_SETTINGS="development"
+ - SECRET_KEY="mvangiffj38kncliu3yt7gvLWDNTDISFJWrk'\flQHJsdnlQI2ROH"
+
+language: python
+
+# python version
+python:
+ - "3.6"
+
+# command to install dependencies
+install:
+ - pip install -r requirements.txt
+
+before_script:
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
+ - chmod +x ./cc-test-reporter
+ - ./cc-test-reporter before-build
+
+# command to run tests
+script:
+ - coverage run --source=app.api.v1 -m pytest app/tests/v1 -v -W error::UserWarning && coverage report
+
+after_script:
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
+
+# Post coverage results to coverage.io
+after_success:
+ - coveralls
\ No newline at end of file
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..80a980e
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: gunicorn run:app
\ No newline at end of file
diff --git a/README.md b/README.md
index 41ee15b..6541db5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,86 @@
# store-manager-api
+
+#### Continous Integration badges
+[](https://travis-ci.com/calebrotich10/store-manager-api) [](https://coveralls.io/github/calebrotich10/store-manager-api?branch=develop) [](https://codeclimate.com/github/calebrotich10/store-manager-api/maintainability)
+
+
Store Manager is a web application that helps store owners manage sales and product inventory records. This application is meant for use in a single store. This repository contains the API endpoints for the application.
+
+#### Endpoints
+
+
+ Http Method |
+ Endpoint |
+ Functionality |
+
+
+ POST |
+ api/v1/auth/signup |
+ Creates a new user account |
+
+
+ POST |
+ api/v1/products |
+ Used by the admin to add a new product |
+
+
+ POST |
+ api/v1/saleorder |
+ Used by the sale attendant to add a new sale order |
+
+
+ GET |
+ api/v1/auth/signin |
+ Authenticates and creates a token for the users |
+
+
+ GET |
+ api/v1/products/<product_id> |
+ Enables a user to fetch a specific product |
+
+
+ GET |
+ api/v1/saleorder/<sale_order_id> |
+ Enables a user to fetch a specific sale order |
+
+
+ GET |
+ api/v1/products |
+ Enables a user to fetch all products |
+
+
+ GET |
+ api/v1/saleorder |
+ Enables a user to fetch all sale orders |
+
+
+
+#### Installing the application
+1. Open a command terminal in your preferred folder
+2. Run command `git clone https://github.com/calebrotich10/store-manager-api.git` to have a copy locally
+3. `cd store-manager-api`
+4. Create a virtual environment for the application `virtualenv venv`
+5. Install dependencies from the `requirements.txt` file `pip3 install -r requirements.txt`
+6. Export environment variables to your environment ```export JWT_SECRET_KEY=your-secret-key```, ```export FLASK_APP="run.py"```
+6. Run the application using flask command `flask run` or using python3 `python3 run.py`
+
+#### Running tests
+Inside the virtual environment created above, run command: `coverage run --source=app.api.v1 -m pytest app/tests/v1 -v -W error::UserWarning && coverage report`
+
+#### Technologies used
+1. `JWT` for authentication
+2. `pytest` for running tests
+3. Python based framework `flask`
+4. Flask packages
+
+#### Deployment
+[Heroku](https://store-manager-api.herokuapp.com/api/v1/products)
+
+#### Documentation
+https://documenter.getpostman.com/view/5265531/RWguxwzy
+
+#### Author
+[Caleb Rotich](https://github.com/calebrotich10)
+
+#### Credits
+This application was build as part of the Andela NBO 33 challenge
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..acbc658
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,23 @@
+import os
+
+from flask import Flask
+from flask_jwt_extended import JWTManager
+
+from instance.config import config
+
+jwt = JWTManager()
+
+def create_app(config_name):
+ app = Flask(__name__)
+ app.config.from_object(config[config_name])
+ app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh')
+
+ jwt.init_app(app)
+
+ from .api.v1 import endpoint_v1_blueprint as v1_blueprint
+ app.register_blueprint(v1_blueprint, url_prefix='/api/v1')
+
+ from .api.v1 import auth_v1_blueprint as v1_blueprint
+ app.register_blueprint(v1_blueprint, url_prefix='/api/v1/auth')
+
+ return app
diff --git a/app/api/__init__.py b/app/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py
new file mode 100644
index 0000000..e6978c0
--- /dev/null
+++ b/app/api/v1/__init__.py
@@ -0,0 +1,9 @@
+from flask import Blueprint
+
+
+endpoint_v1_blueprint = Blueprint('endpoint_v1_blueprint', __name__)
+auth_v1_blueprint = Blueprint('auth_v1_blueprint', __name__)
+
+
+
+from . import views
\ No newline at end of file
diff --git a/app/api/v1/models/products.py b/app/api/v1/models/products.py
new file mode 100644
index 0000000..9eb36d2
--- /dev/null
+++ b/app/api/v1/models/products.py
@@ -0,0 +1,24 @@
+"""This module contains the data store
+
+and data logic of the store's products
+"""
+
+PRODUCTS = []
+
+class Products():
+ def __init__(self, product_name, product_price, category):
+ self.id = len(PRODUCTS) + 1
+ self.product_name = product_name
+ self.product_price = product_price
+ self.category = category
+
+ def save(self):
+ new_product = {
+ "product_id": self.id,
+ "product_name": self.product_name,
+ "product_price": self.product_price,
+ "category": self.category
+ }
+
+ PRODUCTS.append(new_product)
+ return new_product
\ No newline at end of file
diff --git a/app/api/v1/models/sale_orders.py b/app/api/v1/models/sale_orders.py
new file mode 100644
index 0000000..ca259b0
--- /dev/null
+++ b/app/api/v1/models/sale_orders.py
@@ -0,0 +1,26 @@
+"""This module contains the data store
+
+and data logic of the store attendant's sale orders
+"""
+from flask import jsonify
+
+SALE_ORDERS = []
+
+class SaleOrder():
+ def __init__(self, product_name, product_price, quantity):
+ self.id = len(SALE_ORDERS) + 1
+ self.product_name = product_name
+ self.product_price = product_price
+ self.quantity = quantity
+
+ def save(self):
+ new_sale_order = {
+ "sale_order_id": self.id,
+ "product_name": self.product_name,
+ "product_price": self.product_price,
+ "quantity": self.quantity,
+ "amount": (self.product_price * self.quantity)
+ }
+
+ SALE_ORDERS.append(new_sale_order)
+ return new_sale_order
\ No newline at end of file
diff --git a/app/api/v1/models/users.py b/app/api/v1/models/users.py
new file mode 100644
index 0000000..138d06d
--- /dev/null
+++ b/app/api/v1/models/users.py
@@ -0,0 +1,27 @@
+from flask import make_response, jsonify
+USERS = []
+
+
+class User_Model():
+ def __init__(self, email, password, role):
+ self.id = len(USERS) + 1
+ self.email = email
+ self.password = password
+ self.role = role
+
+ def save(self):
+ new_user = {
+ "id": self.id,
+ "email": self.email,
+ "password": self.password,
+ "role": self.role
+ }
+
+ new_added_user = {
+ "id": self.id,
+ "email": self.email,
+ "role": self.role
+ }
+
+ USERS.append(new_user)
+ return new_added_user
\ No newline at end of file
diff --git a/app/api/v1/resources/__init__.py b/app/api/v1/resources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/api/v1/resources/admin_endpoints.py b/app/api/v1/resources/admin_endpoints.py
new file mode 100644
index 0000000..94441db
--- /dev/null
+++ b/app/api/v1/resources/admin_endpoints.py
@@ -0,0 +1,70 @@
+"""This module contains endpoints
+
+that are specific to the admin
+"""
+import os
+import jwt
+from functools import wraps
+
+from flask import Flask, jsonify, request, abort, make_response
+from flask_restful import Resource
+from flask_jwt_extended import get_jwt_identity, jwt_required
+
+from . import helper_functions
+from app.api.v1.models import products, sale_orders, users
+from . import verify
+
+
+class ProductsManagement(Resource):
+ """Class contains admin specific endpoints"""
+
+
+ def post(self):
+ """POST /products endpoint"""
+
+ # Token verification and admin user determination
+ logged_user = verify.verify_tokens()
+ helper_functions.abort_if_user_is_not_admin(logged_user)
+
+ data = request.get_json()
+ helper_functions.no_json_in_request(data)
+ try:
+ product_name = data['product_name']
+ product_price = data['product_price']
+ category = data['category']
+ except KeyError:
+ # If product is missing required parameter
+ helper_functions.missing_a_required_parameter()
+
+ verify.verify_post_product_fields(product_price, product_name, category)
+
+ response = helper_functions.add_product_to_store(product_name, product_price, category)
+
+ return make_response(jsonify({
+ "message": "Product added successfully",
+ "product": response
+ }), 201)
+
+
+class SaleAttendantsManagement(Resource):
+ """Class contains the tests managing
+
+ sale orders
+ """
+
+ def get(self):
+ """GET /saleorder endpoint"""
+
+ verify.verify_tokens()
+
+ if not sale_orders.SALE_ORDERS:
+ # If no products exist in the store yet
+ abort(make_response(
+ jsonify(message="There are no sale orders made yet"), 404))
+ # if at least one product exists
+ response = jsonify({
+ 'message': "Successfully fetched all the sale orders",
+ 'sale_orders': sale_orders.SALE_ORDERS
+ })
+ response.status_code = 200
+ return response
diff --git a/app/api/v1/resources/auth.py b/app/api/v1/resources/auth.py
new file mode 100644
index 0000000..6d9bdce
--- /dev/null
+++ b/app/api/v1/resources/auth.py
@@ -0,0 +1,60 @@
+import os
+import datetime
+import jwt
+from werkzeug.security import generate_password_hash, check_password_hash
+from functools import wraps
+
+from flask import Flask, jsonify, request, make_response
+from flask_restful import Resource
+from flask_jwt_extended import (
+ jwt_required, create_access_token,
+ get_jwt_identity
+)
+
+from instance import config
+from app.api.v1.utils.validator import Validator
+from app.api.v1.models import users
+
+class SignUp(Resource):
+ def post(self):
+ data = request.get_json()
+ if not data:
+ return make_response(jsonify({
+ "message": "Missing required credentials"
+ }), 400)
+ email = data["email"].strip()
+ password = generate_password_hash(data["password"].strip(), method='sha256')
+ role = data["role"].strip()
+ Validator.validate_credentials(self, data)
+ user = users.User_Model(email, password, role)
+ res = user.save()
+ return make_response(jsonify({
+ "message": "Account created successfully",
+ "user": res
+ }), 202)
+
+
+class Login(Resource):
+ def post(self):
+ data = request.get_json()
+ if not data:
+ return make_response(jsonify({
+ "message": "Kindly enter your credentials"
+ }
+ ), 400)
+ email = data["email"].strip()
+ password = data["password"].strip()
+
+ for user in users.USERS:
+ if email == user["email"] and check_password_hash(user["password"], password):
+ token = jwt.encode({
+ "email": email,
+ "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
+ }, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh'))
+ return make_response(jsonify({
+ "message": "Login successful",
+ "token": token.decode("UTF-8")}), 200)
+ return make_response(jsonify({
+ "message": "Wrong credentials provided"
+ }
+ ), 403)
diff --git a/app/api/v1/resources/general_users_endpoints.py b/app/api/v1/resources/general_users_endpoints.py
new file mode 100644
index 0000000..fe212ee
--- /dev/null
+++ b/app/api/v1/resources/general_users_endpoints.py
@@ -0,0 +1,73 @@
+"""This module contains endpoints
+
+for both the admin and the normal user
+"""
+from flask import jsonify, abort, make_response
+from flask_restful import Resource
+from flask_jwt_extended import get_jwt_identity, jwt_required
+from . import verify
+
+from . import helper_functions
+from app.api.v1.models import products, sale_orders
+
+class AllProducts(Resource):
+ """Class contains the tests for both
+
+ the admin and the normal user endpoints
+ """
+
+ def get(self):
+ """GET /products endpoint"""
+ verify.verify_tokens()
+ if not products.PRODUCTS:
+ # If no products exist in the store yet
+ abort(make_response(
+ jsonify(message="There are no products in the store yet"), 404))
+ # if at least one product exists
+ response = jsonify({
+ 'message': "Successfully fetched all the products",
+ 'products': products.PRODUCTS
+ })
+ response.status_code = 200
+ return response
+
+class SpecificProduct(Resource):
+
+ def get(self, product_id):
+
+ verify.verify_tokens()
+
+ for product in products.PRODUCTS:
+ if product["product_id"] == product_id:
+ return make_response(jsonify({
+ "message": "{} retrieved successfully".format(product["product_name"]),
+ "product": product
+ }
+ ), 200)
+
+ else:
+ return make_response(jsonify({
+ "message": "Product with id {} not found".format(product_id)
+ }
+ ), 404)
+
+
+class SpecificSaleOrder(Resource):
+
+ def get(self, sale_order_id):
+ """GET /saleorder/"""
+
+ verify.verify_tokens()
+
+ for sale_order in sale_orders.SALE_ORDERS:
+ if sale_order["sale_order_id"] == sale_order_id:
+ return make_response(jsonify({
+ "message": "Sale Order with Id {} retrieved successfully".format(sale_order["sale_order_id"]),
+ "product": sale_order
+ }
+ ), 200)
+
+ return make_response(jsonify({
+ "message": "Sale Order with id {} not found".format(sale_order_id)
+ }
+ ), 404)
\ No newline at end of file
diff --git a/app/api/v1/resources/helper_functions.py b/app/api/v1/resources/helper_functions.py
new file mode 100644
index 0000000..a0aeac7
--- /dev/null
+++ b/app/api/v1/resources/helper_functions.py
@@ -0,0 +1,57 @@
+from datetime import datetime
+
+from flask import abort, jsonify, make_response
+
+from app.api.v1.models import products, sale_orders, users
+
+def no_json_in_request(data):
+ """Aborts if the data does
+
+ not contain a json object
+ """
+
+ if data is None:
+ # If a json was not obtained from the request
+ abort(make_response(jsonify(
+ message="Bad request. Request data must be in json format"), 400))
+
+
+def missing_a_required_parameter():
+ """Aborts if request data is missing a
+
+ required argument
+ """
+ abort(make_response(jsonify(
+ message="Bad request. Request missing a required argument"), 400))
+
+
+def abort_if_user_is_not_admin(user):
+ user_role = [users['role'] for users in users.USERS if users['email'] == user][0]
+ if user_role!= "admin":
+ abort(make_response(jsonify(
+ message="Unauthorized. This action is not for you"
+ ), 401))
+
+
+def add_product_to_store(product_name, product_price, category):
+ if products.PRODUCTS:
+ # If products are in the store already
+ try:
+ # Check if a product with a similar name exists
+ existing_product = [
+ product for product in products.PRODUCTS if product['product_name'] == product_name][0]
+
+ abort(make_response(jsonify({
+ "message": "Product with a similar name already exists",
+ "product": existing_product}), 400))
+
+ except IndexError:
+ # If there is no product with the same name
+ added_product = products.Products(product_name, product_price, category)
+ response = added_product.save()
+ else:
+ # If there are no products in the store
+ added_product = products.Products(product_name, product_price, category)
+ response = added_product.save()
+
+ return response
\ No newline at end of file
diff --git a/app/api/v1/resources/store_attendant_endpoints.py b/app/api/v1/resources/store_attendant_endpoints.py
new file mode 100644
index 0000000..ae2be35
--- /dev/null
+++ b/app/api/v1/resources/store_attendant_endpoints.py
@@ -0,0 +1,63 @@
+"""This module contains endpoints
+
+that are specific to the store attendant
+"""
+from flask import Flask, jsonify, request, abort, make_response
+from flask_restful import Resource
+from flask_jwt_extended import get_jwt_identity, jwt_required
+
+from . import helper_functions
+from app.api.v1.models import products, sale_orders
+from . import verify
+
+class SaleRecords(Resource):
+ """Class contains the tests for store attendant
+
+ specific endpoints
+ """
+
+ def post(self):
+ """POST /saleorder endpoint"""
+
+ verify.verify_tokens()
+
+ data = request.get_json()
+ helper_functions.no_json_in_request(data)
+ try:
+ product_name = data['product_name']
+ product_price = data['product_price']
+ quantity = data['quantity']
+ except KeyError:
+ # If product is missing required parameter
+ helper_functions.missing_a_required_parameter()
+
+ if not isinstance(product_price, int):
+ abort(make_response(jsonify(
+ message="Bad request. The product price should be digits"
+ ), 400))
+
+ if product_price < 1:
+ abort(make_response(jsonify(
+ message="Bad request. Price of the product should be a positive integer above 0."
+ ), 400))
+
+ if not isinstance(product_name, str):
+ abort(make_response(jsonify(
+ message="Bad request. Product name should be a string"
+ ), 400))
+
+
+ if not isinstance(quantity, int):
+ abort(make_response(jsonify(
+ message="Bad request. The quantity should be specified in digits"
+ ), 400))
+
+ # response = helper_functions.add_new_sale_record(product_name, product_price, quantity, amount)
+ sale_order = sale_orders.SaleOrder(product_name, product_price, quantity)
+ response = sale_order.save()
+
+
+ return make_response(jsonify({
+ "message": "Checkout complete",
+ "saleorder": response
+ }), 201)
diff --git a/app/api/v1/resources/verify.py b/app/api/v1/resources/verify.py
new file mode 100644
index 0000000..691e418
--- /dev/null
+++ b/app/api/v1/resources/verify.py
@@ -0,0 +1,43 @@
+import os
+import jwt
+from functools import wraps
+
+from flask import request, make_response, jsonify, abort
+from ..models import users
+
+
+def verify_tokens():
+ token = None
+ if 'Authorization' in request.headers:
+ token = request.headers['Authorization']
+ if not token:
+ abort(make_response(jsonify({
+ "Message": "You need to login"}), 401))
+ try:
+ data = jwt.decode(token, os.getenv('JWT_SECRET_KEY', default='SdaHv342nx!jknr837bjwd?c,lsajjjhw673hdsbgeh'))
+ for user in users.USERS:
+ if user['email'] == data['email']:
+ return user["email"]
+
+ except:
+ abort(make_response(jsonify({
+ "Message": "The token is either expired or wrong"
+ }), 403))
+
+
+
+def verify_post_product_fields(product_price, product_name, category):
+ if product_price < 1:
+ abort(make_response(jsonify(
+ message="Bad request. Price of the product should be a positive integer above 0."
+ ), 400))
+
+ if not isinstance(product_name, str):
+ abort(make_response(jsonify(
+ message="Bad request. Product name should be a string"
+ ), 400))
+
+ if not isinstance(category, str):
+ abort(make_response(jsonify(
+ message="Bad request. The Category should be a string"
+ ), 400))
\ No newline at end of file
diff --git a/app/api/v1/utils/validator.py b/app/api/v1/utils/validator.py
new file mode 100644
index 0000000..af8acaf
--- /dev/null
+++ b/app/api/v1/utils/validator.py
@@ -0,0 +1,39 @@
+import re
+
+from flask import make_response, jsonify, abort
+from validate_email import validate_email
+
+from app.api.v1.models import users
+
+
+class Validator:
+ def validate_credentials(self, data):
+ self.email = data["email"].strip()
+ self.password = data["password"].strip()
+ self.role = data["role"].strip()
+ valid_email = validate_email(self.email)
+ for user in users.USERS:
+ if self.email == user["email"]:
+ Message = "User already exists"
+ abort(406, Message)
+ if self.email == "" or self.password == "" or self.role == "":
+ Message = "You are missing a required credential"
+ abort(400, Message)
+ if not valid_email:
+ Message = "Invalid email"
+ abort(400, Message)
+ elif len(self.password) < 6 or len(self.password) > 12:
+ Message = "Password must be long than 6 characters or less than 12"
+ abort(400, Message)
+ elif not any(char.isdigit() for char in self.password):
+ Message = "Password must have a digit"
+ abort(400, Message)
+ elif not any(char.isupper() for char in self.password):
+ Message = "Password must have an upper case character"
+ abort(400, Message)
+ elif not any(char.islower() for char in self.password):
+ Message = "Password must have a lower case character"
+ abort(400, Message)
+ elif not re.search("^.*(?=.*[@#$%^&+=]).*$", self.password):
+ Message = "Password must have a special charater"
+ abort(400, Message)
diff --git a/app/api/v1/views.py b/app/api/v1/views.py
new file mode 100644
index 0000000..4c71a85
--- /dev/null
+++ b/app/api/v1/views.py
@@ -0,0 +1,20 @@
+"""Define API endpoints as routes"""
+
+from flask_restful import Api, Resource
+
+from . import endpoint_v1_blueprint, auth_v1_blueprint
+from .resources import admin_endpoints, general_users_endpoints, store_attendant_endpoints, auth
+
+API = Api(endpoint_v1_blueprint)
+AUTH_API = Api(auth_v1_blueprint)
+
+
+API.add_resource(admin_endpoints.ProductsManagement, '/products')
+API.add_resource(general_users_endpoints.AllProducts, '/products')
+API.add_resource(general_users_endpoints.SpecificProduct, '/products/')
+API.add_resource(store_attendant_endpoints.SaleRecords, '/saleorder')
+API.add_resource(admin_endpoints.SaleAttendantsManagement, '/saleorder')
+API.add_resource(general_users_endpoints.SpecificSaleOrder, '/saleorder/')
+
+AUTH_API.add_resource(auth.SignUp, '/signup')
+AUTH_API.add_resource(auth.Login, '/login')
\ No newline at end of file
diff --git a/app/tests/__init__.py b/app/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/v1/__init__.py b/app/tests/v1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/v1/base_test.py b/app/tests/v1/base_test.py
new file mode 100644
index 0000000..6a528d8
--- /dev/null
+++ b/app/tests/v1/base_test.py
@@ -0,0 +1,85 @@
+"""
+ Contains the base test class for the
+ other test classes
+"""
+import unittest
+
+# local imports
+from app import create_app
+from instance.config import config
+from . import helper_functions
+
+
+class TestBaseClass(unittest.TestCase):
+ """Base test class"""
+
+
+ def setUp(self):
+ """Create and setup the application
+
+ for testing purposes
+ """
+ self.app = create_app('testing')
+ self.BASE_URL = 'api/v1'
+ self.app_context = self.app.app_context()
+ self.app_context.push()
+ self.app_test_client = self.app.test_client()
+ self.app.testing = True
+
+ self.PRODUCT = {
+ 'product_name': 'Phone Model 1',
+ 'product_price': 55000,
+ 'category': 'Phones'
+ }
+
+ self.SALE_ORDERS = {
+ 'product_name': 'Phone Model 1',
+ 'product_price': 55000,
+ 'quantity': 6,
+ 'amount': (55000 * 6)
+ }
+
+
+ def tearDown(self):
+ """Destroy the application that
+
+ is created for testing
+ """
+ self.app_context.pop()
+
+ def register_test_admin_account(self):
+ #Register attendant
+ """Registers an admin test user account"""
+
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "user@gmail.com",
+ "role": "admin",
+ "password": "Password12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ return res
+
+ def login_test_admin(self):
+ """Validates the test account for the admin"""
+
+ # Login the test account for the admin
+ resp = self.app_test_client.post("api/v1/auth/login",
+ json={
+ "email": "user@gmail.com",
+ "password": "Password12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ auth_token = helper_functions.convert_response_to_json(
+ resp)['token']
+
+ return auth_token
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/app/tests/v1/helper_functions.py b/app/tests/v1/helper_functions.py
new file mode 100644
index 0000000..36e4717
--- /dev/null
+++ b/app/tests/v1/helper_functions.py
@@ -0,0 +1,7 @@
+import json
+
+def convert_response_to_json(response):
+ """Converts the response to a json type"""
+
+ json_response = json.loads(response.data.decode('utf-8'))
+ return json_response
\ No newline at end of file
diff --git a/app/tests/v1/test_admin_endpoints.py b/app/tests/v1/test_admin_endpoints.py
new file mode 100644
index 0000000..f0c8ad1
--- /dev/null
+++ b/app/tests/v1/test_admin_endpoints.py
@@ -0,0 +1,165 @@
+"""Module contains tests for admin
+
+specific endpoints
+"""
+
+import json
+
+from flask import current_app
+
+from . import base_test
+from . import helper_functions
+
+class TestAdminEndpoints(base_test.TestBaseClass):
+ """ Class contains tests for admin specific endpoints """
+
+ def test_add_new_product(self):
+ """Test POST /products"""
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ # send a dummy data response for testing
+ response = self.app_test_client.post('{}/products'.format(
+ self.BASE_URL), json=self.PRODUCT, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 201)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['product']['product_name'], self.PRODUCT['product_name'])
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['product']['product_id'], 1)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['product']['product_price'], 55000)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['product']['category'], self.PRODUCT['category'])
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Product added successfully')
+
+ def test_add_new_product_parameter_missing(self):
+ """Test POST /products
+
+ with one of the required parameters missing
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/products'.format(
+ self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Bad request. Request missing a required argument')
+
+ def test_add_new_product_price_under_one(self):
+ """Test POST /products
+
+ with the price of the product below minimum
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/products'.format(
+ self.BASE_URL), json={
+ 'product_id': 1, 'product_name': "Hammer", 'product_price': 0, 'category':'Tools'
+ }, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'],
+ 'Bad request. Price of the product should be a positive integer above 0.')
+
+
+ def test_add_new_product_with_product_name_not_string(self):
+ """Test POST /products
+
+ with the product name not a string
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/products'.format(
+ self.BASE_URL), json={
+ 'product_id': 1, 'product_name': 200, 'product_price': 200, 'category':'Tools'
+ }, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'],
+ 'Bad request. Product name should be a string')
+
+ def test_add_new_product_with_category_not_string(self):
+ """Test POST /products
+
+ with the category not a string
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/products'.format(
+ self.BASE_URL), json={
+ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': 200
+ }, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'],
+ 'Bad request. The Category should be a string')
+
+ def test_add_new_product_with_product_name_already_existing(self):
+ """Test POST /products
+
+ with the product name already existing
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ self.app_test_client.post('{}/products'.format(
+ self.BASE_URL), json={
+ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools"
+ }, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ response = self.app_test_client.post('{}/products'.format(
+ self.BASE_URL), json={
+ 'product_id': 1, 'product_name': "Hammer", 'product_price': 200, 'category': "Tools"
+ }, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'],
+ 'Product with a similar name already exists')
+
+
+ def test_fetch_sale_orders(self):
+ """Test GET /saleorder - when sale order exists"""
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ self.app_test_client.post(
+ '{}/saleorder'.format(self.BASE_URL), json={
+ 'sale_order_id': 1,
+ 'product_name': "Test Product",
+ 'product_price': 20,
+ 'quantity': 1,
+ 'amount': 20
+ },
+ headers=dict(Authorization=token),
+ content_type='application/json')
+
+ response = self.app_test_client.get(
+ '{}/saleorder'.format(self.BASE_URL),
+
+ headers=dict(Authorization=token),
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['sale_orders'][0]['product_name'], "Test Product")
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['sale_orders'][0]['product_price'], 20)
diff --git a/app/tests/v1/test_app_working.py b/app/tests/v1/test_app_working.py
new file mode 100644
index 0000000..4574c93
--- /dev/null
+++ b/app/tests/v1/test_app_working.py
@@ -0,0 +1,18 @@
+"""
+ Module contains tests for the working
+ of the application
+"""
+from flask import current_app
+
+#local imports
+from . import base_test
+
+class TestConfigCase(base_test.TestBaseClass):
+
+
+ def test_app_exists(self):
+ self.assertFalse(current_app is None)
+
+
+ def test_app_is_testing(self):
+ self.assertTrue(current_app.config['TESTING'])
\ No newline at end of file
diff --git a/app/tests/v1/test_auth_endpoints.py b/app/tests/v1/test_auth_endpoints.py
new file mode 100644
index 0000000..9f2c3af
--- /dev/null
+++ b/app/tests/v1/test_auth_endpoints.py
@@ -0,0 +1,219 @@
+"""Module contains tests to endpoints that
+
+are used for user registration and authentication
+"""
+
+import json
+
+from . import base_test
+from . import helper_functions
+
+class TestAuthEndpoints(base_test.TestBaseClass):
+ """ Class contains tests for auth endpoints """
+
+ def test_add_new_user(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "test_add_new_user@gmail.com",
+ "role": "Admin",
+ "password": "Password12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['user']['email'], "test_add_new_user@gmail.com")
+ self.assertEqual(res.status_code, 202)
+
+ def test_add_new_user_no_data(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(res)
+
+ self.assertEqual(data['message'], "Missing required credentials")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_missing_params(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "",
+ "role": "Admin",
+ "password": "Password12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['message'], "You are missing a required credential")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_invalid_email(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "test_add_new_user",
+ "role": "Admin",
+ "password": "Password12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['message'], "Invalid email")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_no_digit_password(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "test_add_new_user_invalid_email@gmail.com",
+ "role": "Admin",
+ "password": "No#digit"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['message'], "Password must have a digit")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_short_password(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "test_add_new_user_invalid_email@gmail.com",
+ "role": "Admin",
+ "password": "Shor#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['message'], "Password must be long than 6 characters or less than 12")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_no_special_ch_password(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "test_add_new_user_invalid_email@gmail.com",
+ "role": "Admin",
+ "password": "NoSplCh12"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['message'], "Password must have a special charater")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_no_upper_case_password(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "test_add_new_user_invalid_email@gmail.com",
+ "role": "Admin",
+ "password": "noupper12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['message'], "Password must have an upper case character")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_no_lower_case_password(self):
+ res = self.app_test_client.post("api/v1/auth/signup",
+ json={
+ "email": "test_add_new_user_invalid_email@gmail.com",
+ "role": "Admin",
+ "password": "NOLOWER12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ data = json.loads(res.data.decode())
+ print(data)
+
+ self.assertEqual(data['message'], "Password must have a lower case character")
+ self.assertEqual(res.status_code, 400)
+
+ def test_add_new_user_existing(self):
+ """Test POST /auth/signup"""
+ self.register_test_admin_account()
+ response = self.register_test_admin_account()
+ data = json.loads(response.data.decode())
+
+ self.assertEqual(data['message'], "User already exists")
+ self.assertEqual(response.status_code, 406)
+
+ def test_login_existing_user(self):
+ self.register_test_admin_account()
+ resp = self.app_test_client.post("api/v1/auth/login",
+ json={
+ "email": "user@gmail.com",
+ "password": "Password12#"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ self.assertTrue(helper_functions.convert_response_to_json(
+ resp)['token'])
+ self.assertTrue(helper_functions.convert_response_to_json(
+ resp)['message'], "You are successfully logged in!")
+ self.assertEqual(resp.status_code, 200)
+
+ def test_login_no_credentials(self):
+ self.register_test_admin_account()
+ resp = self.app_test_client.post("api/v1/auth/login",
+ json={
+
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ self.assertTrue(helper_functions.convert_response_to_json(
+ resp)['message'], "Kindly enter your credentials")
+ self.assertEqual(resp.status_code, 400)
+
+ def test_login_non_matching_credentials(self):
+ self.register_test_admin_account()
+ resp = self.app_test_client.post("api/v1/auth/login",
+ json={
+ "email": "non_matching_credentials_user_1018@gmail.com",
+ "password": "neverexpecteduser"
+ },
+ headers={
+ "Content-Type": "application/json"
+ })
+
+ self.assertTrue(helper_functions.convert_response_to_json(
+ resp)['message'], "Wrong credentials provided")
+ self.assertEqual(resp.status_code, 403)
+
\ No newline at end of file
diff --git a/app/tests/v1/test_general_user_endpoints.py b/app/tests/v1/test_general_user_endpoints.py
new file mode 100644
index 0000000..d2b2fd6
--- /dev/null
+++ b/app/tests/v1/test_general_user_endpoints.py
@@ -0,0 +1,107 @@
+"""Module contains tests to endpoints that
+
+are general to both the admin and the normal user
+"""
+
+import json
+
+from . import base_test
+from . import helper_functions
+
+class TestGeneralUsersEndpoints(base_test.TestBaseClass):
+ """Class contains the general user, i.e. both admin
+
+ and normal user, endpoints' tests
+ """
+
+
+ def test_retrieve_all_products(self):
+ """Test GET /products - when products exist"""
+
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.get(
+ '{}/products'.format(self.BASE_URL),
+ headers=dict(Authorization=token),
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['products'][0]['product_name'], self.PRODUCT['product_name'])
+
+ def test_retrieve_specific_product(self):
+ """Test GET /products/id - when product exist"""
+
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.get(
+ '{}/products/1'.format(self.BASE_URL),
+ headers=dict(Authorization=token),
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['product']['product_name'], self.PRODUCT['product_name'])
+
+ def test_retrieve_specific_sale_order(self):
+ """Test GET /saleorder/id - when saleorder exists"""
+
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ self.app_test_client.post(
+ '{}/saleorder'.format(self.BASE_URL), json={
+ 'sale_order_id': 1,
+ 'product_name': "Test Product",
+ 'product_price': 20,
+ 'quantity': 1,
+ 'amount': 20
+ },
+ headers=dict(Authorization=token),
+ content_type='application/json')
+
+ response = self.app_test_client.get(
+ '{}/saleorder/1'.format(self.BASE_URL),
+ headers=dict(Authorization=token),
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 200)
+
+
+ def test_missing_token(self):
+ """Test GET /products - when token is missing"""
+
+ self.register_test_admin_account()
+ token = ""
+
+ response = self.app_test_client.get(
+ '{}/products'.format(self.BASE_URL),
+ headers=dict(Authorization=token),
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 401)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)["Message"], "You need to login")
+
+
+ def test_invalid_token(self):
+ """Test GET /products - when token is missing"""
+
+ self.register_test_admin_account()
+ token = "sample_invalid-token-afskdghkfhwkedaf-ksfakjfwey"
+
+ response = self.app_test_client.get(
+ '{}/products'.format(self.BASE_URL),
+ headers=dict(Authorization=token),
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 403)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)["Message"], "The token is either expired or wrong")
\ No newline at end of file
diff --git a/app/tests/v1/test_store_attendant_endpoints.py b/app/tests/v1/test_store_attendant_endpoints.py
new file mode 100644
index 0000000..c78d9b1
--- /dev/null
+++ b/app/tests/v1/test_store_attendant_endpoints.py
@@ -0,0 +1,124 @@
+"""Module contains tests for store attendant
+
+specific endpoints
+"""
+from flask import current_app
+
+from . import base_test
+from . import helper_functions
+
+class TestAdminEndpoints(base_test.TestBaseClass):
+ """ Class contains tests for store attendant specific endpoints """
+
+
+ def test_create_sale_order(self):
+ """Test POST /saleorder"""
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ # send a dummy data response for testing
+ response = self.app_test_client.post('{}/saleorder'.format(
+ self.BASE_URL), json=self.SALE_ORDERS, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 201)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['saleorder']['product_name'], self.SALE_ORDERS['product_name'])
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['saleorder']['product_price'], self.SALE_ORDERS['product_price'])
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['saleorder']['quantity'], self.SALE_ORDERS['quantity'])
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['saleorder']['amount'], self.SALE_ORDERS['amount'])
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Checkout complete')
+
+
+ def test_create_sale_order_parameter_missing(self):
+ """Test POST /saleorder
+
+ with one of the required parameters missing
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/saleorder'.format(
+ self.BASE_URL), json={'product_name': 'Nyundo'}, headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Bad request. Request missing a required argument')
+
+
+ def test_create_sale_order_price_below_one(self):
+ """Test POST /saleorder
+
+ with the price not a valid integer
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/saleorder'.format(
+ self.BASE_URL), json={'product_name': 'Nyundo', 'product_price': -1, 'quantity': 1},
+ headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Bad request. Price of the product should be a positive integer above 0.')
+
+
+ def test_create_sale_order_invalid_product_name(self):
+ """Test POST /saleorder
+
+ with the product name not a string
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/saleorder'.format(
+ self.BASE_URL), json={'product_name': 3, 'product_price': 300, 'quantity': 1},
+ headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Bad request. Product name should be a string')
+
+
+ def test_create_sale_order_price_not_digits(self):
+ """Test POST /saleorder
+
+ with the price not a valid integer
+ """
+
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/saleorder'.format(
+ self.BASE_URL), json={'product_name': "Nyundo", 'product_price': "300", 'quantity': 1},
+ headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Bad request. The product price should be digits')
+
+
+ def test_create_sale_order_invalid_quantity(self):
+ """Test POST /saleorder
+
+ with the quantity not a valid integer
+ """
+ self.register_test_admin_account()
+ token = self.login_test_admin()
+
+ response = self.app_test_client.post('{}/saleorder'.format(
+ self.BASE_URL), json={'product_name': "Nyundo", 'product_price': 300, 'quantity': "1"},
+ headers=dict(Authorization=token),
+ content_type='application/json')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(helper_functions.convert_response_to_json(
+ response)['message'], 'Bad request. The quantity should be specified in digits')
\ No newline at end of file
diff --git a/instance/config.py b/instance/config.py
new file mode 100644
index 0000000..7ef9c3e
--- /dev/null
+++ b/instance/config.py
@@ -0,0 +1,35 @@
+import os
+
+
+class Config(object):
+ """Parent configuration class."""
+ DEBUG = True
+
+
+class Development(Config):
+ """Configurations for Development."""
+ DEBUG = True
+
+
+class Testing(Config):
+ """Configurations for Testing, with a separate test database."""
+ TESTING = True
+ DEBUG = True
+
+class Staging(Config):
+ """Configurations for Staging."""
+ DEBUG = True
+
+
+class Production(Config):
+ """Configurations for Production."""
+ DEBUG = False
+ TESTING = False
+
+
+config = {
+ 'development': Development,
+ 'testing': Testing,
+ 'staging': Staging,
+ 'production': Production
+}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..22ab13e
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,43 @@
+aniso8601==3.0.2
+asn1crypto==0.24.0
+astroid==2.0.4
+atomicwrites==1.2.1
+attrs==18.2.0
+certifi==2018.8.24
+cffi==1.11.5
+chardet==3.0.4
+Click==7.0
+coverage==4.0.3
+coveralls==1.5.1
+cryptography==2.3.1
+docopt==0.6.2
+Flask==1.0.2
+Flask-JWT-Extended==3.13.1
+Flask-RESTful==0.3.6
+gunicorn==19.9.0
+idna==2.7
+isort==4.3.4
+itsdangerous==0.24
+Jinja2==2.10
+lazy-object-proxy==1.3.1
+MarkupSafe==1.0
+mccabe==0.6.1
+more-itertools==4.3.0
+pluggy==0.7.1
+psycopg2-binary==2.7.5
+py==1.7.0
+pycparser==2.19
+PyJWT==1.6.4
+pylint==2.1.1
+pytest==3.8.2
+python-coveralls==2.9.1
+python-dotenv==0.9.1
+pytz==2018.5
+PyYAML==3.13
+requests==2.19.1
+six==1.11.0
+typed-ast==1.1.0
+urllib3==1.23
+validate-email==1.3
+Werkzeug==0.14.1
+wrapt==1.10.11
diff --git a/run.py b/run.py
new file mode 100644
index 0000000..3da315a
--- /dev/null
+++ b/run.py
@@ -0,0 +1,10 @@
+import os
+
+from app import create_app
+
+
+app = create_app('production')
+
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file