├── .github └── FUNDING.yml ├── .gitignore ├── .gitlab-ci.yml ├── DockerfileNginx ├── DockerfilePython ├── LICENSE ├── Procfile ├── app.json ├── app.py ├── docker-compose.yml ├── img ├── design_patterns_in_python_book.jpg ├── flag_au.gif ├── flag_ca.gif ├── flag_de.gif ├── flag_es.gif ├── flag_fr.gif ├── flag_in.gif ├── flag_it.gif ├── flag_jp.gif ├── flag_uk.gif └── flag_us.gif ├── nginx.conf ├── readme.md ├── requirements.txt ├── routes ├── __init__.py └── request_api.py ├── static ├── cosmo1.png └── swagger.json ├── swagger.png └── tests └── app_test.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Sean-Bradley]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: seanwasere 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: sean_bradley 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | __pycache__ 4 | model/__pycache__ 5 | routes/__pycache__ 6 | .vscode 7 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | build-master: 2 | image: docker:latest 3 | stage: build 4 | services: 5 | - docker:dind 6 | before_script: 7 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 8 | script: 9 | - docker build --pull -t "$CI_REGISTRY_IMAGE"/nginx -f DockerfileNginx . 10 | - docker push "$CI_REGISTRY_IMAGE"/nginx 11 | - docker build --pull -t "$CI_REGISTRY_IMAGE"/python -f DockerfilePython . 12 | - docker push "$CI_REGISTRY_IMAGE"/python 13 | only: 14 | - master 15 | 16 | # build: 17 | # image: docker:latest 18 | # stage: build 19 | # services: 20 | # - docker:dind 21 | # before_script: 22 | # - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 23 | # script: 24 | # - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" . 25 | # - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" 26 | # except: 27 | # - master 28 | -------------------------------------------------------------------------------- /DockerfileNginx: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY ./nginx.conf /etc/nginx/nginx.conf 4 | 5 | -------------------------------------------------------------------------------- /DockerfilePython: -------------------------------------------------------------------------------- 1 | FROM python:3.8-rc-slim 2 | 3 | COPY app.py /app/app.py 4 | COPY routes /app/routes 5 | COPY static /app/static 6 | COPY requirements.txt /app/requirements.txt 7 | 8 | RUN pip install -r /app/requirements.txt 9 | 10 | WORKDIR /app 11 | 12 | EXPOSE 5000 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 seanwasere youtube 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn -b :$PORT app:APP -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seans-python3-flask-rest-boilerplate", 3 | "description": "A barebones Python3 Flask Rest Example", 4 | "repository": "https://github.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate", 5 | "logo": "https://seans-python3-flask-rest.herokuapp.com/static/cosmo1.png", 6 | "keywords": ["python", "flask", "rest", "crud", "swagger-ui", "pylint", "pep8", "nosetests", "seanwasere"] 7 | } -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """A Python Flask REST API BoilerPlate (CRUD) Style""" 2 | 3 | import argparse 4 | import os 5 | from flask import Flask, jsonify, make_response 6 | from flask_cors import CORS 7 | from flask_swagger_ui import get_swaggerui_blueprint 8 | from routes import request_api 9 | 10 | APP = Flask(__name__) 11 | 12 | ### swagger specific ### 13 | SWAGGER_URL = '/swagger' 14 | API_URL = '/static/swagger.json' 15 | SWAGGERUI_BLUEPRINT = get_swaggerui_blueprint( 16 | SWAGGER_URL, 17 | API_URL, 18 | config={ 19 | 'app_name': "Seans-Python-Flask-REST-Boilerplate" 20 | } 21 | ) 22 | APP.register_blueprint(SWAGGERUI_BLUEPRINT, url_prefix=SWAGGER_URL) 23 | ### end swagger specific ### 24 | 25 | 26 | APP.register_blueprint(request_api.get_blueprint()) 27 | 28 | 29 | @APP.errorhandler(400) 30 | def handle_400_error(_error): 31 | """Return a http 400 error to client""" 32 | return make_response(jsonify({'error': 'Misunderstood'}), 400) 33 | 34 | 35 | @APP.errorhandler(401) 36 | def handle_401_error(_error): 37 | """Return a http 401 error to client""" 38 | return make_response(jsonify({'error': 'Unauthorised'}), 401) 39 | 40 | 41 | @APP.errorhandler(404) 42 | def handle_404_error(_error): 43 | """Return a http 404 error to client""" 44 | return make_response(jsonify({'error': 'Not found'}), 404) 45 | 46 | 47 | @APP.errorhandler(500) 48 | def handle_500_error(_error): 49 | """Return a http 500 error to client""" 50 | return make_response(jsonify({'error': 'Server error'}), 500) 51 | 52 | 53 | if __name__ == '__main__': 54 | 55 | PARSER = argparse.ArgumentParser( 56 | description="Seans-Python-Flask-REST-Boilerplate") 57 | 58 | PARSER.add_argument('--debug', action='store_true', 59 | help="Use flask debug/dev mode with file change reloading") 60 | ARGS = PARSER.parse_args() 61 | 62 | PORT = int(os.environ.get('PORT', 5000)) 63 | 64 | if ARGS.debug: 65 | print("Running in debug mode") 66 | CORS = CORS(APP) 67 | APP.run(host='0.0.0.0', port=PORT, debug=True) 68 | else: 69 | APP.run(host='0.0.0.0', port=PORT, debug=False) 70 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | container_name: nginx 6 | build: 7 | context: . 8 | dockerfile: DockerfileNginx 9 | ports: 10 | - "80:80" 11 | command: nginx -g "daemon off"; 12 | depends_on: 13 | - python_api 14 | 15 | python_api: 16 | container_name: python 17 | build: 18 | context: . 19 | dockerfile: DockerfilePython 20 | expose: 21 | - "5000" 22 | command: gunicorn -w 1 app:APP -b :5000 --reload 23 | -------------------------------------------------------------------------------- /img/design_patterns_in_python_book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/design_patterns_in_python_book.jpg -------------------------------------------------------------------------------- /img/flag_au.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_au.gif -------------------------------------------------------------------------------- /img/flag_ca.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_ca.gif -------------------------------------------------------------------------------- /img/flag_de.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_de.gif -------------------------------------------------------------------------------- /img/flag_es.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_es.gif -------------------------------------------------------------------------------- /img/flag_fr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_fr.gif -------------------------------------------------------------------------------- /img/flag_in.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_in.gif -------------------------------------------------------------------------------- /img/flag_it.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_it.gif -------------------------------------------------------------------------------- /img/flag_jp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_jp.gif -------------------------------------------------------------------------------- /img/flag_uk.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_uk.gif -------------------------------------------------------------------------------- /img/flag_us.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/img/flag_us.gif -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 1; 3 | pid /run/nginx.pid; 4 | events { 5 | worker_connections 768; 6 | } 7 | http { 8 | sendfile off; 9 | tcp_nopush on; 10 | tcp_nodelay on; 11 | keepalive_timeout 65; 12 | types_hash_max_size 2048; 13 | include /etc/nginx/mime.types; 14 | default_type application/octet-stream; 15 | gzip on; 16 | gzip_disable "msie6"; 17 | server { 18 | listen 80; 19 | server_name localhost; 20 | location / { 21 | proxy_pass http://python_api:5000/; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Seans Python3 Flask Rest Boilerplate 2 | 3 | ### MIT License 4 | Rememeber, No guarantees, or even fit for a particular purpose. 5 | 6 | This project will be updated slowly as required, so stay tuned. 7 | 8 | If you have a suggestion, or you want to contribute some code, you are free to make a pull request. 9 | 10 | Your contributions will be visible since this project is public. 11 | 12 | ### To Setup and Start 13 | ```bash 14 | pip install -r requirements.txt 15 | python app.py 16 | ``` 17 | 18 | ### Get All Request Records 19 | ```bash 20 | curl -X GET http://127.0.0.1:5000/request 21 | ``` 22 | 23 | ### Get One Request Record 24 | ```bash 25 | curl -X GET http://127.0.0.1:5000/request/04cfc704-acb2-40af-a8d3-4611fab54ada 26 | ``` 27 | 28 | ### Add A New Record 29 | ```bash 30 | curl -X POST http://127.0.0.1:5000/request -H 'Content-Type: application/json' -d '{"title":"Good & Bad Book", "email": "testuser3@test.com"}' 31 | ``` 32 | 33 | ### Edit An Existing Record 34 | ```bash 35 | curl -X PUT http://127.0.0.1:5000/request -H 'Content-Type: application/json' -d '{"title":"edited Good & Bad Book", "email": "testuser4@test.com"}' 36 | ``` 37 | 38 | ### Delete A Record 39 | ```bash 40 | curl -X DELETE http://127.0.0.1:5000/request/04cfc704-acb2-40af-a8d3-4611fab54ada 41 | ``` 42 | 43 | ## Unit Test with Nose 44 | ```bash 45 | nosetests --verbosity=2 46 | ``` 47 | 48 | ### Test Output 49 | ```bash 50 | $ nosetests --verbose --nocapture 51 | app_test.test_get_all_requests ... ok 52 | app_test.test_get_individual_request ... ok 53 | app_test.test_get_individual_request_404 ... ok 54 | app_test.test_add_new_record ... ok 55 | app_test.test_get_new_record ... ok 56 | app_test.test_edit_new_record_title ... ok 57 | app_test.test_edit_new_record_email ... ok 58 | app_test.test_add_new_record_bad_email_format ... ok 59 | app_test.test_add_new_record_bad_title_key ... ok 60 | app_test.test_add_new_record_no_email_key ... ok 61 | app_test.test_add_new_record_no_title_key ... ok 62 | app_test.test_add_new_record_unicode_title ... ok 63 | app_test.test_add_new_record_no_payload ... ok 64 | app_test.test_delete_new_record ... ok 65 | app_test.test_delete_new_record_404 ... ok 66 | 67 | ------------------------------------------------------------------------------------ 68 | Ran 15 tests in 15.285s 69 | 70 | OK 71 | ``` 72 | 73 | 74 | ## Swagger UI 75 | ![swagger.png](swagger.png) 76 | 77 | Hosted Locally 78 | http://127.0.0.1:5000/swagger/ 79 | 80 | ### 81 | Hosted via Heroku 82 | https://seans-python3-flask-rest.herokuapp.com/swagger/ 83 | 84 | ### 85 | Hosted via Docker-compose and Nginx 86 | http://127.0.0.1/swagger/ 87 | 88 | ### Video Tutorial on adding Swagger-UI to this Python Flask API 89 | [![Video Tutorial on adding Swagger-UI to Python Flask API](https://img.youtube.com/vi/iZ2Tah3IxQc/0.jpg)](https://youtu.be/iZ2Tah3IxQc) 90 | 91 | 92 | 93 | ## Heroku 94 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)]( 95 | https://heroku.com/deploy?template=https://github.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate) 96 | 97 | You can also test this api on heroku. 98 | 99 | Live : https://seans-python3-flask-rest.herokuapp.com/request 100 | 101 | use the above curl commands replacing `http://127.0.0.1` with `https://seans-python3-flask-rest.herokuapp.com` 102 | 103 | ### Video Tutorial Hosting this Python Flask Rest API on Heroku 104 | 105 | [![Video Tutorial Hosting this Python Flask Rest API on Heroku](https://img.youtube.com/vi/O_xEqtjh1io/0.jpg)](https://youtu.be/O_xEqtjh1io) 106 | 107 | # Design Patterns In Python 108 | 109 | To help support this project, please check out my book titled **Design Patterns In Python** 110 | 111 | 112 | 113 |    https://www.amazon.com/dp/B08XLJ8Z2J
114 |    https://www.amazon.co.uk/dp/B08XLJ8Z2J
115 |    https://www.amazon.in/dp/B08Z282SBC
116 |    https://www.amazon.de/dp/B08XLJ8Z2J
117 |    https://www.amazon.fr/dp/B08XLJ8Z2J
118 |    https://www.amazon.es/dp/B08XLJ8Z2J
119 |    https://www.amazon.it/dp/B08XLJ8Z2J
120 |    https://www.amazon.co.jp/dp/B08XLJ8Z2J
121 |    https://www.amazon.ca/dp/B08XLJ8Z2J
122 |    https://www.amazon.com.au/dp/B08Z282SBC 123 | 124 | ASIN : B08XLJ8Z2J / B08Z282SBC 125 | 126 | --- 127 | 128 | Thanks 129 | 130 | Sean 131 | 132 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | Werkzeug==3.0.6 3 | flask_cors==6.0.0 4 | gunicorn==23.0.0 5 | validate_email==1.3 6 | nose==1.3.7 7 | requests==2.32.4 8 | argparse==1.4.0 9 | uuid==1.30 10 | flask-swagger-ui==3.20.9 -------------------------------------------------------------------------------- /routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/routes/__init__.py -------------------------------------------------------------------------------- /routes/request_api.py: -------------------------------------------------------------------------------- 1 | """The Endpoints to manage the BOOK_REQUESTS""" 2 | import uuid 3 | from datetime import datetime, timedelta 4 | from flask import jsonify, abort, request, Blueprint 5 | 6 | from validate_email import validate_email 7 | REQUEST_API = Blueprint('request_api', __name__) 8 | 9 | 10 | def get_blueprint(): 11 | """Return the blueprint for the main app module""" 12 | return REQUEST_API 13 | 14 | 15 | BOOK_REQUESTS = { 16 | "8c36e86c-13b9-4102-a44f-646015dfd981": { 17 | 'title': u'Good Book', 18 | 'email': u'testuser1@test.com', 19 | 'timestamp': (datetime.today() - timedelta(1)).timestamp() 20 | }, 21 | "04cfc704-acb2-40af-a8d3-4611fab54ada": { 22 | 'title': u'Bad Book', 23 | 'email': u'testuser2@test.com', 24 | 'timestamp': (datetime.today() - timedelta(2)).timestamp() 25 | } 26 | } 27 | 28 | 29 | @REQUEST_API.route('/request', methods=['GET']) 30 | def get_records(): 31 | """Return all book requests 32 | @return: 200: an array of all known BOOK_REQUESTS as a \ 33 | flask/response object with application/json mimetype. 34 | """ 35 | return jsonify(BOOK_REQUESTS) 36 | 37 | 38 | @REQUEST_API.route('/request/', methods=['GET']) 39 | def get_record_by_id(_id): 40 | """Get book request details by it's id 41 | @param _id: the id 42 | @return: 200: a BOOK_REQUESTS as a flask/response object \ 43 | with application/json mimetype. 44 | @raise 404: if book request not found 45 | """ 46 | if _id not in BOOK_REQUESTS: 47 | abort(404) 48 | return jsonify(BOOK_REQUESTS[_id]) 49 | 50 | 51 | @REQUEST_API.route('/request', methods=['POST']) 52 | def create_record(): 53 | """Create a book request record 54 | @param email: post : the requesters email address 55 | @param title: post : the title of the book requested 56 | @return: 201: a new_uuid as a flask/response object \ 57 | with application/json mimetype. 58 | @raise 400: misunderstood request 59 | """ 60 | if not request.get_json(): 61 | abort(400) 62 | data = request.get_json(force=True) 63 | 64 | if not data.get('email'): 65 | abort(400) 66 | if not validate_email(data['email']): 67 | abort(400) 68 | if not data.get('title'): 69 | abort(400) 70 | 71 | new_uuid = str(uuid.uuid4()) 72 | book_request = { 73 | 'title': data['title'], 74 | 'email': data['email'], 75 | 'timestamp': datetime.now().timestamp() 76 | } 77 | BOOK_REQUESTS[new_uuid] = book_request 78 | # HTTP 201 Created 79 | return jsonify({"id": new_uuid}), 201 80 | 81 | 82 | @REQUEST_API.route('/request/', methods=['PUT']) 83 | def edit_record(_id): 84 | """Edit a book request record 85 | @param email: post : the requesters email address 86 | @param title: post : the title of the book requested 87 | @return: 200: a booke_request as a flask/response object \ 88 | with application/json mimetype. 89 | @raise 400: misunderstood request 90 | """ 91 | if _id not in BOOK_REQUESTS: 92 | abort(404) 93 | 94 | if not request.get_json(): 95 | abort(400) 96 | data = request.get_json(force=True) 97 | 98 | if not data.get('email'): 99 | abort(400) 100 | if not validate_email(data['email']): 101 | abort(400) 102 | if not data.get('title'): 103 | abort(400) 104 | 105 | book_request = { 106 | 'title': data['title'], 107 | 'email': data['email'], 108 | 'timestamp': datetime.now().timestamp() 109 | } 110 | 111 | BOOK_REQUESTS[_id] = book_request 112 | return jsonify(BOOK_REQUESTS[_id]), 200 113 | 114 | 115 | @REQUEST_API.route('/request/', methods=['DELETE']) 116 | def delete_record(_id): 117 | """Delete a book request record 118 | @param id: the id 119 | @return: 204: an empty payload. 120 | @raise 404: if book request not found 121 | """ 122 | if _id not in BOOK_REQUESTS: 123 | abort(404) 124 | 125 | del BOOK_REQUESTS[_id] 126 | 127 | return '', 204 128 | -------------------------------------------------------------------------------- /static/cosmo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/static/cosmo1.png -------------------------------------------------------------------------------- /static/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "description": "sean", 5 | "version": "1.0.0", 6 | "title": "Seans-Python3-Flask-Rest-Boilerplate", 7 | "license": { 8 | "name": "MIT", 9 | "url": "https://opensource.org/licenses/MIT" 10 | } 11 | }, 12 | "servers": [ 13 | { 14 | "url": "/" 15 | } 16 | ], 17 | "tags": [ 18 | { 19 | "name": "Book Request", 20 | "description": "Example API for requesting and return book requests" 21 | } 22 | ], 23 | "paths": { 24 | "/request": { 25 | "get": { 26 | "tags": [ 27 | "Book Request" 28 | ], 29 | "summary": "Returns bookRequests", 30 | "responses": { 31 | "200": { 32 | "description": "OK", 33 | "schema": { 34 | "$ref": "#/components/schemas/bookRequests" 35 | } 36 | } 37 | } 38 | }, 39 | "post": { 40 | "tags": [ 41 | "Book Request" 42 | ], 43 | "summary": "Create a new book request system", 44 | "requestBody": { 45 | "description": "Book Request Post Object", 46 | "required": true, 47 | "content": { 48 | "application/json": { 49 | "schema": { 50 | "$ref": "#/components/schemas/bookRequestPostBody" 51 | } 52 | } 53 | } 54 | }, 55 | "produces": [ 56 | "application/json" 57 | ], 58 | "responses": { 59 | "201": { 60 | "description": "OK", 61 | "schema": { 62 | "$ref": "#/components/schemas/id" 63 | } 64 | }, 65 | "400": { 66 | "description": "Failed. Bad post data." 67 | } 68 | } 69 | } 70 | }, 71 | "/request/{id}": { 72 | "parameters": [ 73 | { 74 | "name": "id", 75 | "in": "path", 76 | "required": true, 77 | "description": "ID of the cat that we want to match", 78 | "type": "string" 79 | } 80 | ], 81 | "get": { 82 | "tags": [ 83 | "Book Request" 84 | ], 85 | "summary": "Get book request with given ID", 86 | "parameters": [ 87 | { 88 | "in": "path", 89 | "name": "id", 90 | "required": true, 91 | "description": "Book Request id", 92 | "schema": { 93 | "$ref": "#/components/schemas/id" 94 | } 95 | } 96 | ], 97 | "responses": { 98 | "200": { 99 | "description": "OK", 100 | "schema": { 101 | "$ref": "#/components/schemas/bookRequest" 102 | } 103 | }, 104 | "400": { 105 | "description": "Failed. Misunderstood Request." 106 | }, 107 | "404": { 108 | "description": "Failed. Book request not found." 109 | } 110 | } 111 | }, 112 | "put": { 113 | "summary": "edit a book request by ID", 114 | "tags": [ 115 | "Book Request" 116 | ], 117 | "parameters": [ 118 | { 119 | "in": "path", 120 | "name": "id", 121 | "required": true, 122 | "description": "Book Request id", 123 | "schema": { 124 | "$ref": "#/components/schemas/id" 125 | } 126 | } 127 | ], 128 | "requestBody": { 129 | "description": "Book Request Object", 130 | "required": true, 131 | "content": { 132 | "application/json": { 133 | "schema": { 134 | "$ref": "#/components/schemas/bookRequest" 135 | } 136 | } 137 | } 138 | }, 139 | "produces": [ 140 | "application/json" 141 | ], 142 | "responses": { 143 | "200": { 144 | "description": "OK", 145 | "schema": { 146 | "$ref": "#/components/schemas/bookRequest" 147 | } 148 | }, 149 | "400": { 150 | "description": "Failed. Bad post data." 151 | } 152 | } 153 | }, 154 | "delete": { 155 | "summary": "Delete Book Request by ID", 156 | "tags": [ 157 | "Book Request" 158 | ], 159 | "parameters": [ 160 | { 161 | "in": "path", 162 | "name": "id", 163 | "required": true, 164 | "description": "Book Request Id", 165 | "schema": { 166 | "$ref": "#/components/schemas/id" 167 | } 168 | } 169 | ], 170 | "responses": { 171 | "204": { 172 | "description": "OK", 173 | "schema": { 174 | "$ref": "#/components/schemas/id" 175 | } 176 | }, 177 | "400": { 178 | "description": "Failed. Misunderstood Request." 179 | }, 180 | "404": { 181 | "description": "Failed. Book Request not found." 182 | } 183 | } 184 | } 185 | } 186 | }, 187 | "components": { 188 | "schemas": { 189 | "id": { 190 | "properties": { 191 | "uuid": { 192 | "type": "string" 193 | } 194 | } 195 | }, 196 | "bookRequestPostBody": { 197 | "type": "object", 198 | "properties": { 199 | "title": { 200 | "type": "string", 201 | "format": "string" 202 | }, 203 | "email": { 204 | "type": "string", 205 | "format": "email" 206 | } 207 | } 208 | }, 209 | "bookRequest": { 210 | "type": "object", 211 | "properties": { 212 | "title": { 213 | "type": "string", 214 | "format": "string" 215 | }, 216 | "email": { 217 | "type": "string", 218 | "format": "email" 219 | }, 220 | "timestamp": { 221 | "type": "string", 222 | "format": "number" 223 | } 224 | } 225 | }, 226 | "bookRequests": { 227 | "type": "object", 228 | "properties": { 229 | "bookRequest": { 230 | "type": "object", 231 | "additionalProperties": { 232 | "$ref": "#/components/schemas/bookRequest" 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/Seans-Python3-Flask-Rest-Boilerplate/f01f8b1698d4761f666ff7874549a556b276ec06/swagger.png -------------------------------------------------------------------------------- /tests/app_test.py: -------------------------------------------------------------------------------- 1 | """The tests to run in this project. 2 | To run the tests type, 3 | $ nosetests --verbose 4 | """ 5 | 6 | from nose.tools import assert_true 7 | import requests 8 | 9 | # BASE_URL = "http://127.0.0.1" 10 | BASE_URL = "http://localhost:5000" 11 | # BASE_URL = "https://python3-flask-uat.herokuapp.com/" 12 | 13 | 14 | class NewUUID(): # pylint: disable=too-few-public-methods 15 | """The new uuid created when creating a new book request. 16 | The NewUUID is used for further tests 17 | """ 18 | 19 | def __init__(self, value): 20 | self.value = value 21 | #NEW_UUID = None 22 | 23 | 24 | def test_get_all_requests(): 25 | "Test getting all requests" 26 | response = requests.get('%s/request' % (BASE_URL)) 27 | assert_true(response.ok) 28 | 29 | 30 | def test_get_individual_request(): 31 | "Test getting an individual request" 32 | response = requests.get( 33 | '%s/request/04cfc704-acb2-40af-a8d3-4611fab54ada' % (BASE_URL)) 34 | assert_true(response.ok) 35 | 36 | 37 | def test_get_individual_request_404(): 38 | "Test getting a non existent request" 39 | response = requests.get('%s/request/an_incorrect_id' % (BASE_URL)) 40 | assert_true(response.status_code == 404) 41 | 42 | 43 | def test_add_new_record(): 44 | "Test adding a new record" 45 | payload = {'title': 'Good & Bad Book', 'email': 'testuser3@test.com'} 46 | response = requests.post('%s/request' % (BASE_URL), json=payload) 47 | NewUUID.value = str(response.json()['id']) 48 | assert_true(response.status_code == 201) 49 | 50 | 51 | def test_get_new_record(): 52 | "Test getting the new record" 53 | url = '%s/request/%s' % (BASE_URL, NewUUID.value) 54 | response = requests.get(url) 55 | assert_true(response.ok) 56 | 57 | 58 | def test_edit_new_record_title(): 59 | "Test editing the new records title" 60 | payload = {'title': 'edited Good & Bad Book', 61 | 'email': 'testuser3@test.com'} 62 | response = requests.put('%s/request/%s' % 63 | (BASE_URL, NewUUID.value), json=payload) 64 | assert_true(response.json()['title'] == "edited Good & Bad Book") 65 | 66 | 67 | def test_edit_new_record_email(): 68 | "Test editing the new records email" 69 | payload = {'title': 'edited Good & Bad Book', 70 | 'email': 'testuser4@test.com'} 71 | response = requests.put('%s/request/%s' % 72 | (BASE_URL, NewUUID.value), json=payload) 73 | assert_true(response.json()['email'] == "testuser4@test.com") 74 | 75 | 76 | def test_add_new_record_bad_email_format(): 77 | "Test adding a new record with a bad email" 78 | payload = {'title': 'Good & Bad Book', 'email': 'badEmailFormat'} 79 | response = requests.post('%s/request' % (BASE_URL), json=payload) 80 | assert_true(response.status_code == 400) 81 | 82 | 83 | def test_add_new_record_bad_title_key(): 84 | "Test adding a new record with a bad title" 85 | payload = {'badTitleKey': 'Good & Bad Book', 'email': 'testuser4@test.com'} 86 | response = requests.post('%s/request' % (BASE_URL), json=payload) 87 | assert_true(response.status_code == 400) 88 | 89 | 90 | def test_add_new_record_no_email_key(): 91 | "Test adding a new record no email" 92 | payload = {'title': 'Good & Bad Book'} 93 | response = requests.post('%s/request' % (BASE_URL), json=payload) 94 | assert_true(response.status_code == 400) 95 | 96 | 97 | def test_add_new_record_no_title_key(): 98 | "Test adding a new record no title" 99 | payload = {'email': 'testuser5@test.com'} 100 | response = requests.post('%s/request' % (BASE_URL), json=payload) 101 | assert_true(response.status_code == 400) 102 | 103 | 104 | def test_add_new_record_unicode_title(): 105 | "Test adding a new record with a unicode title" 106 | payload = {'title': '▚Ⓜ⌇⇲', 'email': 'testuser5@test.com'} 107 | response = requests.post('%s/request' % (BASE_URL), json=payload) 108 | assert_true(response.ok) 109 | 110 | 111 | def test_add_new_record_no_payload(): 112 | "Test adding a new record with no payload" 113 | payload = None 114 | response = requests.post('%s/request' % (BASE_URL), json=payload) 115 | assert_true(response.status_code == 400) 116 | 117 | 118 | def test_delete_new_record(): 119 | "Test deleting the new record" 120 | response = requests.delete('%s/request/%s' % (BASE_URL, NewUUID.value)) 121 | assert_true(response.status_code == 204) 122 | 123 | 124 | def test_delete_new_record_404(): 125 | "Test deleting the new record that was already deleted" 126 | response = requests.delete('%s/request/%s' % (BASE_URL, NewUUID.value)) 127 | assert_true(response.status_code == 404) 128 | --------------------------------------------------------------------------------