├── .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 | 
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 | [](https://youtu.be/iZ2Tah3IxQc)
90 |
91 |
92 |
93 | ## Heroku
94 | [](
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 | [](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 |
--------------------------------------------------------------------------------