├── .gitignore ├── Part - 0 ├── Part-0 Hello Flask Rest API.md └── movie-bag │ ├── Pipfile │ ├── Pipfile.lock │ └── app.py ├── Part - 1 ├── Part-1 Using MongoDB with Flask.md └── movie-bag │ ├── Pipfile │ ├── Pipfile.lock │ ├── app.py │ └── database │ ├── db.py │ └── models.py ├── Part - 2 ├── Part-2 Better Structure with Blueprint and Flask-restful.md └── movie-bag │ ├── Pipfile │ ├── Pipfile.lock │ ├── app.py │ ├── database │ ├── db.py │ └── models.py │ └── resources │ ├── movie.py │ └── routes.py ├── Part - 3 ├── Part-3 Authenticaion and authorization.md └── movie-bag │ ├── .env │ ├── Pipfile │ ├── Pipfile.lock │ ├── app.py │ ├── database │ ├── db.py │ └── models.py │ └── resources │ ├── auth.py │ ├── movie.py │ └── routes.py ├── Part - 4 ├── Part-4 Exception Handling.md ├── master.patch └── movie-bag │ ├── .env │ ├── Pipfile │ ├── Pipfile.lock │ ├── app.py │ ├── database │ ├── db.py │ └── models.py │ └── resources │ ├── auth.py │ ├── errors.py │ ├── movie.py │ └── routes.py ├── Part - 5 ├── Part-5 Password Reset.md └── movie-bag │ ├── .env │ ├── Pipfile │ ├── Pipfile.lock │ ├── app.py │ ├── database │ ├── db.py │ └── models.py │ ├── resources │ ├── auth.py │ ├── errors.py │ ├── movie.py │ ├── reset_password.py │ └── routes.py │ ├── run.py │ ├── services │ └── mail_service.py │ └── templates │ └── email │ ├── reset_password.html │ └── reset_password.txt └── Part - 6 ├── Part-6 Testing REST APIs.md └── movie-bag ├── .env ├── .env.test ├── Pipfile ├── Pipfile.lock ├── app.py ├── database ├── db.py └── models.py ├── resources ├── auth.py ├── errors.py ├── movie.py ├── reset_password.py └── routes.py ├── run.py ├── services └── mail_service.py ├── templates └── email │ ├── reset_password.html │ └── reset_password.txt └── tests ├── BaseCase.py ├── __init__.py ├── test_create_movie.py ├── test_get_movies.py ├── test_login.py └── test_signup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /Part - 0/Part-0 Hello Flask Rest API.md: -------------------------------------------------------------------------------- 1 | ## Part 0: Setup & Basic CRUD API 2 | 3 | Howdy! Welcome to the Flask Rest API - Zero to Yoda, tutorial series. We will go through building a Movie database where a user can (Add, Edit, Update & delete) `Genre`, `Movie`, and `Casts`. First of all, we will start with a basic APIs structure with just `Flask` and then learn about integrating `MongoDB` with the application, finally, we will learn about structuring our API following the best practices with the minimal setup using `Flask-RESTful`. 4 | 5 | What we are going to learn in this series? 6 | - [Flask](https://palletsprojects.com/p/flask/) - For our web server. 7 | - [flask-restful](https://flask-restful.readthedocs.io/en/latest/installation.html) - For building cool Rest-APIs. 8 | - [Pipenv](https://pipenv.readthedocs.io/en/latest/) For managing python virtual environments. 9 | - [mongoengine](http://docs.mongoengine.org/projects/flask-mongoengine/en/latest/) - For managing our database. 10 | - [flask-marshmallow](https://flask-marshmallow.readthedocs.io/en/latest/) - For serializing user requests. 11 | - [Postman](https://www.getpostman.com/downloads/) - For testing our APIs 12 | 13 | 14 | Why flask? 15 | - Easy to get started 😊 16 | - Great for building Rest APIs and microservices. 17 | - Used by big companies like Netflix, Reddit, Lyft, Et cetera. 18 | - Great for building APIS for machine learning applications. 19 | - Force is strong with this one 😉 20 | 21 | Who is this series for? 22 | - Beginners with basic Python knowledge who wants to build cool apps. 23 | - Experienced flask developers who have been working with flask `SSR` (Server Side Rendering). 24 | 25 | ### So, let's get started. 26 | First of all, create a new directory and browse to the newly created directory, I'm gonna call it `movie-bag`. 27 | 28 | ``` 29 | mkdir movie-bag 30 | cd movie-bag 31 | ``` 32 | 33 | First of all install `pipenv` 34 | using command 35 | 36 | ``` 37 | pip install --user pipenv 38 | ``` 39 | 40 | `Pipenv` is used to create a `virtual environment` which isolates the python packages you used in this project from other system python packages. So, that you can have a different version of same python packages for different projects. 41 | 42 | Now, let's install `flask` using `pipenv` 43 | ``` 44 | pipenv install flask 45 | ``` 46 | This will create a new virtual environment and install flask. This command will create two files `Pipfile` and `Pipfile.lock`. 47 | ``` 48 | #~ movie-bag/Pipfile 49 | 50 | [[source]] 51 | name = "pypi" 52 | url = "https://pypi.org/simple" 53 | verify_ssl = true 54 | 55 | [dev-packages] 56 | 57 | [packages] 58 | flask = "*" 59 | 60 | [requires] 61 | python_version = "3.7" 62 | ``` 63 | 64 | Pipfile contains the packages that are required for your application. As you can see `flask` is added to `[packages]` list. This means when someone downloads your code and runs `pipenv install`, `flask` gets installed in their system. Great, right? 65 | 66 | If you are familiar with `requirements.txt`, think `Pipfile` as the `requirements.txt` on steroids. 67 | 68 | 69 | Flask is so simple that you can create an API using a single file. (But you don't have to 😅) 70 | 71 | Create a new file called `app.py` where we are gonna write our Flask Hello World API. Write the following code in `app.py` 72 | 73 | ```python 74 | #~movie-bag/app.py 75 | 76 | from flask import Flask 77 | 78 | app = Flask(__name__) 79 | 80 | @app.route('/') 81 | def hello(): 82 | return {'hello': 'world'} 83 | 84 | 85 | app.run() 86 | ``` 87 | 88 | Let's understand what we just wrote. First of all, we imported the `Flask` class from `flask` package. 89 | 90 | Then we define a root endpoint with `@app.route('/')`, `@app.route()` is called a `decorator` which basically takes function `hello()` extends it's behavior so that it is invoked when `/` endpoint is requested. And `hello()` returns `{'hello': 'world'}`, finally `Flask` server is started with `app.run()` 91 | 92 | Wanna learn more about decorators? Read this great article {% link https://dev.to/sonnk/python-decorator-and-flask-4c16 %} 93 | 94 | There you go, you have made yourself your very first Flask API (Pat on your back). 95 | 96 | To run the app, first, enable the virtual environment that you created earlier while installing `Flask` with 97 | 98 | ``` 99 | pipenv shell 100 | ``` 101 | Now run the app using, 102 | ``` 103 | python app.py 104 | ``` 105 | 106 | The output looks like this: 107 | ``` 108 | * Serving Flask app "app" (lazy loading) 109 | * Environment: production 110 | WARNING: This is a development server. Do not use it in a production deployment. 111 | Use a production WSGI server instead. 112 | * Debug mode: off 113 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 114 | ``` 115 | 116 | As you can see the app is running on `http://127.0.0.1:5000/`. Type the URL in your browser of choice and see the `JSON` response from the server. 117 | 118 | *Note: Alias for 127.0.0.1 is localhost, So you can access your API at http:localhost:5000 aswell* 119 | 120 | Now let's update our `app.py` to have more fun 😉 121 | 122 | ```diff 123 | #~/movie-bag/app.py 124 | 125 | -from flask import Flask 126 | +from flask import Flask, jsonify 127 | 128 | app = Flask(__name__) 129 | 130 | +movies = [ 131 | + { 132 | + "name": "The Shawshank Redemption", 133 | + "casts": ["Tim Robbins", "Morgan Freeman", "Bob Gunton", "William Sadler"], 134 | + "genres": ["Drama"] 135 | + }, 136 | + { 137 | + "name": "The Godfather ", 138 | + "casts": ["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"], 139 | + "genres": ["Crime", "Drama"] 140 | + } 141 | +] 142 | + 143 | -@app.route('/') 144 | +@app.route('/movies') 145 | def hello(): 146 | - return {'hello': 'world'} 147 | + return jsonify(movies) 148 | 149 | 150 | app.run() 151 | ``` 152 | 153 | *Note:* *In the code snippet above, `-`(`red`) represents the part of the previous code that was removed and `+`(`green`) represents the code that replaced it. So, if you are coding along with me. Only copy the `green` codes excluding `+` sign.* 154 | 155 | Here we imported `jsonify` from `flask` which is used to convert our `movies` list into proper `JSON` value. 156 | Notice we also renamed our API endpoint from `/` to `/movies`. 157 | So, now our API should be accessable at `http://localhost:5000/movies` 158 | 159 | To see the changes, restart your `Flask` server. 160 | 161 | 162 | To work with our APIs, we are going to use `Postman`. Postman is used for testing the APIs with different HTTP methods. Such as sending data to our web server with `POST` request, updating the data in our server with `PUT` request, getting the data from the server with `GET` and deleting them with `DELETE` request. 163 | 164 | Learn more about [REST API HTTP methods here.](https://restfulapi.net/http-methods/) 165 | 166 | Install [Postman](https://www.getpostman.com/downloads/) to test your API endpoint easily. 167 | 168 | After installing `Postman`, open a new tab and send the `GET` request to your server (http://localhost:5000). 169 | 170 | ![Postman get request response](https://thepracticaldev.s3.amazonaws.com/i/fzj8uyk1fec894r28xu0.png) 171 | *GET request response using Postman* 172 | 173 | 174 | Ok now we are ready to add new API endpoints for `CRUD` (Create, Update, Retrieve & Delete) 175 | 176 | Add the following code in `app.py` before `app.run()` 177 | 178 | ```diff 179 | #~movie-bag/app.py 180 | 181 | -from flask import Flask, jsonify 182 | +from flask import Flask, jsonify, request 183 | 184 | app = Flask(__name__) 185 | 186 | @@ -19,5 +19,21 @@ movies = [ 187 | def hello(): 188 | return jsonify(movies) 189 | 190 | +@app.route('/movies', methods=['POST']) 191 | +def add_movie(): 192 | + movie = request.get_json() 193 | + movies.append(movie) 194 | + return {'id': len(movies)}, 200 195 | + 196 | +@app.route('/movies/', methods=['PUT']) 197 | +def update_movie(index): 198 | + movie = request.get_json() 199 | + movies[index] = movie 200 | + return jsonify(movies[index]), 200 201 | + 202 | +@app.route('/movies/', methods=['DELETE']) 203 | +def delete_movie(index): 204 | + movies.pop(index) 205 | + return 'None', 200 206 | 207 | app.run() 208 | ``` 209 | 210 | As you can see `@app.route()` can take one more argument in addition to the API endpoint. Which is `methods` for API endpoint. 211 | 212 | What we have just added are: 213 | 1) `@app.route('/movies', methods=['POST'])` 214 | endpoint for adding a new movie to our `movies` list. This endpoint accepts the `POST` request. With `POST` request you can send a new movie for the list.
215 | *Use postman to send a movie via POST request* 216 | - Select `POST` from the dropdown 217 | - Click on the `Body` tab and then click `raw`. 218 | - Select `JSON` from the drop down (indicating we are sending the data to our server in JSON format.) 219 | - Enter the following movie details and hit `SEND` 220 | ```json 221 | { 222 | "name": "The Dark Knight", 223 | "casts": ["Christian Bale", "Heath Ledger", "Aaron Eckhart", "Michael Caine"], 224 | "genres": ["Action", "Crime", "Drama"] 225 | } 226 | ``` 227 | ![Postman POST request response](https://thepracticaldev.s3.amazonaws.com/i/6zll28s9eli18nhc91j8.png) 228 | *POST request response using Postman* 229 |
230 | In the response you get the id of the recently added movie 231 | ```json 232 | {"id": 2} 233 | ``` 234 | - Now again send `GET` request and see a list of `3` movies is responsed by the server 😊 235 | 236 | 2) `@app.route('/movies/', methods=['PUT'])` endpoint for editing the movie which is already existing on the list based on it's `index` as suggested by ``. Similarly for `PUT` request also you have to include the `JSON` body for the movie you want to update.
237 | 238 | ![Postman PUT request response](https://thepracticaldev.s3.amazonaws.com/i/4abhnnqiqszksbg88m8q.png) 239 | 240 | *PUT request response using Postman* 241 | - Now send a `GET` request to see the movie you updated actually getting updated on the list of movies. 242 | 243 | 3) `@app.route('/movies/', methods=['DELETE'])` API endpoint for deleting the movie from the given index of movies list. 244 | ![Postman DELETE request response](https://thepracticaldev.s3.amazonaws.com/i/bpbmmfh3p12vppax6s81.png) 245 | *DELETE request response using Postman* 246 | - Now send a `GET` request to `/movies` and see that the movie is already removed from the `movies` list. 247 | 248 | ### What we learned from this part of the series? 249 | - Install `Flask` in a new virtual environment using `pipenv` 250 | - Create a simple hello world flask application 251 | - Create API endpoints with `CRUD` functionality. 252 | 253 | 254 | That's it for this part of the series y'all. 255 | You can find the code for this part [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%200). 256 | 257 | In this part, we only learned how to store movies in a python list but in the next part of the series, we are going to learn how we can use `Mongoengine` to store our movies in the real `MongoDB` database. 258 | 259 | Until then happy coding 😊 -------------------------------------------------------------------------------- /Part - 0/movie-bag/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "*" 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /Part - 0/movie-bag/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3e0db1a484d33cd9be0c95731d950880091cc5071606da532df1ab0e154d9cf9" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aniso8601": { 20 | "hashes": [ 21 | "sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072", 22 | "sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a" 23 | ], 24 | "version": "==8.0.0" 25 | }, 26 | "click": { 27 | "hashes": [ 28 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 29 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 30 | ], 31 | "version": "==7.0" 32 | }, 33 | "flask": { 34 | "hashes": [ 35 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 36 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 37 | ], 38 | "index": "pypi", 39 | "version": "==1.1.1" 40 | }, 41 | "flask-restful": { 42 | "hashes": [ 43 | "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", 44 | "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" 45 | ], 46 | "index": "pypi", 47 | "version": "==0.3.7" 48 | }, 49 | "itsdangerous": { 50 | "hashes": [ 51 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 52 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 53 | ], 54 | "version": "==1.1.0" 55 | }, 56 | "jinja2": { 57 | "hashes": [ 58 | "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", 59 | "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" 60 | ], 61 | "version": "==2.10.3" 62 | }, 63 | "markupsafe": { 64 | "hashes": [ 65 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 66 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 67 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 68 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 69 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 70 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 71 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 72 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 73 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 74 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 75 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 76 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 77 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 78 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 79 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 80 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 81 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 82 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 83 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 84 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 85 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 86 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 87 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 88 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 89 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 90 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 91 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 92 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 93 | ], 94 | "version": "==1.1.1" 95 | }, 96 | "pytz": { 97 | "hashes": [ 98 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 99 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 100 | ], 101 | "version": "==2019.3" 102 | }, 103 | "six": { 104 | "hashes": [ 105 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 106 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 107 | ], 108 | "version": "==1.13.0" 109 | }, 110 | "werkzeug": { 111 | "hashes": [ 112 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 113 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 114 | ], 115 | "version": "==0.16.0" 116 | } 117 | }, 118 | "develop": {} 119 | } 120 | -------------------------------------------------------------------------------- /Part - 0/movie-bag/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | 3 | app = Flask(__name__) 4 | 5 | movies = [ 6 | { 7 | "name": "The Shawshank Redemption", 8 | "casts": ["Tim Robbins", "Morgan Freeman", "Bob Gunton", "William Sadler"], 9 | "genres": ["Drama"] 10 | }, 11 | { 12 | "name": "The Godfather ", 13 | "casts": ["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"], 14 | "genres": ["Crime", "Drama"] 15 | } 16 | ] 17 | 18 | @app.route('/movies') 19 | def hello(): 20 | return jsonify(movies) 21 | 22 | @app.route('/movies', methods=['POST']) 23 | def add_movie(): 24 | movie = request.get_json() 25 | movies.append(movie) 26 | return {'id': len(movies)}, 200 27 | 28 | @app.route('/movies/', methods=['PUT']) 29 | def update_movie(index): 30 | movie = request.get_json() 31 | movies[index] = movie 32 | return jsonify(movies[index]), 200 33 | 34 | @app.route('/movies/', methods=['DELETE']) 35 | def delete_movie(index): 36 | movies.pop(index) 37 | return 'None', 200 38 | 39 | app.run() -------------------------------------------------------------------------------- /Part - 1/Part-1 Using MongoDB with Flask.md: -------------------------------------------------------------------------------- 1 | ## Part 1: Using MongoDB with Flask 2 | 3 | Howdy! In the last [Part](https://dev.to/paurakhsharma/flask-rest-api-part-0-setup-basic-crud-api-4650) of the series, we learned how to create a basic `CRUD` REST API functionality using python `list`. But that's not how the real-world applications are built, because if your server is restarted or god forbids crashes then you are gonna lose all the information stored in your server. To solve those problems (and many others) database is used. So, that's what we are gonna do. We are going to use [MongoDB](https://docs.mongodb.com/manual/) as our database. 4 | 5 | If you are just starting from this part, you can find all the code we wrote till now [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%200). 6 | 7 | Before we start make sure you have installed MongoDB in your system. If you haven't already you can install for [Linux](https://docs.mongodb.com/manual/administration/install-on-linux/), [Windown](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/) and [macOS](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/). 8 | 9 | There are mainly to popular libraries which makes working with MongoDB easier: 10 | 11 | 1) [Pymongo](https://api.mongodb.com/python/current/) is a low-level Python wrapper around MongoDB, working with `Pymongo` is similar to writing the MongoDB query directly. 12 | Here is the simple example of updating the name of a movie whose `id` matches the given `id` using `Pymongo`. 13 | ```python 14 | db['movies'].update({'_id': id}, 15 | {'$set': {'name': 'My new title'}}) 16 | ``` 17 | `Pymongo` doesn't use any predefined schema so it can make full use of Schemaless nature of MongoDB. 18 | 19 | 2) [MongoEngine](http://docs.mongoengine.org/) is an Object-Document Mapper, which uses a document schema that makes working with MongoDB clear and easier. 20 | Here is the same example using `mongoengine`. 21 | ```python 22 | Movies.objects(id=id).update(name='My new title') 23 | ``` 24 | `Mongoengine` uses predefined schema for the fields in the database which restricts it from using the Schemaless nature of MongoDB. 25 | 26 | As we can see both sides have their advantages and disadvantages. So, choose the one that fits your project well. In this series we are going to learn about `Mongoengine`, please do let me know in the comment section below if you want me to cover `Pymongo` as well. 27 | 28 | To work better with `Mongoengine` in our `Flask` application there is a great `Flask` extension called [Flask-Mongengine](http://docs.mongoengine.org/projects/flask-mongoengine/en/latest/). 29 | 30 | So, let's get started by installing `flask-mongoengine`. 31 | ``` 32 | pipenv install flask-mongoengine 33 | ``` 34 | *Note: Since `flask-mongoengine` is built on top of `mongoengine` it gets installed automatically while installing flask-mongoengine, also `mongoengine` is build on top of `pymongo` so, it also gets installed* 35 | 36 | 37 | Now, let's create a new folder inside `movie-bag`. I am gonna call it `database`. Inside `database` folder create a file named `db.py`. Also, create another file and name it `models.py` 38 | 39 | Let's see how files/folder looks like now. 40 | 41 | ```bash 42 | movie-bag 43 | │ app.py 44 | | Pipfile 45 | | Pipfile.lock 46 | └───database 47 | │ db.py 48 | └───models.py 49 | ``` 50 | 51 | Now, let's dive into the interesting part. 52 | First of all, let's initialize our database by adding the following code to our `db.py` 53 | 54 | ```python 55 | #~movie-bag/database/db.py 56 | 57 | from flask_mongoengine import MongoEngine 58 | 59 | db = MongoEngine() 60 | 61 | def initialize_db(app): 62 | db.init_app(app) 63 | ``` 64 | Here we have imported `MongoEngine` and created the `db` object and we have defined a function `initialize_db()` which we are gonna call from our `app.py` to initialize the database. 65 | 66 | 67 | Let's write the following code in our `movie.py` inside `models` directory 68 | 69 | ```python 70 | #~movie-bag/database/models.py 71 | from .db import db 72 | 73 | class Movie(db.Document): 74 | name = db.StringField(required=True, unique=True) 75 | casts = db.ListField(db.StringField(), required=True) 76 | genres = db.ListField(db.StringField(), required=True) 77 | ``` 78 | What we just created is a document for our database. So, that the users cannot add other fields then what are defined here. 79 | Here we can see the `Movie` document has three fields: 80 | 1) `name`: is a field of type `String`, we also have two constraints in this field. 81 | - `required` which means the user cannot create a new movie without giving its title. 82 | - `unique` which means the movie name must be unique and cannot be repeated. 83 | 84 | 2) `casts`: is a field of type `list` which contains the values of type `String` 85 | 86 | 3) `genres`: same as `casts` 87 | 88 | Finally, we can initialize the database in our `app.py` and change our `view` functions (functions handling our API request) to use the `Movie` document we defined earlier. 89 | 90 | ```diff 91 | #~movie-bag/app.py 92 | 93 | -from flask import Flask, jsonify, request 94 | +from flask import Flask, request, Response 95 | +from database.db import initialize_db 96 | +from database.models import Movie 97 | 98 | app = Flask(__name__) 99 | 100 | -movies = [ 101 | - { 102 | - "name": "The Shawshank Redemption", 103 | - "casts": ["Tim Robbins", "Morgan Freeman", "Bob Gunton", "William Sadler"], 104 | - "genres": ["Drama"] 105 | - }, 106 | - { 107 | - "name": "The Godfather ", 108 | - "casts": ["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"], 109 | - "genres": ["Crime", "Drama"] 110 | - } 111 | -] 112 | +app.config['MONGODB_SETTINGS'] = { 113 | + 'host': 'mongodb://localhost/movie-bag' 114 | +} 115 | + 116 | +initialize_db(app) 117 | 118 | -@app.route('/movies') 119 | -def hello(): 120 | - return jsonify(movies) 121 | 122 | +@app.route('/movies') 123 | +def get_movies(): 124 | + movies = Movie.objects().to_json() 125 | + return Response(movies, mimetype="application/json", status=200) 126 | 127 | -@app.route('/movies', methods=['POST']) 128 | -def add_movie(): 129 | - movie = request.get_json() 130 | - movies.append(movie) 131 | - return {'id': len(movies)}, 200 132 | 133 | +@app.route('/movies', methods=['POST']) 134 | + body = request.get_json() 135 | + movie = Movie(**body).save() 136 | + id = movie.id 137 | + return {'id': str(id)}, 200 138 | 139 | -@app.route('/movies/', methods=['PUT']) 140 | -def update_movie(index): 141 | - movie = request.get_json() 142 | - movies[index] = movie 143 | - return jsonify(movies[index]), 200 144 | 145 | +@app.route('/movies/', methods=['PUT']) 146 | +def update_movie(id): 147 | + body = request.get_json() 148 | + Movie.objects.get(id=id).update(**body) 149 | + return '', 200 150 | 151 | -@app.route('/movies/', methods=['DELETE']) 152 | -def delete_movie(index): 153 | - movies.pop(index) 154 | - return 'None', 200 155 | 156 | +@app.route('/movies/', methods=['DELETE']) 157 | +def delete_movie(id): 158 | + Movie.objects.get(id=id).delete() 159 | + return '', 200 160 | 161 | app.run() 162 | ``` 163 | 164 | Wow! that a lot of changes, let's go step by step with the changes. 165 | 166 | ```diff 167 | -from flask import Flask, jsonify, request 168 | +from flask import Flask, request, Response 169 | +from database.db import initialize_db 170 | +from database.models.movie import Movie 171 | ``` 172 | 173 | Here we removed `jsonify` as we no longer need and added `Response` which we use to set the type of response. Then we import `initialize_db` form `db.py` which we defined earlier to initialize our database. And lastly, we imported the `Movie` document form `movie.py` 174 | 175 | ```diff 176 | +app.config['MONGODB_SETTINGS'] = { 177 | + 'host': 'mongodb://localhost/movie-bag' 178 | +} 179 | + 180 | +db = initialize_db(app) 181 | ``` 182 | 183 | Here we set the configuration for our mongodb database. Here the host is in the format `/`. Since we have installed mongodb locally so we can access it from `mongodb://localhost/` and we are gonna name our database `movie-bag`. 184 | And at the last, we initialize our database. 185 | 186 | ```diff 187 | +@app.route('/movies') 188 | +def get_movies(): 189 | + movies = Movie.objects().to_json() 190 | + return Response(movies, mimetype="application/json", status=200) 191 | + 192 | ``` 193 | Here we get all the objects from `Movie` document using `Movies.objects()` and convert them to `JSON` using `to_json()`. At last, we return a `Response` object, where we defined our response type to `application/json`. 194 | 195 | 196 | ```diff 197 | +@app.route('/movies', methods=['POST']) 198 | + body = request.get_json() 199 | + movie = Movie(**body).save() 200 | + id = movie.id 201 | + return {'id': str(id)}, 200 202 | ``` 203 | 204 | In the `POST` request we first get the `JSON` that we send and a request. And then we load the `Movie` document with the fields from our request with `Movie(**body)`. Here `**` is called the spread operator which is written as `...` in JavaScript (if you are familiar with it.). What it does is like the name suggests, spreads the `dict` object.
205 | So, that `Movie(**body)` becomes 206 | ```python 207 | Movie(name="Name of the movie", 208 | casts=["a caste"], 209 | genres=["a genre"]) 210 | ``` 211 | At last, we save the document and get its `id` which we return as a response. 212 | 213 | ```diff 214 | +@app.route('/movies/', methods=['PUT']) 215 | +def update_movie(id): 216 | + body = request.get_json() 217 | + Movie.objects.get(id=id).update(**body) 218 | + return '', 200 219 | ``` 220 | 221 | Here we first find the Movie document matching the `id` sent in the request and then update it. Here also we have applied the spread operator to pass the values to the `update()` function. 222 | 223 | 224 | ```diff 225 | +@app.route('/movies/', methods=['DELETE']) 226 | +def delete_movie(id): 227 | + Movie.objects.get(id=id).delete() 228 | + return '', 200 229 | ``` 230 | Similar to the `update_movie()` here we get the Movie document matching given `id` and delete it from the database. 231 | 232 | Oh, **I just remembered** that we haven't added the API endpoint to `GET` only one document from our server. 233 | Let's add it: 234 | Add the following code right above `app.run()` 235 | 236 | ```python 237 | @app.route('/movies/') 238 | def get_movie(id): 239 | movies = Movie.objects.get(id=id).to_json() 240 | return Response(movies, mimetype="application/json", status=200) 241 | ``` 242 | Now you can get the single movie from API endpoint `/movies/`. 243 | 244 | To run the server make sure you are at `movie-bag` directory. 245 | 246 | Then run 247 | ```bash 248 | pipenv shell 249 | python app.py 250 | ``` 251 | 252 | To activate the virtual environment in your terminal and start the server. 253 | 254 | Wow! Congratulations on making this far. To test the APIs, use `Postman` as we used in the [previous]((https://dev.to/paurakhsharma/flask-rest-api-part-0-setup-basic-crud-api-4650)) part of this series. 255 | 256 | You might have noticed that if we send invalid data to our endpoint e.g: without a name, or other fields we get an unfriendly error in the form of `HTML`. If we try to get the movie document with `id` that doesn't exist in the database then also we get an unfriendly error in the form of `HTML` response. Which is not an excepted behavior of a nicely build API. We will learn how we can handle such errors in the later parts of the series. 257 | 258 | ### What we learned from this part of the series? 259 | - Difference between `Pymongo` and `Mongoengine`. 260 | - How to create Document schema using `Mongoengine`. 261 | - How to perform `CRUD` operation using `Mongoengine`. 262 | - Python spread operator. 263 | 264 | You can find the complete code of this part [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%201) 265 | 266 | In the next part, we are going to learn how to better structure your flask application using `Blueprint`. And also how to create REST APIs faster, following best practices with the minimal setup using `flask-restful` 267 | 268 | Until then happy coding 😊 -------------------------------------------------------------------------------- /Part - 1/movie-bag/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "*" 10 | flask-mongoengine = "*" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /Part - 1/movie-bag/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "f4c5b8cb26eb59dc3b48c3aa07f085b9cb649b6774b50b704ed76575ddcecb73" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "click": { 20 | "hashes": [ 21 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 22 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 23 | ], 24 | "version": "==7.0" 25 | }, 26 | "flask": { 27 | "hashes": [ 28 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 29 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 30 | ], 31 | "index": "pypi", 32 | "version": "==1.1.1" 33 | }, 34 | "flask-mongoengine": { 35 | "hashes": [ 36 | "sha256:0f426aeafc4be2c37e9b4c0f8b5d02d012b7afc4b3b97a4119024684fe148fc1", 37 | "sha256:b6376b33cd1e624c09983a0884dc87303e54084f0e6b7f8df6794d56d35fa66f" 38 | ], 39 | "index": "pypi", 40 | "version": "==0.9.5" 41 | }, 42 | "flask-wtf": { 43 | "hashes": [ 44 | "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", 45 | "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" 46 | ], 47 | "version": "==0.14.2" 48 | }, 49 | "itsdangerous": { 50 | "hashes": [ 51 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 52 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 53 | ], 54 | "version": "==1.1.0" 55 | }, 56 | "jinja2": { 57 | "hashes": [ 58 | "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", 59 | "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" 60 | ], 61 | "version": "==2.10.3" 62 | }, 63 | "markupsafe": { 64 | "hashes": [ 65 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 66 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 67 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 68 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 69 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 70 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 71 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 72 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 73 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 74 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 75 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 76 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 77 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 78 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 79 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 80 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 81 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 82 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 83 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 84 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 85 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 86 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 87 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 88 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 89 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 90 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 91 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 92 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 93 | ], 94 | "version": "==1.1.1" 95 | }, 96 | "mongoengine": { 97 | "hashes": [ 98 | "sha256:9301ca84ada9377a200a50541f9be7d5308081bf2112049d00e1dd163f80b940", 99 | "sha256:fa3e73c966fca2b814cc1103ac4f55bcca7aae05028b112ef0cc8b321ee4a2f7" 100 | ], 101 | "version": "==0.18.2" 102 | }, 103 | "pymongo": { 104 | "hashes": [ 105 | "sha256:0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", 106 | "sha256:08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", 107 | "sha256:0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", 108 | "sha256:0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", 109 | "sha256:1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", 110 | "sha256:22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", 111 | "sha256:264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", 112 | "sha256:3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", 113 | "sha256:339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", 114 | "sha256:38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", 115 | "sha256:4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", 116 | "sha256:4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", 117 | "sha256:4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", 118 | "sha256:4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", 119 | "sha256:4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", 120 | "sha256:53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", 121 | "sha256:681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", 122 | "sha256:6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", 123 | "sha256:72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", 124 | "sha256:7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", 125 | "sha256:72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", 126 | "sha256:87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", 127 | "sha256:87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", 128 | "sha256:88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", 129 | "sha256:89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", 130 | "sha256:908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", 131 | "sha256:9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", 132 | "sha256:9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", 133 | "sha256:98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", 134 | "sha256:99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", 135 | "sha256:9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", 136 | "sha256:a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", 137 | "sha256:a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", 138 | "sha256:a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", 139 | "sha256:b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", 140 | "sha256:bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", 141 | "sha256:c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", 142 | "sha256:c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", 143 | "sha256:c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", 144 | "sha256:c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", 145 | "sha256:ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", 146 | "sha256:d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", 147 | "sha256:d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", 148 | "sha256:d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", 149 | "sha256:d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", 150 | "sha256:dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", 151 | "sha256:e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", 152 | "sha256:e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", 153 | "sha256:e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", 154 | "sha256:e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", 155 | "sha256:ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", 156 | "sha256:fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", 157 | "sha256:fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1" 158 | ], 159 | "version": "==3.10.0" 160 | }, 161 | "six": { 162 | "hashes": [ 163 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 164 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 165 | ], 166 | "version": "==1.13.0" 167 | }, 168 | "werkzeug": { 169 | "hashes": [ 170 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 171 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 172 | ], 173 | "version": "==0.16.0" 174 | }, 175 | "wtforms": { 176 | "hashes": [ 177 | "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", 178 | "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" 179 | ], 180 | "version": "==2.2.1" 181 | } 182 | }, 183 | "develop": {} 184 | } 185 | -------------------------------------------------------------------------------- /Part - 1/movie-bag/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, Response 2 | from database.db import initialize_db 3 | from database.models import Movie 4 | import json 5 | 6 | app = Flask(__name__) 7 | 8 | app.config['MONGODB_SETTINGS'] = { 9 | 'host': 'mongodb://localhost/movie-bag' 10 | } 11 | 12 | initialize_db(app) 13 | 14 | @app.route('/movies') 15 | def get_movies(): 16 | movies = Movie.objects().to_json() 17 | return Response(movies, mimetype="application/json", status=200) 18 | 19 | @app.route('/movies', methods=['POST']) 20 | def add_movie(): 21 | body = request.get_json() 22 | movie = Movie(**body).save() 23 | id = movie.id 24 | return {'id': str(id)}, 200 25 | 26 | @app.route('/movies/', methods=['PUT']) 27 | def update_movie(id): 28 | body = request.get_json() 29 | Movie.objects.get(id=id).update(**body) 30 | return '', 200 31 | 32 | @app.route('/movies/', methods=['DELETE']) 33 | def delete_movie(id): 34 | movie = Movie.objects.get(id=id).delete() 35 | return '', 200 36 | 37 | @app.route('/movies/') 38 | def get_movie(id): 39 | movies = Movie.objects.get(id=id).to_json() 40 | return Response(movies, mimetype="application/json", status=200) 41 | 42 | app.run() -------------------------------------------------------------------------------- /Part - 1/movie-bag/database/db.py: -------------------------------------------------------------------------------- 1 | from flask_mongoengine import MongoEngine 2 | 3 | db = MongoEngine() 4 | 5 | def initialize_db(app): 6 | db.init_app(app) -------------------------------------------------------------------------------- /Part - 1/movie-bag/database/models.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | class Movie(db.Document): 4 | name = db.StringField(required=True, unique=True) 5 | casts = db.ListField(db.StringField(), required=True) 6 | genres = db.ListField(db.StringField(), required=True) 7 | -------------------------------------------------------------------------------- /Part - 2/Part-2 Better Structure with Blueprint and Flask-restful.md: -------------------------------------------------------------------------------- 1 | ## Part 2: Better Structure with Blueprint and Flask-restful 2 | 3 | Howdy! In the previous [Part](https://dev.to/paurakhsharma/flask-rest-api-part-1-using-mongodb-with-flask-3g7d) of the series, we learned how to use `Mongoengine` to store our movies data into a MongoDB database. Now let's learn how you can structure your flask application in a more maintainable way. 4 | 5 | If you are just starting from this part, you can find all the code we wrote till now [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%201). 6 | 7 | We are going to learn two ways of structuring the flask application: 8 | - [Blueprint](https://flask.palletsprojects.com/en/1.1.x/blueprints/): It is used to structure the Flask application into different components, making structuring the application based on different functionality. 9 | 10 | - [Flask-restful](https://flask-restful.readthedocs.io/en/latest/): It is an extension for Flask that helps your build REST APIs quickly and following best practices. 11 | 12 | *Note: `Blueprint` and `Flask-restful` are not a replacement for each other, they can co-exist in a single project* 13 | 14 | ### Structuring Flask App using Blueprint 15 | 16 | Create a new folder `resources` inside `mongo-bag` and a new file `movie.py` inside `resources.` 17 | 18 | ```bash 19 | mkdir resources 20 | cd resources 21 | touch movie.py 22 | ``` 23 | 24 | Now move all the route related codes from your `app.py` into `movies.py` 25 | 26 | ```python 27 | #~/movie-bag/resources/movie.py 28 | 29 | @app.route('/movies') 30 | def get_movies(): 31 | movies = Movie.objects().to_json() 32 | return Response(movies, mimetype="application/json", status=200) 33 | 34 | @app.route('/movies', methods=['POST']) 35 | def add_movie(): 36 | body = request.get_json() 37 | movie = Movie(**body).save() 38 | id = movie.id 39 | return {'id': str(id)}, 200 40 | 41 | @app.route('/movies/', methods=['PUT']) 42 | def update_movie(id): 43 | body = request.get_json() 44 | Movie.objects.get(id=id).update(**body) 45 | return '', 200 46 | 47 | @app.route('/movies/', methods=['DELETE']) 48 | def delete_movie(id): 49 | movie = Movie.objects.get(id=id).delete() 50 | return '', 200 51 | 52 | @app.route('/movies/') 53 | def get_movie(id): 54 | movies = Movie.objects.get(id=id).to_json() 55 | return Response(movies, mimetype="application/json", status=200) 56 | ``` 57 | 58 | And you app.py file should look like this 59 | 60 | ```python 61 | #~/movie-bag/app.py 62 | 63 | from flask import Flask, request, Response 64 | from database.db import initialize_db 65 | from database.models import Movie 66 | import json 67 | 68 | app = Flask(__name__) 69 | 70 | app.config['MONGODB_SETTINGS'] = { 71 | 'host': 'mongodb://localhost/movie-bag' 72 | } 73 | 74 | initialize_db(app) 75 | 76 | app.run() 77 | 78 | ``` 79 |
80 | 81 | Clean right? 82 | Now you might be wondering how does this work? - This doesn't work. 83 | We must first create a blueprint in our `movie.py` 84 | 85 | Update your `movie.py` like so, 86 | 87 | ```diff 88 | #~/movie-bag/resources/movie.py 89 | 90 | +from flask import Blueprint, Response, request 91 | +from database.models import Movie 92 | + 93 | +movies = Blueprint('movies', __name__) 94 | 95 | -@app.route('/movies') 96 | +@movies.route('/movies') 97 | def get_movies(): 98 | movies = Movie.objects().to_json() 99 | return Response(movies, mimetype="application/json", status=200) 100 | 101 | -@app.route('/movies', methods=['POST']) 102 | +@movies.route('/movies', methods=['POST']) 103 | def add_movie(): 104 | body = request.get_json() 105 | movie = Movie(**body).save() 106 | id = movie.id 107 | return {'id': str(id)}, 200 108 | 109 | -@app.route('/movies/', methods=['PUT']) 110 | +@movies.route('/movies/', methods=['PUT']) 111 | def update_movie(id): 112 | body = request.get_json() 113 | Movie.objects.get(id=id).update(**body) 114 | return '', 200 115 | 116 | -@app.route('/movies/', methods=['DELETE']) 117 | +@movies.route('/movies/', methods=['DELETE']) 118 | def delete_movie(id): 119 | movie = Movie.objects.get(id=id).delete() 120 | return '', 200 121 | 122 | -@app.route('/movies/') 123 | +@movies.route('/movies/') 124 | def get_movie(id): 125 | movies = Movie.objects.get(id=id).to_json() 126 | return Response(movies, mimetype="application/json", status=200) 127 | ``` 128 | 129 | So, we have created a new `Blueprint` using 130 | 131 | ```diff 132 | +movies = Blueprint('movies', __name__) 133 | ``` 134 | with arguments `name` and `import_name`. Usually, import_name will just be `__name__`, which is a special Python variable containing the name of the current module. 135 | 136 | Now we can replace every instance of `app` inside this `Blueprint` with `movies`. 137 | 138 | So, let's update our `app.py` to register the `Blueprint` we created. 139 | 140 | ```diff 141 | #~/movie-bag/app.py 142 | 143 | -from flask import Flask, request, Response 144 | +from flask import Flask 145 | from database.db import initialize_db 146 | -from database.models import Movie 147 | -import json 148 | +from resources.movie import movies 149 | 150 | app = Flask(__name__) 151 | 152 | app.config['MONGODB_SETTINGS'] = { 153 | 'host': 'mongodb://localhost/movie-bag' 154 | } 155 | 156 | initialize_db(app) 157 | +app.register_blueprint(movies) 158 | 159 | app.run() 160 | ``` 161 | 162 | That's it, you have used `Blueprint` to structure your `Flask` application. And it looks way cleaner than it was before. 163 | 164 | ### Structuring Flask REST API using Flask-restful 165 | 166 | Now, let's get to the main topic we have been waiting for from the beginning. 167 | 168 | Installing flask-restful 169 | 170 | ```bash 171 | pipenv install flask-restful 172 | ``` 173 | 174 | Now, let's update our `movie.py` to use `flask-restful` 175 | 176 | ```diff 177 | #~movie-bag/resources/movie.py 178 | 179 | -from flask import Blueprint, Response, request 180 | +from flask import Response, request 181 | from database.models import Movie 182 | +from flask_restful import Resource 183 | + 184 | -movies = Blueprint('movies', __name__) 185 | +class MoviesApi(Resource): 186 | + def get(self): 187 | + movies = Movie.objects().to_json() 188 | + return Response(movies, mimetype="application/json", status=200) 189 | + 190 | + def post(self): 191 | + body = request.get_json() 192 | + movie = Movie(**body).save() 193 | + id = movie.id 194 | + return {'id': str(id)}, 200 195 | + 196 | +class MovieApi(Resource): 197 | + def put(self, id): 198 | + body = request.get_json() 199 | + Movie.objects.get(id=id).update(**body) 200 | + return '', 200 201 | + 202 | + def delete(self, id): 203 | + movie = Movie.objects.get(id=id).delete() 204 | + return '', 200 205 | + def get(self, id): 206 | + movies = Movie.objects.get(id=id).to_json() 207 | + return Response(movies, mimetype="application/json", status=200) 208 | + 209 | -@movies.route('/') 210 | -def get_movies(): 211 | - movies = Movie.objects().to_json() 212 | - return Response(movies, mimetype="application/json", status=200) 213 | - 214 | -@movies.route('/', methods=['POST']) 215 | -def add_movie(): 216 | - body = request.get_json() 217 | - movie = Movie(**body).save() 218 | - id = movie.id 219 | - return {'id': str(id)}, 200 220 | - 221 | -@movies.route('/', methods=['PUT']) 222 | -def update_movie(id): 223 | - body = request.get_json() 224 | - Movie.objects.get(id=id).update(**body) 225 | - return '', 200 226 | - 227 | -@movies.route('/', methods=['DELETE']) 228 | -def delete_movie(id): 229 | - movie = Movie.objects.get(id=id).delete() 230 | - return '', 200 231 | - 232 | -@movies.route('/') 233 | -def get_movie(id): 234 | - movies = Movie.objects.get(id=id).to_json() 235 | - return Response(movies, mimetype="application/json", status=200) 236 | 237 | ``` 238 | 239 | As we can see `flask-restful` uses a Class-based syntex so, if we want to define a resource (i.e API) we can just define a class which extends `flask-restful`'s `Resource` 240 | i.e 241 | ```diff 242 | +class MoviesApi(Resource): 243 | + def get(self): 244 | + movies = Movie.objects().to_json() 245 | + return Response(movies, mimetype="application/json", status=200) 246 | ``` 247 | This creates an endpoint which accepts `GET` request. 248 | 249 | Now let's register these endpoints that we just created. 250 | Let's create a new file `routes.py` inside `resources` directory and add the following to it. 251 | 252 | ```python 253 | #~movie-bag/resources/routes.py 254 | 255 | from .movie import MoviesApi, MovieApi 256 | 257 | def initialize_routes(api): 258 | api.add_resource(MoviesApi, '/movies') 259 | api.add_resource(MovieApi, '/movies/') 260 | ``` 261 | 262 | We have defined the function to initialize the routes. Let's call this function from our `app.py` 263 | 264 | ```diff 265 | #~/movie-bag/app.py 266 | 267 | -from resources.movie import movies 268 | +from flask_restful import Api 269 | +from resources.routes import initialize_routes 270 | 271 | app = Flask(__name__) 272 | +api = Api(app) 273 | 274 | app.config['MONGODB_SETTINGS'] = { 275 | 'host': 'mongodb://localhost/movie-bag' 276 | } 277 | 278 | initialize_db(app) 279 | -app.register_blueprint(movies) 280 | - 281 | +initialize_routes(api) 282 | 283 | app.run() 284 | 285 | ``` 286 | Here we first created an `Api` instance with `app = Api(app)` and then initialized the API routes with `initialize_routes(api)` 287 | 288 | Wow! we did it y'all. Now we can access our movies at `http://localhost:5000/movies`. As we can see just by looking at the URL we cannot know if this is an API. So, let's update our `routes.py` to add `api/` in front of our API routes. 289 | 290 | 291 | ```diff 292 | #~movie-bag/resources/routes.py 293 | - api.add_resource(MoviesApi, '/movies') 294 | - api.add_resource(MovieApi, '/movies/') 295 | + api.add_resource(MoviesApi, '/api/movies') 296 | + api.add_resource(MovieApi, '/api/movies/') 297 | ``` 298 | 299 | Now we can access our movies at `htpp://localhost:5000/api/movies`. 300 | 301 | You can find the complete code of this part [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%202) 302 | 303 | 304 | ### What we learned from this part of the series? 305 | - How to structure our Flask App using `Blueprint` 306 | - How to create REST APIs using `Flask-restful` 307 | 308 | I guess that's it for this [Flask Rest API - Zero to Yoda](https://dev.to/paurakhsharma/flask-rest-api-part-0-setup-basic-crud-api-4650) series. I hope you learned something of value from this series. I had lots of fun while making this series. I hope you had a mutual feeling. 309 | 310 |
311 | 312 | Please let me know if you want to learn how to create a frontend for this REST APIs using modern JavaScript frameworks like [Vue.js](https://vuejs.org/), [Svelte](https://svelte.dev/) and [React.js](https://reactjs.org/) 313 | 314 |
315 | 316 | Also, comment down if you want to learn other concepts in Flask like authentication, internalization, sending mail, testing, deploying, etc. 317 |
318 | 319 | 320 | Until then happy coding 😊 -------------------------------------------------------------------------------- /Part - 2/movie-bag/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "*" 10 | flask-mongoengine = "*" 11 | flask-restful = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | -------------------------------------------------------------------------------- /Part - 2/movie-bag/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3ab9ae3c3952468cfad35f39a276d4efc55f0e65bb0dbce0f305f77be2cbb428" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aniso8601": { 20 | "hashes": [ 21 | "sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072", 22 | "sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a" 23 | ], 24 | "version": "==8.0.0" 25 | }, 26 | "click": { 27 | "hashes": [ 28 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 29 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 30 | ], 31 | "version": "==7.0" 32 | }, 33 | "flask": { 34 | "hashes": [ 35 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 36 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 37 | ], 38 | "index": "pypi", 39 | "version": "==1.1.1" 40 | }, 41 | "flask-mongoengine": { 42 | "hashes": [ 43 | "sha256:0f426aeafc4be2c37e9b4c0f8b5d02d012b7afc4b3b97a4119024684fe148fc1", 44 | "sha256:b6376b33cd1e624c09983a0884dc87303e54084f0e6b7f8df6794d56d35fa66f" 45 | ], 46 | "index": "pypi", 47 | "version": "==0.9.5" 48 | }, 49 | "flask-restful": { 50 | "hashes": [ 51 | "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", 52 | "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" 53 | ], 54 | "index": "pypi", 55 | "version": "==0.3.7" 56 | }, 57 | "flask-wtf": { 58 | "hashes": [ 59 | "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", 60 | "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" 61 | ], 62 | "version": "==0.14.2" 63 | }, 64 | "itsdangerous": { 65 | "hashes": [ 66 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 67 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 68 | ], 69 | "version": "==1.1.0" 70 | }, 71 | "jinja2": { 72 | "hashes": [ 73 | "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", 74 | "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" 75 | ], 76 | "version": "==2.10.3" 77 | }, 78 | "markupsafe": { 79 | "hashes": [ 80 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 81 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 82 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 83 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 84 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 85 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 86 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 87 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 88 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 89 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 90 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 91 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 92 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 93 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 94 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 95 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 96 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 97 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 98 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 99 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 100 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 101 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 102 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 103 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 104 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 105 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 106 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 107 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 108 | ], 109 | "version": "==1.1.1" 110 | }, 111 | "mongoengine": { 112 | "hashes": [ 113 | "sha256:9301ca84ada9377a200a50541f9be7d5308081bf2112049d00e1dd163f80b940", 114 | "sha256:fa3e73c966fca2b814cc1103ac4f55bcca7aae05028b112ef0cc8b321ee4a2f7" 115 | ], 116 | "version": "==0.18.2" 117 | }, 118 | "pymongo": { 119 | "hashes": [ 120 | "sha256:0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", 121 | "sha256:08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", 122 | "sha256:0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", 123 | "sha256:0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", 124 | "sha256:1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", 125 | "sha256:22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", 126 | "sha256:264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", 127 | "sha256:3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", 128 | "sha256:339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", 129 | "sha256:38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", 130 | "sha256:4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", 131 | "sha256:4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", 132 | "sha256:4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", 133 | "sha256:4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", 134 | "sha256:4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", 135 | "sha256:53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", 136 | "sha256:681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", 137 | "sha256:6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", 138 | "sha256:72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", 139 | "sha256:7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", 140 | "sha256:72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", 141 | "sha256:87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", 142 | "sha256:87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", 143 | "sha256:88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", 144 | "sha256:89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", 145 | "sha256:908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", 146 | "sha256:9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", 147 | "sha256:9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", 148 | "sha256:98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", 149 | "sha256:99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", 150 | "sha256:9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", 151 | "sha256:a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", 152 | "sha256:a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", 153 | "sha256:a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", 154 | "sha256:b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", 155 | "sha256:bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", 156 | "sha256:c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", 157 | "sha256:c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", 158 | "sha256:c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", 159 | "sha256:c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", 160 | "sha256:ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", 161 | "sha256:d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", 162 | "sha256:d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", 163 | "sha256:d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", 164 | "sha256:d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", 165 | "sha256:dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", 166 | "sha256:e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", 167 | "sha256:e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", 168 | "sha256:e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", 169 | "sha256:e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", 170 | "sha256:ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", 171 | "sha256:fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", 172 | "sha256:fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1" 173 | ], 174 | "version": "==3.10.0" 175 | }, 176 | "pytz": { 177 | "hashes": [ 178 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 179 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 180 | ], 181 | "version": "==2019.3" 182 | }, 183 | "six": { 184 | "hashes": [ 185 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 186 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 187 | ], 188 | "version": "==1.13.0" 189 | }, 190 | "werkzeug": { 191 | "hashes": [ 192 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 193 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 194 | ], 195 | "version": "==0.16.0" 196 | }, 197 | "wtforms": { 198 | "hashes": [ 199 | "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", 200 | "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" 201 | ], 202 | "version": "==2.2.1" 203 | } 204 | }, 205 | "develop": {} 206 | } 207 | -------------------------------------------------------------------------------- /Part - 2/movie-bag/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from database.db import initialize_db 3 | from flask_restful import Api 4 | from resources.routes import initialize_routes 5 | 6 | app = Flask(__name__) 7 | api = Api(app) 8 | 9 | app.config['MONGODB_SETTINGS'] = { 10 | 'host': 'mongodb://localhost/movie-bag' 11 | } 12 | 13 | initialize_db(app) 14 | initialize_routes(api) 15 | 16 | app.run() -------------------------------------------------------------------------------- /Part - 2/movie-bag/database/db.py: -------------------------------------------------------------------------------- 1 | from flask_mongoengine import MongoEngine 2 | 3 | db = MongoEngine() 4 | 5 | def initialize_db(app): 6 | db.init_app(app) -------------------------------------------------------------------------------- /Part - 2/movie-bag/database/models.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | class Movie(db.Document): 4 | name = db.StringField(required=True, unique=True) 5 | casts = db.ListField(db.StringField(), required=True) 6 | genres = db.ListField(db.StringField(), required=True) 7 | -------------------------------------------------------------------------------- /Part - 2/movie-bag/resources/movie.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from database.models import Movie 3 | from flask_restful import Resource 4 | 5 | class MoviesApi(Resource): 6 | def get(self): 7 | movies = Movie.objects().to_json() 8 | return Response(movies, mimetype="application/json", status=200) 9 | 10 | def post(self): 11 | body = request.get_json() 12 | movie = Movie(**body).save() 13 | id = movie.id 14 | return {'id': str(id)}, 200 15 | 16 | class MovieApi(Resource): 17 | def put(self, id): 18 | body = request.get_json() 19 | Movie.objects.get(id=id).update(**body) 20 | return '', 200 21 | 22 | def delete(self, id): 23 | movie = Movie.objects.get(id=id).delete() 24 | return '', 200 25 | 26 | def get(self, id): 27 | movies = Movie.objects.get(id=id).to_json() 28 | return Response(movies, mimetype="application/json", status=200) 29 | -------------------------------------------------------------------------------- /Part - 2/movie-bag/resources/routes.py: -------------------------------------------------------------------------------- 1 | from .movie import MoviesApi, MovieApi 2 | 3 | def initialize_routes(api): 4 | api.add_resource(MoviesApi, '/api/movies') 5 | api.add_resource(MovieApi, '/api/movies/') 6 | -------------------------------------------------------------------------------- /Part - 3/Part-3 Authenticaion and authorization.md: -------------------------------------------------------------------------------- 1 | 2 | Howdy! In the previous [Part](https://dev.to/paurakhsharma/flask-rest-api-part-2-better-structure-with-blueprint-and-flask-restful-2n93) of the series, we learned how to use `Blueprint` and `Flask-Restful` to structure our Flask REST API in a more maintainable way. 3 | 4 | Currently, anyone can read, add, delete and update the movies in our application. Now, let's learn how we can restrict the creation of movies by any untrusted person (`Authentication`). Also, we will learn how to implement `Authorization` so that only the person who added the movie in our application can delete/modify it. 5 | 6 | 7 | To implement these features, first of all, we must create a new document model to store the user information. So, let's do it. 8 | 9 | Similar to how we created our `Movie` document model we are going to create a `User` document model. Let's add the following code after the `Movie` document model. 10 | 11 | ```python 12 | #~/movie-bag/database/models.py 13 | 14 | ... 15 | 16 | class User(db.Document): 17 | email = db.EmailField(required=True, unique=True) 18 | password = db.StringField(required=True, min_length=6) 19 | ``` 20 | 21 | Here we created this so that when the user signs up, a new user document is created with fields `email` and `password`.
22 | 23 | But saving a password in the plain `StringField` is a terrible idea. If somebody gets access to your database, all user passwords are exposed. To prevent that from happening, we are going to `hash` our password to some `cryptic` form so that nobody can find out the real password easily. 24 | 25 | For hashing our password we are going to use a popular hashing function called `bcrypt`. You might have already guessed it, we are going to use a flask extension called [flask-bcrypt](https://flask-bcrypt.readthedocs.io/en/latest/) for this. 26 | 27 | Let's install `flask-bcrypt`. 28 | 29 | ``` 30 | pipenv install flask-bcrypt 31 | ``` 32 | 33 | Let' initialize flask-bcrypt in our `app.py`. 34 | 35 | ```diff 36 | #~/movie-bag/app.py 37 | 38 | from flask import Flask 39 | +from flask_bcrypt import Bcrypt 40 | from database.db import initialize_db 41 | from flask_restful import Api 42 | from resources.routes import initialize_routes 43 | 44 | app = Flask(__name__) 45 | api = Api(app) 46 | +bcrypt = Bcrypt(app) 47 | 48 | app.config['MONGODB_SETTINGS'] = { 49 | 'host': 'mongodb://localhost/movie-bag' 50 | 51 | 52 | ``` 53 | 54 | Now we are going to create two methods: one to create a password hash `generate_password_hash()` and the other to check if the password used by the user to login generates the hash which is equal to the password saved in the database `check_password_hash()`. 55 | 56 | Let's update our `models.py` to look like this: 57 | 58 | ```diff 59 | #~/movie-bag/database/models.py 60 | 61 | from .db import db 62 | +from flask_bcrypt import generate_password_hash, check_password_hash 63 | 64 | ... 65 | 66 | class User(db.Document): 67 | email = db.EmailField(required=True, unique=True) 68 | password = db.StringField(required=True, min_length=6) 69 | + 70 | + def hash_password(self): 71 | + self.password = generate_password_hash(self.password).decode('utf8') 72 | + 73 | + def check_password(self, password): 74 | + return check_password_hash(self.password, password) 75 | ``` 76 | 77 | 78 | Now let's create an API endpoint for `signup`. Add `auth.py` inside `resources` folder with the following code. 79 | 80 | ```python 81 | #~/movie-bag/resources/auth.py 82 | 83 | from flask import request 84 | from database.models import User 85 | from flask_restful import Resource 86 | 87 | class SignupApi(Resource): 88 | def post(self): 89 | body = request.get_json() 90 | user = User(**body) 91 | user.hash_password() 92 | user.save() 93 | id = user.id 94 | return {'id': str(id)}, 200 95 | ``` 96 | 97 | This endpoint creates a user document with `email` and `password` received from the `JSON` object sent by the user. 98 | 99 | Let's register this endpoint in our `routes.py`. 100 | ```diff 101 | from .movie import MoviesApi, MovieApi 102 | +from .auth import SignupApi 103 | 104 | def initialize_routes(api): 105 | api.add_resource(MoviesApi, '/api/movies') 106 | api.add_resource(MovieApi, '/api/movies/') 107 | + 108 | + api.add_resource(SignupApi, '/api/auth/signup') 109 | ``` 110 | Let's test user signup. Send `JSON` body with `email` and `password` to `http://localhost:5000/api/auth/signup` 111 | 112 | ![Postman Signup request](https://thepracticaldev.s3.amazonaws.com/i/4qmtg25bjyfrllwimgn8.png) 113 | 114 | If we take a look at our database, we can see that our password is hashed to some random password compared to the password we sent in the API request. 115 | 116 | ![Mongo Compass database entry](https://thepracticaldev.s3.amazonaws.com/i/fa212nj1c9hj7equre4y.png) 117 | 118 | *Note: To view the information stored in our database I used [mongo compass](https://www.mongodb.com/download-center/compass)* 119 | 120 | 121 | Alright, we have created the functionality of creating a user through `signup`, now we need to be able to `login` as that user. 122 | 123 | For logging users into a website, we need functionality to verify if the user is who they claim them to be. So, users can send `email` and `password` every time they need to do something on the website, which is not a good idea from a security viewpoint. So, we need functionality such that once the user is logged in into the website they can use their token to access other parts of the website. 124 | 125 | There are many methods for working with token-based authentication, In this part, we are going to learn about `JWT` also known as [JSON Web Token](https://jwt.io/introduction/). 126 | 127 | To use JWT, let's install another flask extension called [flask-jwt-extended](https://flask-jwt-extended.readthedocs.io/en/stable/basic_usage/) it uses a value we want to save as token (in our case it's `userid`) and combines that with the `salt` (secret key) to create a token. 128 | 129 | ``` 130 | pipenv instll flask-jwt-extended 131 | ``` 132 | 133 | Since the `secret-key` we use to create a JWT needs to be kept somewhere else from your codebase, we are going to use `.env` file to save the secret and give the location of `.env` file to our application using the environment variable. 134 | 135 | For that let's create a new file `.env` inside the `movie-bag` folder and add the following to it. 136 | 137 | ```env 138 | JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss' 139 | ``` 140 | 141 | The value of `JWT_SECRET_KEY` can be anything but make that something harder to guess. 142 | 143 | Let's update our `app.py` to use configs from `.env` file and initialize `JWT`. 144 | 145 | ```diff 146 | #~/movie-bag/app.py 147 | 148 | from flask import Flask 149 | from flask_bcrypt import Bcrypt 150 | +from flask_jwt_extended import JWTManager 151 | + 152 | from database.db import initialize_db 153 | from flask_restful import Api 154 | from resources.routes import initialize_routes 155 | 156 | app = Flask(__name__) 157 | +app.config.from_envvar('ENV_FILE_LOCATION') 158 | + 159 | api = Api(app) 160 | bcrypt = Bcrypt(app) 161 | +jwt = JWTManager(app) 162 | 163 | app.config['MONGODB_SETTINGS'] = { 164 | 'host': 'mongodb://localhost/movie-bag' 165 | ``` 166 | 167 | Here `ENV_FILE_LOCATION` is the environment variable which should store the location of `.env` file relative to `app.py` 168 | 169 | To set this value mac/linux can run the command: 170 | ``` 171 | export ENV_FILE_LOCATION=./.env 172 | ``` 173 | and windows user can run the command: 174 | ``` 175 | set ENV_FILE_LOCATION=./.env 176 | ``` 177 | 178 | Now, we are finally ready to implement the `login` endpoint. Let's update our `auth.py` inside the `resources` folder: 179 | 180 | ```diff 181 | 182 | -from flask import request 183 | +from flask import Response, request 184 | +from flask_jwt_extended import create_access_token 185 | from database.models import User 186 | from flask_restful import Resource 187 | +import datetime 188 | + 189 | class SignupApi(Resource): 190 | def post(self): 191 | body = request.get_json() 192 | @@ -9,4 +11,16 @@ class SignupApi(Resource): 193 | user.hash_password() 194 | user.save() 195 | id = user.id 196 | return {'id': str(id)}, 200 197 | + 198 | +class LoginApi(Resource): 199 | + def post(self): 200 | + body = request.get_json() 201 | + user = User.objects.get(email=body.get('email')) 202 | + authorized = user.check_password(body.get('password')) 203 | + if not authorized: 204 | + return {'error': 'Email or password invalid'}, 401 205 | + 206 | + expires = datetime.timedelta(days=7) 207 | + access_token = create_access_token(identity=str(user.id), expires_delta=expires) 208 | + return {'token': access_token}, 200 209 | 210 | ``` 211 | 212 | Here we search for the user with the given email and check if the password sent is the same as the hashed password saved in the database. 213 | If the password and email are correct we then create access token using `create_access_token()` which uses `user.id` as the identifier and the token expires in `7 days.` which means a user cannot access the website using this token after 7 days. 214 | 215 | Let's register this API endpoint in our `routes.py` 216 | ```diff 217 | from .movie import MoviesApi, MovieApi 218 | -from .auth import SignupApi 219 | +from .auth import SignupApi, LoginApi 220 | 221 | def initialize_routes(api): 222 | api.add_resource(MoviesApi, '/api/movies') 223 | api.add_resource(MovieApi, '/api/movies/') 224 | 225 | api.add_resource(SignupApi, '/api/auth/signup') 226 | + api.add_resource(LoginApi, '/api/auth/login') 227 | 228 | ``` 229 | 230 | Now, we need to restrict an unauthorized user from adding, editing and deleting the movies in our application. To do that, let's add `@jwt_required` decorator to our endpoints. This protects our endpoints form invalid or expired jwt. 231 | 232 | Update `movie.py` as: 233 | 234 | ```diff 235 | #~/movie-bag/resources/movie.py 236 | 237 | from flask import Response, request 238 | +from flask_jwt_extended import jwt_required 239 | from database.models import Movie 240 | from flask_restful import Resource 241 | 242 | class MoviesApi(Resource): 243 | ... 244 | + 245 | + @jwt_required 246 | def post(self): 247 | body = request.get_json() 248 | movie = Movie(**body).save() 249 | ... 250 | 251 | 252 | class MovieApi(Resource): 253 | + @jwt_required 254 | def put(self, id): 255 | body = request.get_json() 256 | Movie.objects.get(id=id).update(**body) 257 | return '', 200 258 | + 259 | + @jwt_required 260 | def delete(self, id): 261 | movie = Movie.objects.get(id=id).delete() 262 | return '', 200 263 | ``` 264 | 265 | Let's test this now. 266 | First of all, we have to login as the user we created earlier with `signup`. 267 | 268 | ![Postman login request](https://thepracticaldev.s3.amazonaws.com/i/tggxm49c1glytejkkv35.png) 269 | 270 | We got the token back from the server, now let's try to create a movie from the API endpoint `http://localhost:5000/api/movies`. As you can see you cannot do it and get an error, because it is protected by `jwt`.
271 | *Note: We will learn how to make error message friendly later in this series.* 272 | 273 | Now let's use the token we got earlier from `login` in our `Authorization` header. 274 | 275 | To use authorization header in Postman follow the steps: 276 | 1) Go to the `Authorization` tab. 277 | 2) Select the `Bearer Token` form `TYPE` dropdown. 278 | 3) Paste the token you got earlier from `/login` 279 | 4) Finally, send the request. 280 | 281 | ![Postman request with authorization header](https://thepracticaldev.s3.amazonaws.com/i/zxmx2filtj5vh8chh187.png) 282 | 283 |
284 | 285 | Let's add a feature such that only the user who created the movie can delete or edit the movie. 286 | 287 | Let's update our `models.py` and create a relation between the user and the movie. 288 | 289 | ```diff 290 | #~/movie-bag/database/models.py 291 | 292 | class Movie(db.Document): 293 | name = db.StringField(required=True, unique=True) 294 | casts = db.ListField(db.StringField(), required=True) 295 | genres = db.ListField(db.StringField(), required=True) 296 | + added_by = db.ReferenceField('User') 297 | 298 | class User(db.Document): 299 | email = db.EmailField(required=True, unique=True) 300 | password = db.StringField(required=True, min_length=6) 301 | + movies = db.ListField(db.ReferenceField('Movie', reverse_delete_rule=db.PULL)) 302 | 303 | 304 | 305 | + 306 | +User.register_delete_rule(Movie, 'added_by', db.CASCADE) 307 | ``` 308 | 309 | We have created a one-many relationship between `user` and `movie`. That means a user can have one or more movies and a movie can only be created by one user. Here `reverse_delete_rule` in the movies field of `User` represents that a movie should be pulled from the user document if the movie is deleted. 310 | Similarly, `User.register_delete_rule(Movie, 'added_by', db.CASCADE)` creates another delete rule which means if a user is deleted then the movie created by the user is also deleted.
311 | *Note: I had to register delete rule for `added_by` separately because `User` is not yet defined while defining `Movie`* 312 | 313 | 314 | Now, let's update `movie.py` to apply the authorization. 315 | 316 | ```diff 317 | 318 | from flask import Response, request 319 | -from flask_jwt_extended import jwt_required 320 | -from database.models import Movie 321 | +from database.models import Movie, User 322 | +from flask_jwt_extended import jwt_required, get_jwt_identity 323 | from flask_restful import Resource 324 | 325 | class MoviesApi(Resource): 326 | def get(self): 327 | movies = Movie.objects().to_json() 328 | return Response(movies, mimetype="application/json", status=200) 329 | 330 | @jwt_required 331 | def post(self): 332 | + user_id = get_jwt_identity() 333 | body = request.get_json() 334 | - movie = Movie(**body).save() 335 | + user = User.objects.get(id=user_id) 336 | + movie = Movie(**body, added_by=user) 337 | + movie.save() 338 | + user.update(push__movies=movie) 339 | + user.save() 340 | id = movie.id 341 | return {'id': str(id)}, 200 342 | 343 | class MovieApi(Resource): 344 | @jwt_required 345 | def put(self, id): 346 | + user_id = get_jwt_identity() 347 | + movie = Movie.objects.get(id=id, added_by=user_id) 348 | body = request.get_json() 349 | Movie.objects.get(id=id).update(**body) 350 | return '', 200 351 | 352 | @jwt_required 353 | def delete(self, id): 354 | - movie = Movie.objects.get(id=id).delete() 355 | + user_id = get_jwt_identity() 356 | + movie = Movie.objects.get(id=id, added_by=user_id) 357 | + movie.delete() 358 | return '', 200 359 | 360 | def get(self, id): 361 | ... 362 | ``` 363 | 364 | Here `get_jwt_identity()` method returns the value encoded by `create_access_token()` which in our case is `user.id`. So, we only delete/update the movie which is added_by the user who is sending the request to the application. 365 | 366 |
367 | 368 | You can find the complete code of this part [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%203) 369 | 370 | ### What we learned from this part of the series? 371 | - How to hash user password using `flask-bcrypt` 372 | - How to create JSON token using `flask-jwt-extended` 373 | - How to protect API endpoints from unauthorized access. 374 | - How to implement authorization so that only the user who added the movie can delete/update the movie. 375 | 376 |
377 | 378 | Since there are a lot of unfriendly errors and exceptions in our application, in the next part we are going to learn how to handle errors and exceptions in our REST API. 379 | 380 |
381 | Please let me know if you are stuck at any point so that I can guide you. Also, if there is something you want me to cover in the next parts/series don't forget to mention that below. 382 |
383 | 384 | 385 | Until then happy coding 😊 -------------------------------------------------------------------------------- /Part - 3/movie-bag/.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss' -------------------------------------------------------------------------------- /Part - 3/movie-bag/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "*" 10 | flask-mongoengine = "*" 11 | flask-restful = "*" 12 | flask-bcrypt = "*" 13 | flask-jwt-extended = "*" 14 | 15 | [requires] 16 | python_version = "3.7" 17 | -------------------------------------------------------------------------------- /Part - 3/movie-bag/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ee3b79fd36b201c844fb10781f425211e9185d6eea21f7f9879bba757b337661" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aniso8601": { 20 | "hashes": [ 21 | "sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072", 22 | "sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a" 23 | ], 24 | "version": "==8.0.0" 25 | }, 26 | "bcrypt": { 27 | "hashes": [ 28 | "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", 29 | "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", 30 | "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", 31 | "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", 32 | "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", 33 | "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", 34 | "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", 35 | "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", 36 | "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", 37 | "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", 38 | "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", 39 | "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", 40 | "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", 41 | "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", 42 | "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", 43 | "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", 44 | "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", 45 | "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" 46 | ], 47 | "version": "==3.1.7" 48 | }, 49 | "cffi": { 50 | "hashes": [ 51 | "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", 52 | "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", 53 | "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", 54 | "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", 55 | "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", 56 | "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", 57 | "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", 58 | "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", 59 | "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", 60 | "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", 61 | "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", 62 | "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", 63 | "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", 64 | "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", 65 | "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", 66 | "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", 67 | "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", 68 | "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", 69 | "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", 70 | "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", 71 | "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", 72 | "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", 73 | "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", 74 | "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", 75 | "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", 76 | "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", 77 | "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", 78 | "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", 79 | "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", 80 | "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", 81 | "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", 82 | "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", 83 | "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d" 84 | ], 85 | "version": "==1.13.2" 86 | }, 87 | "click": { 88 | "hashes": [ 89 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 90 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 91 | ], 92 | "version": "==7.0" 93 | }, 94 | "flask": { 95 | "hashes": [ 96 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 97 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 98 | ], 99 | "index": "pypi", 100 | "version": "==1.1.1" 101 | }, 102 | "flask-bcrypt": { 103 | "hashes": [ 104 | "sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f" 105 | ], 106 | "index": "pypi", 107 | "version": "==0.7.1" 108 | }, 109 | "flask-jwt-extended": { 110 | "hashes": [ 111 | "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd" 112 | ], 113 | "index": "pypi", 114 | "version": "==3.24.1" 115 | }, 116 | "flask-mongoengine": { 117 | "hashes": [ 118 | "sha256:0f426aeafc4be2c37e9b4c0f8b5d02d012b7afc4b3b97a4119024684fe148fc1", 119 | "sha256:b6376b33cd1e624c09983a0884dc87303e54084f0e6b7f8df6794d56d35fa66f" 120 | ], 121 | "index": "pypi", 122 | "version": "==0.9.5" 123 | }, 124 | "flask-restful": { 125 | "hashes": [ 126 | "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", 127 | "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" 128 | ], 129 | "index": "pypi", 130 | "version": "==0.3.7" 131 | }, 132 | "flask-wtf": { 133 | "hashes": [ 134 | "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", 135 | "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" 136 | ], 137 | "version": "==0.14.2" 138 | }, 139 | "itsdangerous": { 140 | "hashes": [ 141 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 142 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 143 | ], 144 | "version": "==1.1.0" 145 | }, 146 | "jinja2": { 147 | "hashes": [ 148 | "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", 149 | "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" 150 | ], 151 | "version": "==2.10.3" 152 | }, 153 | "markupsafe": { 154 | "hashes": [ 155 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 156 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 157 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 158 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 159 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 160 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 161 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 162 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 163 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 164 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 165 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 166 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 167 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 168 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 169 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 170 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 171 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 172 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 173 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 174 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 175 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 176 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 177 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 178 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 179 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 180 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 181 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 182 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 183 | ], 184 | "version": "==1.1.1" 185 | }, 186 | "mongoengine": { 187 | "hashes": [ 188 | "sha256:9301ca84ada9377a200a50541f9be7d5308081bf2112049d00e1dd163f80b940", 189 | "sha256:fa3e73c966fca2b814cc1103ac4f55bcca7aae05028b112ef0cc8b321ee4a2f7" 190 | ], 191 | "version": "==0.18.2" 192 | }, 193 | "pycparser": { 194 | "hashes": [ 195 | "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" 196 | ], 197 | "version": "==2.19" 198 | }, 199 | "pyjwt": { 200 | "hashes": [ 201 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 202 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 203 | ], 204 | "version": "==1.7.1" 205 | }, 206 | "pymongo": { 207 | "hashes": [ 208 | "sha256:0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", 209 | "sha256:08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", 210 | "sha256:0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", 211 | "sha256:0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", 212 | "sha256:1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", 213 | "sha256:22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", 214 | "sha256:264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", 215 | "sha256:3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", 216 | "sha256:339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", 217 | "sha256:38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", 218 | "sha256:4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", 219 | "sha256:4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", 220 | "sha256:4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", 221 | "sha256:4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", 222 | "sha256:4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", 223 | "sha256:53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", 224 | "sha256:681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", 225 | "sha256:6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", 226 | "sha256:72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", 227 | "sha256:7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", 228 | "sha256:72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", 229 | "sha256:87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", 230 | "sha256:87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", 231 | "sha256:88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", 232 | "sha256:89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", 233 | "sha256:908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", 234 | "sha256:9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", 235 | "sha256:9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", 236 | "sha256:98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", 237 | "sha256:99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", 238 | "sha256:9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", 239 | "sha256:a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", 240 | "sha256:a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", 241 | "sha256:a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", 242 | "sha256:b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", 243 | "sha256:bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", 244 | "sha256:c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", 245 | "sha256:c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", 246 | "sha256:c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", 247 | "sha256:c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", 248 | "sha256:ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", 249 | "sha256:d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", 250 | "sha256:d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", 251 | "sha256:d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", 252 | "sha256:d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", 253 | "sha256:dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", 254 | "sha256:e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", 255 | "sha256:e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", 256 | "sha256:e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", 257 | "sha256:e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", 258 | "sha256:ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", 259 | "sha256:fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", 260 | "sha256:fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1" 261 | ], 262 | "version": "==3.10.0" 263 | }, 264 | "pytz": { 265 | "hashes": [ 266 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 267 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 268 | ], 269 | "version": "==2019.3" 270 | }, 271 | "six": { 272 | "hashes": [ 273 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 274 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 275 | ], 276 | "version": "==1.13.0" 277 | }, 278 | "werkzeug": { 279 | "hashes": [ 280 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 281 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 282 | ], 283 | "version": "==0.16.0" 284 | }, 285 | "wtforms": { 286 | "hashes": [ 287 | "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", 288 | "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" 289 | ], 290 | "version": "==2.2.1" 291 | } 292 | }, 293 | "develop": {} 294 | } 295 | -------------------------------------------------------------------------------- /Part - 3/movie-bag/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_bcrypt import Bcrypt 3 | from flask_jwt_extended import JWTManager 4 | 5 | from database.db import initialize_db 6 | from flask_restful import Api 7 | from resources.routes import initialize_routes 8 | 9 | app = Flask(__name__) 10 | app.config.from_envvar('ENV_FILE_LOCATION') 11 | 12 | api = Api(app) 13 | bcrypt = Bcrypt(app) 14 | jwt = JWTManager(app) 15 | 16 | app.config['MONGODB_SETTINGS'] = { 17 | 'host': 'mongodb://localhost/movie-bag' 18 | } 19 | 20 | initialize_db(app) 21 | initialize_routes(api) 22 | 23 | app.run() -------------------------------------------------------------------------------- /Part - 3/movie-bag/database/db.py: -------------------------------------------------------------------------------- 1 | from flask_mongoengine import MongoEngine 2 | 3 | db = MongoEngine() 4 | 5 | def initialize_db(app): 6 | db.init_app(app) -------------------------------------------------------------------------------- /Part - 3/movie-bag/database/models.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from flask_bcrypt import generate_password_hash, check_password_hash 3 | 4 | class Movie(db.Document): 5 | name = db.StringField(required=True, unique=True) 6 | casts = db.ListField(db.StringField(), required=True) 7 | genres = db.ListField(db.StringField(), required=True) 8 | added_by = db.ReferenceField('User') 9 | 10 | class User(db.Document): 11 | email = db.EmailField(required=True, unique=True) 12 | password = db.StringField(required=True, min_length=6) 13 | movies = db.ListField(db.ReferenceField('Movie', reverse_delete_rule=db.PULL)) 14 | 15 | def hash_password(self): 16 | self.password = generate_password_hash(self.password).decode('utf8') 17 | 18 | def check_password(self, password): 19 | return check_password_hash(self.password, password) 20 | 21 | User.register_delete_rule(Movie, 'added_by', db.CASCADE) -------------------------------------------------------------------------------- /Part - 3/movie-bag/resources/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from flask_jwt_extended import create_access_token 3 | from database.models import User 4 | from flask_restful import Resource 5 | import datetime 6 | 7 | class SignupApi(Resource): 8 | def post(self): 9 | body = request.get_json() 10 | user = User(**body) 11 | user.hash_password() 12 | user.save() 13 | id = user.id 14 | return {'id': str(id)}, 200 15 | 16 | class LoginApi(Resource): 17 | def post(self): 18 | body = request.get_json() 19 | user = User.objects.get(email=body.get('email')) 20 | authorized = user.check_password(body.get('password')) 21 | if not authorized: 22 | return {'error': 'Email or password invalid'}, 401 23 | 24 | expires = datetime.timedelta(days=7) 25 | access_token = create_access_token(identity=str(user.id), expires_delta=expires) 26 | return {'token': access_token}, 200 -------------------------------------------------------------------------------- /Part - 3/movie-bag/resources/movie.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from database.models import Movie, User 3 | from flask_jwt_extended import jwt_required, get_jwt_identity 4 | from flask_restful import Resource 5 | 6 | class MoviesApi(Resource): 7 | def get(self): 8 | query = Movie.objects() 9 | movies = Movie.objects().to_json() 10 | return Response(movies, mimetype="application/json", status=200) 11 | 12 | @jwt_required 13 | def post(self): 14 | user_id = get_jwt_identity() 15 | body = request.get_json() 16 | user = User.objects.get(id=user_id) 17 | movie = Movie(**body, added_by=user) 18 | movie.save() 19 | user.update(push__movies=movie) 20 | user.save() 21 | id = movie.id 22 | return {'id': str(id)}, 200 23 | 24 | class MovieApi(Resource): 25 | @jwt_required 26 | def put(self, id): 27 | user_id = get_jwt_identity() 28 | movie = Movie.objects.get(id=id, added_by=user_id) 29 | body = request.get_json() 30 | Movie.objects.get(id=id).update(**body) 31 | return '', 200 32 | 33 | @jwt_required 34 | def delete(self, id): 35 | user_id = get_jwt_identity() 36 | movie = Movie.objects.get(id=id, added_by=user_id) 37 | movie.delete() 38 | return '', 200 39 | 40 | def get(self, id): 41 | movies = Movie.objects.get(id=id).to_json() 42 | return Response(movies, mimetype="application/json", status=200) 43 | -------------------------------------------------------------------------------- /Part - 3/movie-bag/resources/routes.py: -------------------------------------------------------------------------------- 1 | from .movie import MoviesApi, MovieApi 2 | from .auth import SignupApi, LoginApi 3 | 4 | def initialize_routes(api): 5 | api.add_resource(MoviesApi, '/api/movies') 6 | api.add_resource(MovieApi, '/api/movies/') 7 | 8 | api.add_resource(SignupApi, '/api/auth/signup') 9 | api.add_resource(LoginApi, '/api/auth/login') 10 | -------------------------------------------------------------------------------- /Part - 4/Part-4 Exception Handling.md: -------------------------------------------------------------------------------- 1 | ## Part 4: Exception Handling 2 | 3 | Howdy! In the previous [Part](https://dev.to/paurakhsharma/flask-rest-api-part-3-authentication-and-authorization-5935) of the series, we learned how we can 4 | add `authentication` and `authorization`. In this part, we are going to learn how we can add make 5 | our flask application more resilient to errors, and how to send a proper error message to the client. 6 | 7 | When something goes wrong in a computer program, the program throws a specific `Exception` giving some hint to the user what went wrong. In our application when something goes wrong e.g user tries to create another account with the already used email address, they get an `Internal Server Error` and the client has no idea what they did wrong. So, to solve such issues we are going to use `Exception Handling` to catch such exceptions and send a proper error message to the client indicating what went wrong. 8 | 9 | We are going to use a really useful feature of `flask-restful` which lets us define [Custom Error Messages](https://flask-restful.readthedocs.io/en/0.3.5/extending.html#custom-error-handlers). 10 | 11 | Let's create a new file `errors.py` inside the `resources` folder and add the following code: 12 | 13 | ```bash 14 | cd resources 15 | touch errors.py 16 | ``` 17 | 18 | ```python 19 | #~/movie-bag/resources/errors.py 20 | 21 | class InternalServerError(Exception): 22 | pass 23 | 24 | class SchemaValidationError(Exception): 25 | pass 26 | 27 | class MovieAlreadyExistsError(Exception): 28 | pass 29 | 30 | class UpdatingMovieError(Exception): 31 | pass 32 | 33 | class DeletingMovieError(Exception): 34 | pass 35 | 36 | class MovieNotExistsError(Exception): 37 | pass 38 | 39 | class EmailAlreadyExistsError(Exception): 40 | pass 41 | 42 | class UnauthorizedError(Exception): 43 | pass 44 | 45 | errors = { 46 | "InternalServerError": { 47 | "message": "Something went wrong", 48 | "status": 500 49 | }, 50 | "SchemaValidationError": { 51 | "message": "Request is missing required fields", 52 | "status": 400 53 | }, 54 | "MovieAlreadyExistsError": { 55 | "message": "Movie with given name already exists", 56 | "status": 400 57 | }, 58 | "UpdatingMovieError": { 59 | "message": "Updating movie added by other is forbidden", 60 | "status": 403 61 | }, 62 | "DeletingMovieError": { 63 | "message": "Deleting movie added by other is forbidden", 64 | "status": 403 65 | }, 66 | "MovieNotExistsError": { 67 | "message": "Movie with given id doesn't exists", 68 | "status": 400 69 | }, 70 | "EmailAlreadyExistsError": { 71 | "message": "User with given email address already exists", 72 | "status": 400 73 | }, 74 | "UnauthorizedError": { 75 | "message": "Invalid username or password", 76 | "status": 401 77 | } 78 | } 79 | ``` 80 | 81 | As you can see firs we have extended the `Exception` class to create different custom exceptions and then we created an `errors` dictionary, which contains the error message and status codes for each exception. Now, we need to add these errors to the `flask-restful` `Api` class. 82 | 83 | Update `app.py` to import recently created `errors` dictionary and add this as a parameter to `Api` class. 84 | 85 | ```diff 86 | #~/movie-bag/app.py 87 | 88 | from database.db import initialize_db 89 | from flask_restful import Api 90 | from resources.routes import initialize_routes 91 | +from resources.errors import errors 92 | 93 | app = Flask(__name__) 94 | app.config.from_envvar('ENV_FILE_LOCATION') 95 | 96 | -api = Api(app) 97 | +api = Api(app, errors=errors) 98 | bcrypt = Bcrypt(app) 99 | jwt = JWTManager(app) 100 | 101 | ``` 102 | 103 | Finally, we are ready to perform some exception handling in our application. Update `movie.py` view functions according to the following: 104 | 105 | ```diff 106 | #~/movie-bag/resources/movie.py 107 | from flask import Response, request 108 | from database.models import Movie, User 109 | from flask_jwt_extended import jwt_required, get_jwt_identity 110 | from flask_restful import Resource 111 | + 112 | +from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist, ValidationError, InvalidQueryError 113 | 114 | +from resources.errors import SchemaValidationError, MovieAlreadyExistsError, InternalServerError, \ 115 | +UpdatingMovieError, DeletingMovieError, MovieNotExistsError 116 | + 117 | 118 | class MoviesApi(Resource): 119 | def get(self): 120 | @@ -11,32 +15,57 @@ class MoviesApi(Resource): 121 | 122 | @jwt_required 123 | def post(self): 124 | - user_id = get_jwt_identity() 125 | - body = request.get_json() 126 | - user = User.objects.get(id=user_id) 127 | - movie = Movie(**body, added_by=user) 128 | - movie.save() 129 | - user.update(push__movies=movie) 130 | - user.save() 131 | - id = movie.id 132 | - return {'id': str(id)}, 200 133 | - 134 | + try: 135 | + user_id = get_jwt_identity() 136 | + body = request.get_json() 137 | + user = User.objects.get(id=user_id) 138 | + movie = Movie(**body, added_by=user) 139 | + movie.save() 140 | + user.update(push__movies=movie) 141 | + user.save() 142 | + id = movie.id 143 | + return {'id': str(id)}, 200 144 | + except (FieldDoesNotExist, ValidationError): 145 | + raise SchemaValidationError 146 | + except NotUniqueError: 147 | + raise MovieAlreadyExistsError 148 | + except Exception as e: 149 | + raise InternalServerError 150 | + 151 | + 152 | class MovieApi(Resource): 153 | @jwt_required 154 | def put(self, id): 155 | - user_id = get_jwt_identity() 156 | - movie = Movie.objects.get(id=id, added_by=user_id) 157 | - body = request.get_json() 158 | - Movie.objects.get(id=id).update(**body) 159 | - return '', 200 160 | + try: 161 | + user_id = get_jwt_identity() 162 | + movie = Movie.objects.get(id=id, added_by=user_id) 163 | + body = request.get_json() 164 | + Movie.objects.get(id=id).update(**body) 165 | + return '', 200 166 | + except InvalidQueryError: 167 | + raise SchemaValidationError 168 | + except DoesNotExist: 169 | + raise UpdatingMovieError 170 | + except Exception: 171 | + raise InternalServerError 172 | 173 | @jwt_required 174 | def delete(self, id): 175 | - user_id = get_jwt_identity() 176 | - movie = Movie.objects.get(id=id, added_by=user_id) 177 | - movie.delete() 178 | - return '', 200 179 | + try: 180 | + user_id = get_jwt_identity() 181 | + movie = Movie.objects.get(id=id, added_by=user_id) 182 | + movie.delete() 183 | + return '', 200 184 | + except DoesNotExist: 185 | + raise DeletingMovieError 186 | + except Exception: 187 | + raise InternalServerError 188 | 189 | def get(self, id): 190 | - movies = Movie.objects.get(id=id).to_json() 191 | - return Response(movies, mimetype="application/json", status=200) 192 | + try: 193 | + movies = Movie.objects.get(id=id).to_json() 194 | + return Response(movies, mimetype="application/json", status=200) 195 | + except DoesNotExist: 196 | + raise MovieNotExistsError 197 | + except Exception: 198 | + raise InternalServerError 199 | 200 | 201 | ``` 202 | 203 | Let's see the example of `post` method in `MoviesApi` class: 204 | 205 | ```python 206 | def post(self): 207 | try: 208 | user_id = get_jwt_identity() 209 | body = request.get_json() 210 | user = User.objects.get(id=user_id) 211 | movie = Movie(**body, added_by=user) 212 | movie.save() 213 | user.update(push__movies=movie) 214 | user.save() 215 | id = movie.id 216 | return {'id': str(id)}, 200 217 | except (FieldDoesNotExist, ValidationError): 218 | raise SchemaValidationError 219 | except NotUniqueError: 220 | raise MovieAlreadyExistsError 221 | except Exception as e: 222 | raise InternalServerError 223 | ``` 224 | Here you can see we have wrapped the whole view opetations in `try...except` block. We have performed an exception chaining so, that when we get some excetion we throw the exceptions which we have defined in `errors.py` and `flask-restful` generates a response based on the values we defined in `errors` dictionary. 225 | 226 | When there is `FieldDoesNotExist` exception or `ValidationError` from `mongoengine` we raise `SchemaValidationError` exception which tells the client that their request JSON is invalid. Similarly, when a user tries to a movie with the name which already exists `mongoengine` throws `NotUniqueError` exception and by catching that exception we raise `MovieAlreadyExistsError` which tells the user that the movie name already exists. 227 | 228 | And lastly, we get an exception that we have not expected then we throw an `InternalServerError`. 229 | 230 | Let's add similar exception handling to our `auth.py` 231 | 232 | ```diff 233 | #~/movie-bag/resources/auth.py 234 | 235 | from database.models import User 236 | from flask_restful import Resource 237 | import datetime 238 | +from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist 239 | +from resources.errors import SchemaValidationError, EmailAlreadyExistsError, UnauthorizedError, \ 240 | +InternalServerError 241 | 242 | class SignupApi(Resource): 243 | def post(self): 244 | - body = request.get_json() 245 | - user = User(**body) 246 | - user.hash_password() 247 | - user.save() 248 | - id = user.id 249 | - return {'id': str(id)}, 200 250 | + try: 251 | + body = request.get_json() 252 | + user = User(**body) 253 | + user.hash_password() 254 | + user.save() 255 | + id = user.id 256 | + return {'id': str(id)}, 200 257 | + except FieldDoesNotExist: 258 | + raise SchemaValidationError 259 | + except NotUniqueError: 260 | + raise EmailAlreadyExistsError 261 | + except Exception as e: 262 | + raise InternalServerError 263 | 264 | class LoginApi(Resource): 265 | def post(self): 266 | - body = request.get_json() 267 | - user = User.objects.get(email=body.get('email')) 268 | - authorized = user.check_password(body.get('password')) 269 | - if not authorized: 270 | - return {'error': 'Email or password invalid'}, 401 271 | - expires = datetime.timedelta(days=7) 272 | - access_token = create_access_token(identity=str(user.id), expires_delta=expires) 273 | - return {'token': access_token}, 200 274 | + try: 275 | + body = request.get_json() 276 | + user = User.objects.get(email=body.get('email')) 277 | + authorized = user.check_password(body.get('password')) 278 | + if not authorized: 279 | + raise UnauthorizedError 280 | + 281 | + expires = datetime.timedelta(days=7) 282 | + access_token = create_access_token(identity=str(user.id), expires_delta=expires) 283 | + return {'token': access_token}, 200 284 | + except (UnauthorizedError, DoesNotExist): 285 | + raise UnauthorizedError 286 | + except Exception as e: 287 | + raise InternalServerError 288 | ``` 289 | 290 | 291 | That's it, people. Now, when there is an error in our application we get a proper error message with relevant status code. 292 | 293 | Let's try creating a user with `/api/auth/signup` with some email address let's say `testemail@gmail.com`. Now again try to create another user with the same email address. We get a response like this: 294 | 295 | ```json 296 | { 297 | "message": "User with given email address already exists", 298 | "status": 400 299 | } 300 | ``` 301 | Now, the user of our application can easily know what went wrong. 302 | 303 | You can find the complete code of this part [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%204) 304 | 305 | ### What we learned from this part of the series? 306 | - How to handle exceptions in our flask application using exception chaining. 307 | - How to send the error message and status code based on the exception occurred. 308 | 309 |
310 | In the next part of the series, we are going to learn how to perform a password reset in our application. 311 |
312 | 313 | Until then happy coding 😊 -------------------------------------------------------------------------------- /Part - 4/master.patch: -------------------------------------------------------------------------------- 1 | diff --git a/Part - 4/movie-bag/master.patch b/Part - 4/movie-bag/master.patch 2 | deleted file mode 100644 3 | index fa2b7aa..0000000 4 | --- a/Part - 4/movie-bag/master.patch 5 | +++ /dev/null 6 | @@ -1,149 +0,0 @@ 7 | -diff --git a/Part - 4/Part-4 Exception Handling.md b/Part - 4/Part-4 Exception Handling.md 8 | -index 90af64f..c1a1c15 100644 9 | ---- a/Part - 4/Part-4 Exception Handling.md 10 | -+++ b/Part - 4/Part-4 Exception Handling.md 11 | -@@ -82,10 +82,27 @@ As you can see firs we have extended the `Exception` class to create different c 12 | - Update `app.py` to import recently created `errors` dictionary and add this as a parameter to `Api` class. 13 | - 14 | - ```diff 15 | --#~/movie-bag/app.y 16 | -+#~/movie-bag/app.py 17 | -+ 18 | -+from database.db import initialize_db 19 | -+ from flask_restful import Api 20 | -+ from resources.routes import initialize_routes 21 | -++from resources.errors import errors 22 | -+ 23 | -+ app = Flask(__name__) 24 | -+ app.config.from_envvar('ENV_FILE_LOCATION') 25 | -+ 26 | -+-api = Api(app) 27 | -++api = Api(app, errors=errors) 28 | -+ bcrypt = Bcrypt(app) 29 | -+ jwt = JWTManager(app) 30 | -+ 31 | -+``` 32 | -+ 33 | -+Finally, we are ready to perform some exception handling in our application. 34 | -+ 35 | - 36 | - 37 | - 38 | --``` 39 | - 40 | - Until then happy coding 😊 41 | -\ No newline at end of file 42 | -diff --git a/Part - 4/movie-bag/app.py b/Part - 4/movie-bag/app.py 43 | -index 04fa601..8281ddd 100644 44 | ---- a/Part - 4/movie-bag/app.py 45 | -+++ b/Part - 4/movie-bag/app.py 46 | -@@ -5,11 +5,12 @@ from flask_jwt_extended import JWTManager 47 | - from database.db import initialize_db 48 | - from flask_restful import Api 49 | - from resources.routes import initialize_routes 50 | -+from resources.errors import errors 51 | - 52 | - app = Flask(__name__) 53 | - app.config.from_envvar('ENV_FILE_LOCATION') 54 | - 55 | --api = Api(app) 56 | -+api = Api(app, errors=errors) 57 | - bcrypt = Bcrypt(app) 58 | - jwt = JWTManager(app) 59 | - 60 | -diff --git a/Part - 4/movie-bag/resources/movie.py b/Part - 4/movie-bag/resources/movie.py 61 | -index d0ecec9..a63a299 100644 62 | ---- a/Part - 4/movie-bag/resources/movie.py 63 | -+++ b/Part - 4/movie-bag/resources/movie.py 64 | -@@ -1,7 +1,11 @@ 65 | - from flask import Response, request 66 | - from database.models import Movie, User 67 | - from flask_jwt_extended import jwt_required, get_jwt_identity 68 | --from flask_restful import Resource 69 | -+from flask_restful import Resource, reqparse 70 | -+from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist, ValidationError, InvalidQueryError 71 | -+from resources.errors import SchemaValidationError, MovieAlreadyExistsError, InternalServerError, \ 72 | -+UpdatingMovieError, DeletingMovieError, MovieNotExistsError 73 | -+ 74 | - 75 | - class MoviesApi(Resource): 76 | - def get(self): 77 | -@@ -11,32 +15,57 @@ class MoviesApi(Resource): 78 | - 79 | - @jwt_required 80 | - def post(self): 81 | -- user_id = get_jwt_identity() 82 | -- body = request.get_json() 83 | -- user = User.objects.get(id=user_id) 84 | -- movie = Movie(**body, added_by=user) 85 | -- movie.save() 86 | -- user.update(push__movies=movie) 87 | -- user.save() 88 | -- id = movie.id 89 | -- return {'id': str(id)}, 200 90 | -- 91 | -+ try: 92 | -+ user_id = get_jwt_identity() 93 | -+ body = request.get_json() 94 | -+ user = User.objects.get(id=user_id) 95 | -+ movie = Movie(**body, added_by=user) 96 | -+ movie.save() 97 | -+ user.update(push__movies=movie) 98 | -+ user.save() 99 | -+ id = movie.id 100 | -+ return {'id': str(id)}, 200 101 | -+ except (FieldDoesNotExist, ValidationError): 102 | -+ raise SchemaValidationError 103 | -+ except NotUniqueError: 104 | -+ raise MovieAlreadyExistsError 105 | -+ except Exception as e: 106 | -+ raise InternalServerError 107 | -+ 108 | -+ 109 | - class MovieApi(Resource): 110 | - @jwt_required 111 | - def put(self, id): 112 | -- user_id = get_jwt_identity() 113 | -- movie = Movie.objects.get(id=id, added_by=user_id) 114 | -- body = request.get_json() 115 | -- Movie.objects.get(id=id).update(**body) 116 | -- return '', 200 117 | -+ try: 118 | -+ user_id = get_jwt_identity() 119 | -+ movie = Movie.objects.get(id=id, added_by=user_id) 120 | -+ body = request.get_json() 121 | -+ Movie.objects.get(id=id).update(**body) 122 | -+ return '', 200 123 | -+ except InvalidQueryError: 124 | -+ raise SchemaValidationError 125 | -+ except DoesNotExist: 126 | -+ raise UpdatingMovieError 127 | -+ except Exception: 128 | -+ raise InternalServerError 129 | - 130 | - @jwt_required 131 | - def delete(self, id): 132 | -- user_id = get_jwt_identity() 133 | -- movie = Movie.objects.get(id=id, added_by=user_id) 134 | -- movie.delete() 135 | -- return '', 200 136 | -+ try: 137 | -+ user_id = get_jwt_identity() 138 | -+ movie = Movie.objects.get(id=id, added_by=user_id) 139 | -+ movie.delete() 140 | -+ return '', 200 141 | -+ except DoesNotExist: 142 | -+ raise DeletingMovieError 143 | -+ except Exception: 144 | -+ raise InternalServerError 145 | - 146 | - def get(self, id): 147 | -- movies = Movie.objects.get(id=id).to_json() 148 | -- return Response(movies, mimetype="application/json", status=200) 149 | -+ try: 150 | -+ movies = Movie.objects.get(id=id).to_json() 151 | -+ return Response(movies, mimetype="application/json", status=200) 152 | -+ except DoesNotExist: 153 | -+ raise MovieNotExistsError 154 | -+ except Exception: 155 | -+ raise InternalServerError 156 | diff --git a/Part - 4/movie-bag/resources/auth.py b/Part - 4/movie-bag/resources/auth.py 157 | index 128f9ab..99d09c8 100644 158 | --- a/Part - 4/movie-bag/resources/auth.py 159 | +++ b/Part - 4/movie-bag/resources/auth.py 160 | @@ -3,24 +3,39 @@ from flask_jwt_extended import create_access_token 161 | from database.models import User 162 | from flask_restful import Resource 163 | import datetime 164 | +from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist 165 | +from resources.errors import SchemaValidationError, EmailAlreadyExistsError, UnauthorizedError, \ 166 | +InternalServerError 167 | 168 | class SignupApi(Resource): 169 | def post(self): 170 | - body = request.get_json() 171 | - user = User(**body) 172 | - user.hash_password() 173 | - user.save() 174 | - id = user.id 175 | - return {'id': str(id)}, 200 176 | + try: 177 | + body = request.get_json() 178 | + user = User(**body) 179 | + user.hash_password() 180 | + user.save() 181 | + id = user.id 182 | + return {'id': str(id)}, 200 183 | + except FieldDoesNotExist: 184 | + raise SchemaValidationError 185 | + except NotUniqueError: 186 | + raise EmailAlreadyExistsError 187 | + except Exception as e: 188 | + raise InternalServerError 189 | 190 | class LoginApi(Resource): 191 | def post(self): 192 | - body = request.get_json() 193 | - user = User.objects.get(email=body.get('email')) 194 | - authorized = user.check_password(body.get('password')) 195 | - if not authorized: 196 | - return {'error': 'Email or password invalid'}, 401 197 | + try: 198 | + body = request.get_json() 199 | + user = User.objects.get(email=body.get('email')) 200 | + authorized = user.check_password(body.get('password')) 201 | + if not authorized: 202 | + raise UnauthorizedError 203 | 204 | - expires = datetime.timedelta(days=7) 205 | - access_token = create_access_token(identity=str(user.id), expires_delta=expires) 206 | - return {'token': access_token}, 200 207 | \ No newline at end of file 208 | + expires = datetime.timedelta(days=7) 209 | + access_token = create_access_token(identity=str(user.id), expires_delta=expires) 210 | + return {'token': access_token}, 200 211 | + except (UnauthorizedError, DoesNotExist): 212 | + raise UnauthorizedError 213 | + except Exception as e: 214 | + raise InternalServerError 215 | \ No newline at end of file 216 | -------------------------------------------------------------------------------- /Part - 4/movie-bag/.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss' -------------------------------------------------------------------------------- /Part - 4/movie-bag/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "*" 10 | flask-mongoengine = "*" 11 | flask-restful = "*" 12 | flask-bcrypt = "*" 13 | flask-jwt-extended = "*" 14 | 15 | [requires] 16 | python_version = "3.7" 17 | -------------------------------------------------------------------------------- /Part - 4/movie-bag/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ee3b79fd36b201c844fb10781f425211e9185d6eea21f7f9879bba757b337661" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aniso8601": { 20 | "hashes": [ 21 | "sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072", 22 | "sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a" 23 | ], 24 | "version": "==8.0.0" 25 | }, 26 | "bcrypt": { 27 | "hashes": [ 28 | "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", 29 | "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", 30 | "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", 31 | "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", 32 | "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", 33 | "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", 34 | "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", 35 | "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", 36 | "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", 37 | "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", 38 | "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", 39 | "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", 40 | "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", 41 | "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", 42 | "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", 43 | "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", 44 | "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", 45 | "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" 46 | ], 47 | "version": "==3.1.7" 48 | }, 49 | "cffi": { 50 | "hashes": [ 51 | "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", 52 | "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", 53 | "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", 54 | "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", 55 | "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", 56 | "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", 57 | "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", 58 | "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", 59 | "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", 60 | "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", 61 | "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", 62 | "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", 63 | "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", 64 | "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", 65 | "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", 66 | "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", 67 | "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", 68 | "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", 69 | "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", 70 | "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", 71 | "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", 72 | "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", 73 | "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", 74 | "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", 75 | "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", 76 | "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", 77 | "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", 78 | "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", 79 | "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", 80 | "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", 81 | "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", 82 | "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", 83 | "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d" 84 | ], 85 | "version": "==1.13.2" 86 | }, 87 | "click": { 88 | "hashes": [ 89 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 90 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 91 | ], 92 | "version": "==7.0" 93 | }, 94 | "flask": { 95 | "hashes": [ 96 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 97 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 98 | ], 99 | "index": "pypi", 100 | "version": "==1.1.1" 101 | }, 102 | "flask-bcrypt": { 103 | "hashes": [ 104 | "sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f" 105 | ], 106 | "index": "pypi", 107 | "version": "==0.7.1" 108 | }, 109 | "flask-jwt-extended": { 110 | "hashes": [ 111 | "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd" 112 | ], 113 | "index": "pypi", 114 | "version": "==3.24.1" 115 | }, 116 | "flask-mongoengine": { 117 | "hashes": [ 118 | "sha256:0f426aeafc4be2c37e9b4c0f8b5d02d012b7afc4b3b97a4119024684fe148fc1", 119 | "sha256:b6376b33cd1e624c09983a0884dc87303e54084f0e6b7f8df6794d56d35fa66f" 120 | ], 121 | "index": "pypi", 122 | "version": "==0.9.5" 123 | }, 124 | "flask-restful": { 125 | "hashes": [ 126 | "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", 127 | "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" 128 | ], 129 | "index": "pypi", 130 | "version": "==0.3.7" 131 | }, 132 | "flask-wtf": { 133 | "hashes": [ 134 | "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", 135 | "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" 136 | ], 137 | "version": "==0.14.2" 138 | }, 139 | "itsdangerous": { 140 | "hashes": [ 141 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 142 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 143 | ], 144 | "version": "==1.1.0" 145 | }, 146 | "jinja2": { 147 | "hashes": [ 148 | "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", 149 | "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" 150 | ], 151 | "version": "==2.10.3" 152 | }, 153 | "markupsafe": { 154 | "hashes": [ 155 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 156 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 157 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 158 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 159 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 160 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 161 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 162 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 163 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 164 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 165 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 166 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 167 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 168 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 169 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 170 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 171 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 172 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 173 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 174 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 175 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 176 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 177 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 178 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 179 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 180 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 181 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 182 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 183 | ], 184 | "version": "==1.1.1" 185 | }, 186 | "mongoengine": { 187 | "hashes": [ 188 | "sha256:9301ca84ada9377a200a50541f9be7d5308081bf2112049d00e1dd163f80b940", 189 | "sha256:fa3e73c966fca2b814cc1103ac4f55bcca7aae05028b112ef0cc8b321ee4a2f7" 190 | ], 191 | "version": "==0.18.2" 192 | }, 193 | "pycparser": { 194 | "hashes": [ 195 | "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" 196 | ], 197 | "version": "==2.19" 198 | }, 199 | "pyjwt": { 200 | "hashes": [ 201 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 202 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 203 | ], 204 | "version": "==1.7.1" 205 | }, 206 | "pymongo": { 207 | "hashes": [ 208 | "sha256:0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", 209 | "sha256:08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", 210 | "sha256:0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", 211 | "sha256:0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", 212 | "sha256:1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", 213 | "sha256:22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", 214 | "sha256:264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", 215 | "sha256:3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", 216 | "sha256:339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", 217 | "sha256:38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", 218 | "sha256:4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", 219 | "sha256:4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", 220 | "sha256:4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", 221 | "sha256:4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", 222 | "sha256:4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", 223 | "sha256:53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", 224 | "sha256:681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", 225 | "sha256:6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", 226 | "sha256:72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", 227 | "sha256:7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", 228 | "sha256:72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", 229 | "sha256:87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", 230 | "sha256:87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", 231 | "sha256:88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", 232 | "sha256:89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", 233 | "sha256:908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", 234 | "sha256:9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", 235 | "sha256:9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", 236 | "sha256:98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", 237 | "sha256:99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", 238 | "sha256:9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", 239 | "sha256:a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", 240 | "sha256:a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", 241 | "sha256:a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", 242 | "sha256:b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", 243 | "sha256:bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", 244 | "sha256:c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", 245 | "sha256:c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", 246 | "sha256:c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", 247 | "sha256:c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", 248 | "sha256:ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", 249 | "sha256:d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", 250 | "sha256:d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", 251 | "sha256:d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", 252 | "sha256:d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", 253 | "sha256:dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", 254 | "sha256:e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", 255 | "sha256:e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", 256 | "sha256:e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", 257 | "sha256:e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", 258 | "sha256:ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", 259 | "sha256:fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", 260 | "sha256:fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1" 261 | ], 262 | "version": "==3.10.0" 263 | }, 264 | "pytz": { 265 | "hashes": [ 266 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 267 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 268 | ], 269 | "version": "==2019.3" 270 | }, 271 | "six": { 272 | "hashes": [ 273 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 274 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 275 | ], 276 | "version": "==1.13.0" 277 | }, 278 | "werkzeug": { 279 | "hashes": [ 280 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 281 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 282 | ], 283 | "version": "==0.16.0" 284 | }, 285 | "wtforms": { 286 | "hashes": [ 287 | "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", 288 | "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" 289 | ], 290 | "version": "==2.2.1" 291 | } 292 | }, 293 | "develop": {} 294 | } 295 | -------------------------------------------------------------------------------- /Part - 4/movie-bag/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_bcrypt import Bcrypt 3 | from flask_jwt_extended import JWTManager 4 | 5 | from database.db import initialize_db 6 | from flask_restful import Api 7 | from resources.routes import initialize_routes 8 | from resources.errors import errors 9 | 10 | app = Flask(__name__) 11 | app.config.from_envvar('ENV_FILE_LOCATION') 12 | 13 | api = Api(app, errors=errors) 14 | bcrypt = Bcrypt(app) 15 | jwt = JWTManager(app) 16 | 17 | app.config['MONGODB_SETTINGS'] = { 18 | 'host': 'mongodb://localhost/movie-bag' 19 | } 20 | 21 | initialize_db(app) 22 | initialize_routes(api) 23 | 24 | app.run(port=3500) -------------------------------------------------------------------------------- /Part - 4/movie-bag/database/db.py: -------------------------------------------------------------------------------- 1 | from flask_mongoengine import MongoEngine 2 | 3 | db = MongoEngine() 4 | 5 | def initialize_db(app): 6 | db.init_app(app) -------------------------------------------------------------------------------- /Part - 4/movie-bag/database/models.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from flask_bcrypt import generate_password_hash, check_password_hash 3 | 4 | class Movie(db.Document): 5 | name = db.StringField(required=True, unique=True) 6 | casts = db.ListField(db.StringField(), required=True) 7 | genres = db.ListField(db.StringField(), required=True) 8 | added_by = db.ReferenceField('User') 9 | 10 | class User(db.Document): 11 | email = db.EmailField(required=True, unique=True) 12 | password = db.StringField(required=True, min_length=6) 13 | movies = db.ListField(db.ReferenceField('Movie', reverse_delete_rule=db.PULL)) 14 | 15 | def hash_password(self): 16 | self.password = generate_password_hash(self.password).decode('utf8') 17 | 18 | def check_password(self, password): 19 | return check_password_hash(self.password, password) 20 | 21 | User.register_delete_rule(Movie, 'added_by', db.CASCADE) -------------------------------------------------------------------------------- /Part - 4/movie-bag/resources/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from flask_jwt_extended import create_access_token 3 | from database.models import User 4 | from flask_restful import Resource 5 | import datetime 6 | from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist 7 | from resources.errors import SchemaValidationError, EmailAlreadyExistsError, UnauthorizedError, \ 8 | InternalServerError 9 | 10 | class SignupApi(Resource): 11 | def post(self): 12 | try: 13 | body = request.get_json() 14 | user = User(**body) 15 | user.hash_password() 16 | user.save() 17 | id = user.id 18 | return {'id': str(id)}, 200 19 | except FieldDoesNotExist: 20 | raise SchemaValidationError 21 | except NotUniqueError: 22 | raise EmailAlreadyExistsError 23 | except Exception as e: 24 | raise InternalServerError 25 | 26 | class LoginApi(Resource): 27 | def post(self): 28 | try: 29 | body = request.get_json() 30 | user = User.objects.get(email=body.get('email')) 31 | authorized = user.check_password(body.get('password')) 32 | if not authorized: 33 | raise UnauthorizedError 34 | 35 | expires = datetime.timedelta(days=7) 36 | access_token = create_access_token(identity=str(user.id), expires_delta=expires) 37 | return {'token': access_token}, 200 38 | except (UnauthorizedError, DoesNotExist): 39 | raise UnauthorizedError 40 | except Exception as e: 41 | raise InternalServerError -------------------------------------------------------------------------------- /Part - 4/movie-bag/resources/errors.py: -------------------------------------------------------------------------------- 1 | class InternalServerError(Exception): 2 | pass 3 | 4 | class SchemaValidationError(Exception): 5 | pass 6 | 7 | class MovieAlreadyExistsError(Exception): 8 | pass 9 | 10 | class UpdatingMovieError(Exception): 11 | pass 12 | 13 | class DeletingMovieError(Exception): 14 | pass 15 | 16 | class MovieNotExistsError(Exception): 17 | pass 18 | 19 | class EmailAlreadyExistsError(Exception): 20 | pass 21 | 22 | class UnauthorizedError(Exception): 23 | pass 24 | 25 | errors = { 26 | "InternalServerError": { 27 | "message": "Something went wrong", 28 | "status": 500 29 | }, 30 | "SchemaValidationError": { 31 | "message": "Request is missing required fields", 32 | "status": 400 33 | }, 34 | "MovieAlreadyExistsError": { 35 | "message": "Movie with given name already exists", 36 | "status": 400 37 | }, 38 | "UpdatingMovieError": { 39 | "message": "Updating movie added by other is forbidden", 40 | "status": 403 41 | }, 42 | "DeletingMovieError": { 43 | "message": "Deleting movie added by other is forbidden", 44 | "status": 403 45 | }, 46 | "MovieNotExistsError": { 47 | "message": "Movie with given id doesn't exists", 48 | "status": 400 49 | }, 50 | "EmailAlreadyExistsError": { 51 | "message": "User with given email address already exists", 52 | "status": 400 53 | }, 54 | "UnauthorizedError": { 55 | "message": "Invalid username or password", 56 | "status": 401 57 | } 58 | } -------------------------------------------------------------------------------- /Part - 4/movie-bag/resources/movie.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from database.models import Movie, User 3 | from flask_jwt_extended import jwt_required, get_jwt_identity 4 | from flask_restful import Resource 5 | from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist, ValidationError, InvalidQueryError 6 | from resources.errors import SchemaValidationError, MovieAlreadyExistsError, InternalServerError, \ 7 | UpdatingMovieError, DeletingMovieError, MovieNotExistsError 8 | 9 | 10 | class MoviesApi(Resource): 11 | def get(self): 12 | query = Movie.objects() 13 | movies = Movie.objects().to_json() 14 | return Response(movies, mimetype="application/json", status=200) 15 | 16 | @jwt_required 17 | def post(self): 18 | try: 19 | user_id = get_jwt_identity() 20 | body = request.get_json() 21 | user = User.objects.get(id=user_id) 22 | movie = Movie(**body, added_by=user) 23 | movie.save() 24 | user.update(push__movies=movie) 25 | user.save() 26 | id = movie.id 27 | return {'id': str(id)}, 200 28 | except (FieldDoesNotExist, ValidationError): 29 | raise SchemaValidationError 30 | except NotUniqueError: 31 | raise MovieAlreadyExistsError 32 | except Exception as e: 33 | raise InternalServerError 34 | 35 | 36 | class MovieApi(Resource): 37 | @jwt_required 38 | def put(self, id): 39 | try: 40 | user_id = get_jwt_identity() 41 | movie = Movie.objects.get(id=id, added_by=user_id) 42 | body = request.get_json() 43 | Movie.objects.get(id=id).update(**body) 44 | return '', 200 45 | except InvalidQueryError: 46 | raise SchemaValidationError 47 | except DoesNotExist: 48 | raise UpdatingMovieError 49 | except Exception: 50 | raise InternalServerError 51 | 52 | @jwt_required 53 | def delete(self, id): 54 | try: 55 | user_id = get_jwt_identity() 56 | movie = Movie.objects.get(id=id, added_by=user_id) 57 | movie.delete() 58 | return '', 200 59 | except DoesNotExist: 60 | raise DeletingMovieError 61 | except Exception: 62 | raise InternalServerError 63 | 64 | def get(self, id): 65 | try: 66 | movies = Movie.objects.get(id=id).to_json() 67 | return Response(movies, mimetype="application/json", status=200) 68 | except DoesNotExist: 69 | raise MovieNotExistsError 70 | except Exception: 71 | raise InternalServerError 72 | -------------------------------------------------------------------------------- /Part - 4/movie-bag/resources/routes.py: -------------------------------------------------------------------------------- 1 | from .movie import MoviesApi, MovieApi 2 | from .auth import SignupApi, LoginApi 3 | 4 | def initialize_routes(api): 5 | api.add_resource(MoviesApi, '/api/movies') 6 | api.add_resource(MovieApi, '/api/movies/') 7 | 8 | api.add_resource(SignupApi, '/api/auth/signup') 9 | api.add_resource(LoginApi, '/api/auth/login') 10 | -------------------------------------------------------------------------------- /Part - 5/Part-5 Password Reset.md: -------------------------------------------------------------------------------- 1 | ## Part 5: Password Reset 2 | 3 | Howdy! In the previous [Part](https://dev.to/paurakhsharma/flask-rest-api-part-4-exception-handling-5c6a) of the series, we learned how to handle errors in Flask and send a meaningful error message to the client. 4 | 5 | In this part, we are going to implement a password reset feature in our application. 6 | Here is the brief diagram of how the password reset flow is gonna look like. 7 | 8 | ![Password reset flow diagram](https://thepracticaldev.s3.amazonaws.com/i/6b04olppbml2z2n6gpo4.png) 9 | *Password reset flow diagram* 10 | 11 | We are going to use the `flask-jwt-extended` library to generate password reset token, the good thing is we have already installed it while implementing authentication. We need to send reset token to the user through email, for that we are going to use [Flask Mail](https://pythonhosted.org/flask-mail/). 12 | 13 | ``` 14 | pipenv install flask-mail 15 | ``` 16 | 17 | Let's register this mail server in our `app.py`: 18 | 19 | ```diff 20 | #~/movie-bag/app.py 21 | 22 | from flask import Flask 23 | from flask_bcrypt import Bcrypt 24 | from flask_jwt_extended import JWTManager 25 | +from flask_mail import Mail 26 | 27 | ... 28 | 29 | api = Api(app, errors=errors) 30 | bcrypt = Bcrypt(app) 31 | jwt = JWTManager(app) 32 | +mail = Mail(app) 33 | 34 | app.config['MONGODB_SETTINGS'] = { 35 | 'host': 'mongodb://localhost/movie-bag' 36 | ... 37 | ``` 38 | 39 | Now, let's create a service to send the email to the client, let's create a new folder `services` and a new file `mail_service.py` inside it. Add the following contents to the newly created file. 40 | 41 | ```bash 42 | mkdir services 43 | cd services 44 | touch mail_service.py 45 | ``` 46 | 47 | ```python 48 | #~/movie-bag/services/mail_service.py 49 | 50 | from threading import Thread 51 | from flask_mail import Message 52 | 53 | from app import app 54 | from app import mail 55 | 56 | 57 | def send_async_email(app, msg): 58 | with app.app_context(): 59 | try: 60 | mail.send(msg) 61 | except ConnectionRefusedError: 62 | raise InternalServerError("[MAIL SERVER] not working") 63 | 64 | 65 | def send_email(subject, sender, recipients, text_body, html_body): 66 | msg = Message(subject, sender=sender, recipients=recipients) 67 | msg.body = text_body 68 | msg.html = html_body 69 | Thread(target=send_async_email, args=(app, msg)).start() 70 | ``` 71 | 72 | Here you can see we have created a function `send_mail()` which takes `subject`, `sender`, `recipients`, `text_body` and `html_body` as arguments. It then creates a message object and runs `send_async_email()` in a separate thread, this is because while sending an email to the client we have to relay to the separate services such as Google, Outlook, etc. 73 | 74 | Since these services can take some time to actually send the email, we are going to tell the client that their request was successful and start sending the email in a separate thread. 75 | 76 | Now we are ready to implement the password reset. As shown in the diagram above we are going to create two different endpoints for this. 77 | 78 | 1) `/forget`: This endpoint takes the `email` of the user whose account needs to be changed. This endpoint then sends the email to the user with the link which contains reset token to reset the password. 79 | 80 | 2) `/reset`: This endpoint takes `reset_token` sent in the email and the new `password`. 81 | 82 | Let's create a `reset_password.py` inside the `resources` folder. With the following code: 83 | 84 | ```python 85 | #~/movie-bag/resources/reset_password.py 86 | 87 | from flask import request, render_template 88 | from flask_jwt_extended import create_access_token, decode_token 89 | from database.models import User 90 | from flask_restful import Resource 91 | import datetime 92 | from resources.errors import SchemaValidationError, InternalServerError, \ 93 | EmailDoesnotExistsError, BadTokenError 94 | from jwt.exceptions import ExpiredSignatureError, DecodeError, \ 95 | InvalidTokenError 96 | from services.mail_service import send_email 97 | 98 | class ForgotPassword(Resource): 99 | def post(self): 100 | url = request.host_url + 'reset/' 101 | try: 102 | body = request.get_json() 103 | email = body.get('email') 104 | if not email: 105 | raise SchemaValidationError 106 | 107 | user = User.objects.get(email=email) 108 | if not user: 109 | raise EmailDoesnotExistsError 110 | 111 | expires = datetime.timedelta(hours=24) 112 | reset_token = create_access_token(str(user.id), expires_delta=expires) 113 | 114 | return send_email('[Movie-bag] Reset Your Password', 115 | sender='support@movie-bag.com', 116 | recipients=[user.email], 117 | text_body=render_template('email/reset_password.txt', 118 | url=url + reset_token), 119 | html_body=render_template('email/reset_password.html', 120 | url=url + reset_token)) 121 | except SchemaValidationError: 122 | raise SchemaValidationError 123 | except EmailDoesnotExistsError: 124 | raise EmailDoesnotExistsError 125 | except Exception as e: 126 | raise InternalServerError 127 | 128 | 129 | class ResetPassword(Resource): 130 | def post(self): 131 | url = request.host_url + 'reset/' 132 | try: 133 | body = request.get_json() 134 | reset_token = body.get('reset_token') 135 | password = body.get('password') 136 | 137 | if not reset_token or not password: 138 | raise SchemaValidationError 139 | 140 | user_id = decode_token(reset_token)['identity'] 141 | 142 | user = User.objects.get(id=user_id) 143 | 144 | user.modify(password=password) 145 | user.hash_password() 146 | user.save() 147 | 148 | return send_email('[Movie-bag] Password reset successful', 149 | sender='support@movie-bag.com', 150 | recipients=[user.email], 151 | text_body='Password reset was successful', 152 | html_body='

Password reset was successful

') 153 | 154 | except SchemaValidationError: 155 | raise SchemaValidationError 156 | except ExpiredSignatureError: 157 | raise ExpiredTokenError 158 | except (DecodeError, InvalidTokenError): 159 | raise BadTokenError 160 | except Exception as e: 161 | raise InternalServerError 162 | ``` 163 | 164 | Here in the `ForgotPassword` resource, we first get the user based on the `email` provided by the client. We are then using `create_access_token()` to create a token based on `user.id` and this token expires in 24 hours. We are then sending the email to the client. The email contains both `HTML` and text format information. 165 | 166 | Similarly in `ResetPassword` resource, we first get the user based on user id from the reset_token and then reset the password of the user based on the password provided by the user. Finally, a reset success email is sent to the user. 167 | 168 | Let's create the new exceptions `EmailDoesnotExistsError` and `BadTokenError` in our `errors.py`. 169 | 170 | ```diff 171 | #~/movie-bag/resources/errors.py 172 | 173 | class UnauthorizedError(Exception): 174 | pass 175 | 176 | +class EmailDoesnotExistsError(Exception): 177 | + pass 178 | + 179 | +class BadTokenError(Exception): 180 | + pass 181 | + 182 | errors = { 183 | "InternalServerError": { 184 | "message": "Something went wrong", 185 | @@ -54,5 +60,13 @@ errors = { 186 | "UnauthorizedError": { 187 | "message": "Invalid username or password", 188 | "status": 401 189 | + }, 190 | + "EmailDoesnotExistsError": { 191 | + "message": "Couldn't find the user with given email address", 192 | + "status": 400 193 | + }, 194 | + "BadTokenError": { 195 | + "message": "Invalid token", 196 | + "status": 403 197 | } 198 | } 199 | ``` 200 | 201 | We need to create templates for HTML and text files that we need to send to the client. Let's create `templates` folder in our root directory, And inside `templates` create another folder `email` where we are creating two new files `reset_password.html` and `reset_password.txt`. 202 | 203 | ```bash 204 | mkdir templates 205 | cd templates 206 | mkdir email 207 | cd email 208 | touch reset_password.html 209 | touch reset_password.txt 210 | ``` 211 | 212 | In reset_password.html let's add the following: 213 | ```html 214 | 215 | 216 |

Dear, User

217 |

218 | To reset your password 219 | 220 | click here 221 | . 222 |

223 |

Alternatively, you can paste the following link in your browser's address bar:

224 |

{{ url }}

225 |

If you have not requested a password reset simply ignore this message.

226 |

Sincerely

227 |

Movie-bag Support Team

228 | 229 | ``` 230 | 231 | Here `{{ url }}` substitutes the url we have sent earlier in the `render_template()` function. 232 | 233 | Similarly, add the following in `reset_password.txt`: 234 | 235 | ```txt 236 | Dear, User 237 | 238 | To reset your password click on the following link: 239 | 240 | {{ url }} 241 | 242 | If you have not requested a password reset simply ignore this message. 243 | 244 | Sincerely 245 | 246 | Movie-bag Support Team 247 | ``` 248 | 249 | Now, we are ready to wire this `Resources` to our `routes.py`. 250 | 251 | ```diff 252 | from .movie import MoviesApi, MovieApi 253 | from .auth import SignupApi, LoginApi 254 | +from .reset_password import ForgotPassword, ResetPassword 255 | 256 | def initialize_routes(api): 257 | 258 | ... 259 | 260 | api.add_resource(LoginApi, '/api/auth/login') 261 | + 262 | + api.add_resource(ForgotPassword, '/api/auth/forgot') 263 | + api.add_resource(ResetPassword, '/api/auth/reset') 264 | 265 | ``` 266 | 267 | Now, if you try to run the application with `python app.py` 268 | 269 | You'll see the error something like this: 270 | ```bash 271 | ImportError: cannot import name 'initialize_routes' from 'resources.routes' (/home/paurakh/blog/flask/flask-restapi-series/movie-bag/resources/routes.py) 272 | ``` 273 | 274 | This is because of the circular dependency problem in python. In our `reset_password.py`, we import `send_mail` which is importing `app` from `app.py` whereas `app` is not yet defined on our `app.py`. 275 | 276 | ![Circular dependency](https://thepracticaldev.s3.amazonaws.com/i/3ymtwfv8cdeyc9t57bgh.png) 277 | 278 | To solve this issue we are going to create another file `run.py` in our root directory, which will be responsible for running our app. Also, we need to initialize our routes/view functions after we have initialized our app. 279 | 280 | ``` 281 | touch run.py 282 | ``` 283 | 284 | Now, our `app.py` should look like this: 285 | 286 | ```diff 287 | #~/movie-bag/app.py 288 | 289 | from database.db import initialize_db 290 | from flask_restful import Api 291 | -from resources.routes import initialize_routes 292 | from resources.errors import errors 293 | 294 | app = Flask(__name__) 295 | app.config.from_envvar('ENV_FILE_LOCATION') 296 | +mail = Mail(app) 297 | + 298 | +# imports requiring app and mail 299 | +from resources.routes import initialize_routes 300 | 301 | api = Api(app, errors=errors) 302 | bcrypt = Bcrypt(app) 303 | jwt = JWTManager(app) 304 | -mail = Mail(app) 305 | 306 | ... 307 | 308 | initialize_db(app) 309 | initialize_routes(api) 310 | - 311 | -app.run() 312 | ``` 313 | 314 | In our `run.py` we just run the app: 315 | 316 | ```python 317 | #~/movie-bag/run.py 318 | 319 | from app import app 320 | 321 | app.run() 322 | ``` 323 | 324 | Add configuration for our `MAIL_SERVER` in `.env` 325 | ```diff 326 | 327 | JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss' 328 | +MAIL_SERVER: "localhost" 329 | +MAIL_PORT = "1025" 330 | +MAIL_USERNAME = "support@movie-bag.com" 331 | +MAIL_PASSWORD = "" 332 | ``` 333 | 334 | Start a SMTP server in next terminal with: 335 | ```bash 336 | python -m smtpd -n -c DebuggingServer localhost:1025 337 | ``` 338 | This will create an SMTP server for testing our email feature. 339 | 340 | 341 | Now run the app with 342 | ```bash 343 | python run.py 344 | ``` 345 | *Note: remember to export `ENV_FILE_LOCATION`* 346 | 347 | ![Postmant forgot endpoint request](https://thepracticaldev.s3.amazonaws.com/i/2rrs1eu5v39t50sysbur.png) 348 | 349 | If the email is of the existing user you can see the email in the terminal running `smtp` server as: 350 | 351 | ```html 352 | 353 |

Dear, User

354 |

355 | To reset your password 356 | 357 | click here 358 | . 359 |

360 |

Alternatively, you can paste the following link in your browser's address bar:

361 |

http://localhost:3000/reset/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NzgzOTU0ODUsIm5iZiI6MTU3ODM5NTQ4NSwianRpIjoiZTEyZDg3ODgtMTkwZS00NWI1LWI0YzYtZTdkMTYzZjc5ZGZlIiwiZXhwIjoxNTc4NDgxODg1LCJpZGVudGl0eSI6IjVlMTQxNTJmOWRlNzQxZDNjNGYwYmNiYiIsImZyZXNoIjpmYWxzZSwidHlwZSI6ImFjY2VzcyJ9.dLJnhYTYMnLuLg_cHDdqi-jsXeISeMq75mb-ozaNxlw

362 |

If you have not requested a password reset simply ignore this message.

363 |

Sincerely

364 |

Movie-bag Support Team

365 | ``` 366 | 367 | As you can see the URL is of format: 368 | 369 | `http://localhost:3000/reset/`, you need to copy this token a send manually in your `/reset` endpoint. 370 | 371 | *Note: We will learn how to implement to reset automatically in our front-end series but for now we need to manually copy the reset_token* 372 | 373 | ![Postman reset password request](https://thepracticaldev.s3.amazonaws.com/i/naz25gfxwa6dy6e9uo6l.png) 374 | 375 | Congratulations your password is changed successfully. Now, you can log in with the new password. 376 | 377 | You should also get the email stating your password was reset successfully. 378 | 379 | ```html 380 |

Password reset was successful

381 | ``` 382 | 383 | You can find all the code we have written till now [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%205) 384 | 385 | ### What we learned from this part of the series? 386 | - How to create token for resetting user password 387 | - How to send email to using `Flask-mail` 388 | - How to reset user password 389 | - How to avoid circular dependancy in flask. 390 | 391 | In the next part of the series we are going to learn about testing our Flask REST APIs. 392 | 393 | Until then, Happy Coding 😊 -------------------------------------------------------------------------------- /Part - 5/movie-bag/.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss' 2 | MAIL_SERVER: "localhost" 3 | MAIL_PORT = "1025" 4 | MAIL_USERNAME = "support@movie-bag.com" 5 | MAIL_PASSWORD = "" -------------------------------------------------------------------------------- /Part - 5/movie-bag/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "*" 10 | flask-mongoengine = "*" 11 | flask-restful = "*" 12 | flask-bcrypt = "*" 13 | flask-jwt-extended = "*" 14 | flask-mail = "*" 15 | 16 | [requires] 17 | python_version = "3.7" 18 | -------------------------------------------------------------------------------- /Part - 5/movie-bag/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_bcrypt import Bcrypt 3 | from flask_jwt_extended import JWTManager 4 | from flask_mail import Mail 5 | 6 | from database.db import initialize_db 7 | from flask_restful import Api 8 | from resources.errors import errors 9 | 10 | app = Flask(__name__) 11 | app.config.from_envvar('ENV_FILE_LOCATION') 12 | mail = Mail(app) 13 | 14 | # imports requiring app and mail 15 | from resources.routes import initialize_routes 16 | 17 | api = Api(app, errors=errors) 18 | bcrypt = Bcrypt(app) 19 | jwt = JWTManager(app) 20 | 21 | app.config['MONGODB_SETTINGS'] = { 22 | 'host': 'mongodb://localhost/movie-bag' 23 | } 24 | 25 | initialize_db(app) 26 | initialize_routes(api) 27 | -------------------------------------------------------------------------------- /Part - 5/movie-bag/database/db.py: -------------------------------------------------------------------------------- 1 | from flask_mongoengine import MongoEngine 2 | 3 | db = MongoEngine() 4 | 5 | def initialize_db(app): 6 | db.init_app(app) -------------------------------------------------------------------------------- /Part - 5/movie-bag/database/models.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from flask_bcrypt import generate_password_hash, check_password_hash 3 | 4 | class Movie(db.Document): 5 | name = db.StringField(required=True, unique=True) 6 | casts = db.ListField(db.StringField(), required=True) 7 | genres = db.ListField(db.StringField(), required=True) 8 | added_by = db.ReferenceField('User') 9 | 10 | class User(db.Document): 11 | email = db.EmailField(required=True, unique=True) 12 | password = db.StringField(required=True, min_length=6) 13 | movies = db.ListField(db.ReferenceField('Movie', reverse_delete_rule=db.PULL)) 14 | 15 | def hash_password(self): 16 | self.password = generate_password_hash(self.password).decode('utf8') 17 | 18 | def check_password(self, password): 19 | return check_password_hash(self.password, password) 20 | 21 | User.register_delete_rule(Movie, 'added_by', db.CASCADE) -------------------------------------------------------------------------------- /Part - 5/movie-bag/resources/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from flask_jwt_extended import create_access_token 3 | from database.models import User 4 | from flask_restful import Resource 5 | import datetime 6 | from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist 7 | from resources.errors import SchemaValidationError, EmailAlreadyExistsError, UnauthorizedError, \ 8 | InternalServerError 9 | 10 | class SignupApi(Resource): 11 | def post(self): 12 | try: 13 | body = request.get_json() 14 | user = User(**body) 15 | user.hash_password() 16 | user.save() 17 | id = user.id 18 | return {'id': str(id)}, 200 19 | except FieldDoesNotExist: 20 | raise SchemaValidationError 21 | except NotUniqueError: 22 | raise EmailAlreadyExistsError 23 | except Exception as e: 24 | raise InternalServerError 25 | 26 | class LoginApi(Resource): 27 | def post(self): 28 | try: 29 | body = request.get_json() 30 | user = User.objects.get(email=body.get('email')) 31 | authorized = user.check_password(body.get('password')) 32 | if not authorized: 33 | raise UnauthorizedError 34 | 35 | expires = datetime.timedelta(days=7) 36 | access_token = create_access_token(identity=str(user.id), expires_delta=expires) 37 | return {'token': access_token}, 200 38 | except (UnauthorizedError, DoesNotExist): 39 | raise UnauthorizedError 40 | except Exception as e: 41 | raise InternalServerError -------------------------------------------------------------------------------- /Part - 5/movie-bag/resources/errors.py: -------------------------------------------------------------------------------- 1 | class InternalServerError(Exception): 2 | pass 3 | 4 | class SchemaValidationError(Exception): 5 | pass 6 | 7 | class MovieAlreadyExistsError(Exception): 8 | pass 9 | 10 | class UpdatingMovieError(Exception): 11 | pass 12 | 13 | class DeletingMovieError(Exception): 14 | pass 15 | 16 | class MovieNotExistsError(Exception): 17 | pass 18 | 19 | class EmailAlreadyExistsError(Exception): 20 | pass 21 | 22 | class UnauthorizedError(Exception): 23 | pass 24 | 25 | class EmailDoesnotExistsError(Exception): 26 | pass 27 | 28 | class BadTokenError(Exception): 29 | pass 30 | 31 | errors = { 32 | "InternalServerError": { 33 | "message": "Something went wrong", 34 | "status": 500 35 | }, 36 | "SchemaValidationError": { 37 | "message": "Request is missing required fields", 38 | "status": 400 39 | }, 40 | "MovieAlreadyExistsError": { 41 | "message": "Movie with given name already exists", 42 | "status": 400 43 | }, 44 | "UpdatingMovieError": { 45 | "message": "Updating movie added by other is forbidden", 46 | "status": 403 47 | }, 48 | "DeletingMovieError": { 49 | "message": "Deleting movie added by other is forbidden", 50 | "status": 403 51 | }, 52 | "MovieNotExistsError": { 53 | "message": "Movie with given id doesn't exists", 54 | "status": 400 55 | }, 56 | "EmailAlreadyExistsError": { 57 | "message": "User with given email address already exists", 58 | "status": 400 59 | }, 60 | "UnauthorizedError": { 61 | "message": "Invalid username or password", 62 | "status": 401 63 | }, 64 | "EmailDoesnotExistsError": { 65 | "message": "Couldn't find the user with given email address", 66 | "status": 400 67 | }, 68 | "BadTokenError": { 69 | "message": "Invalid token", 70 | "status": 403 71 | } 72 | } -------------------------------------------------------------------------------- /Part - 5/movie-bag/resources/movie.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from database.models import Movie, User 3 | from flask_jwt_extended import jwt_required, get_jwt_identity 4 | from flask_restful import Resource 5 | from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist, ValidationError, InvalidQueryError 6 | from resources.errors import SchemaValidationError, MovieAlreadyExistsError, InternalServerError, \ 7 | UpdatingMovieError, DeletingMovieError, MovieNotExistsError 8 | 9 | 10 | class MoviesApi(Resource): 11 | def get(self): 12 | query = Movie.objects() 13 | movies = Movie.objects().to_json() 14 | return Response(movies, mimetype="application/json", status=200) 15 | 16 | @jwt_required 17 | def post(self): 18 | try: 19 | user_id = get_jwt_identity() 20 | body = request.get_json() 21 | user = User.objects.get(id=user_id) 22 | movie = Movie(**body, added_by=user) 23 | movie.save() 24 | user.update(push__movies=movie) 25 | user.save() 26 | id = movie.id 27 | return {'id': str(id)}, 200 28 | except (FieldDoesNotExist, ValidationError): 29 | raise SchemaValidationError 30 | except NotUniqueError: 31 | raise MovieAlreadyExistsError 32 | except Exception as e: 33 | raise InternalServerError 34 | 35 | 36 | class MovieApi(Resource): 37 | @jwt_required 38 | def put(self, id): 39 | try: 40 | user_id = get_jwt_identity() 41 | movie = Movie.objects.get(id=id, added_by=user_id) 42 | body = request.get_json() 43 | Movie.objects.get(id=id).update(**body) 44 | return '', 200 45 | except InvalidQueryError: 46 | raise SchemaValidationError 47 | except DoesNotExist: 48 | raise UpdatingMovieError 49 | except Exception: 50 | raise InternalServerError 51 | 52 | @jwt_required 53 | def delete(self, id): 54 | try: 55 | user_id = get_jwt_identity() 56 | movie = Movie.objects.get(id=id, added_by=user_id) 57 | movie.delete() 58 | return '', 200 59 | except DoesNotExist: 60 | raise DeletingMovieError 61 | except Exception: 62 | raise InternalServerError 63 | 64 | def get(self, id): 65 | try: 66 | movies = Movie.objects.get(id=id).to_json() 67 | return Response(movies, mimetype="application/json", status=200) 68 | except DoesNotExist: 69 | raise MovieNotExistsError 70 | except Exception: 71 | raise InternalServerError 72 | -------------------------------------------------------------------------------- /Part - 5/movie-bag/resources/reset_password.py: -------------------------------------------------------------------------------- 1 | from flask import request, render_template 2 | from flask_jwt_extended import create_access_token, decode_token 3 | from database.models import User 4 | from flask_restful import Resource 5 | import datetime 6 | from resources.errors import SchemaValidationError, InternalServerError, \ 7 | EmailDoesnotExistsError, BadTokenError 8 | from jwt.exceptions import ExpiredSignatureError, DecodeError, \ 9 | InvalidTokenError 10 | from services.mail_service import send_email 11 | 12 | class ForgotPassword(Resource): 13 | def post(self): 14 | url = request.host_url + 'reset/' 15 | try: 16 | body = request.get_json() 17 | email = body.get('email') 18 | if not email: 19 | raise SchemaValidationError 20 | 21 | user = User.objects.get(email=email) 22 | if not user: 23 | raise EmailDoesnotExistsError 24 | 25 | expires = datetime.timedelta(hours=24) 26 | reset_token = create_access_token(str(user.id), expires_delta=expires) 27 | 28 | return send_email('[Movie-bag] Reset Your Password', 29 | sender='support@movie-bag.com', 30 | recipients=[user.email], 31 | text_body=render_template('email/reset_password.txt', 32 | url=url + reset_token), 33 | html_body=render_template('email/reset_password.html', 34 | url=url + reset_token)) 35 | except SchemaValidationError: 36 | raise SchemaValidationError 37 | except EmailDoesnotExistsError: 38 | raise EmailDoesnotExistsError 39 | except Exception as e: 40 | raise InternalServerError 41 | 42 | 43 | class ResetPassword(Resource): 44 | def post(self): 45 | url = request.host_url + 'reset/' 46 | try: 47 | body = request.get_json() 48 | reset_token = body.get('reset_token') 49 | password = body.get('password') 50 | 51 | if not reset_token or not password: 52 | raise SchemaValidationError 53 | 54 | user_id = decode_token(reset_token)['identity'] 55 | 56 | user = User.objects.get(id=user_id) 57 | 58 | user.modify(password=password) 59 | user.hash_password() 60 | user.save() 61 | 62 | return send_email('[Movie-bag] Password reset successful', 63 | sender='support@movie-bag.com', 64 | recipients=[user.email], 65 | text_body='Password reset was successful', 66 | html_body='

Password reset was successful

') 67 | 68 | except SchemaValidationError: 69 | raise SchemaValidationError 70 | except ExpiredSignatureError: 71 | raise ExpiredTokenError 72 | except (DecodeError, InvalidTokenError): 73 | raise BadTokenError 74 | except Exception as e: 75 | raise InternalServerError -------------------------------------------------------------------------------- /Part - 5/movie-bag/resources/routes.py: -------------------------------------------------------------------------------- 1 | from .movie import MoviesApi, MovieApi 2 | from .auth import SignupApi, LoginApi 3 | from .reset_password import ForgotPassword, ResetPassword 4 | 5 | def initialize_routes(api): 6 | api.add_resource(MoviesApi, '/api/movies') 7 | api.add_resource(MovieApi, '/api/movies/') 8 | 9 | api.add_resource(SignupApi, '/api/auth/signup') 10 | api.add_resource(LoginApi, '/api/auth/login') 11 | 12 | api.add_resource(ForgotPassword, '/api/auth/forgot') 13 | api.add_resource(ResetPassword, '/api/auth/reset') 14 | -------------------------------------------------------------------------------- /Part - 5/movie-bag/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | app.run(port=3500) -------------------------------------------------------------------------------- /Part - 5/movie-bag/services/mail_service.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from flask_mail import Message 3 | 4 | from app import app 5 | from app import mail 6 | 7 | 8 | def send_async_email(app, msg): 9 | with app.app_context(): 10 | try: 11 | mail.send(msg) 12 | except ConnectionRefusedError: 13 | raise InternalServerError("[MAIL SERVER] not working") 14 | 15 | 16 | def send_email(subject, sender, recipients, text_body, html_body): 17 | msg = Message(subject, sender=sender, recipients=recipients) 18 | msg.body = text_body 19 | msg.html = html_body 20 | Thread(target=send_async_email, args=(app, msg)).start() -------------------------------------------------------------------------------- /Part - 5/movie-bag/templates/email/reset_password.html: -------------------------------------------------------------------------------- 1 |

Dear, User

2 |

3 | To reset your password 4 | 5 | click here 6 | . 7 |

8 |

Alternatively, you can paste the following link in your browser's address bar:

9 |

{{ url }}

10 |

If you have not requested a password reset simply ignore this message.

11 |

Sincerely

12 |

Movie-bag Support Team

-------------------------------------------------------------------------------- /Part - 5/movie-bag/templates/email/reset_password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paurakhsharma/flask-rest-api-blog-series/3ffdbfa303455b0b1044735f74dab36f08f0d3ca/Part - 5/movie-bag/templates/email/reset_password.txt -------------------------------------------------------------------------------- /Part - 6/Part-6 Testing REST APIs.md: -------------------------------------------------------------------------------- 1 | ## Part 6: Testing REST APIs 2 | 3 | Howdy! In the previous [Part](https://dev.to/paurakhsharma/flask-rest-api-part-5-password-reset-2f2e) of the series, we learned how to perform 4 | password reset in our REST API. 5 | 6 | In this part, we are going to learn how to test our REST API endpoints. 7 | 8 | ### Why should we spend time writing tests? 9 | - To make sure our application doesn't break while making changes/refactoring 10 | - To automate repetitive manual tests reducing human errors 11 | - Be able to release to the production on Fridays ;) 12 | - Testing provides a better CI/ CD workflow. 13 | 14 | I hope you are convinced that we should write tests. Let's get started with testing our Flask application. 15 | 16 | When it comes to testing, there are two most popular tools to test Python applications. 17 | 1) [unittest](https://docs.python.org/3/library/unittest.html): `unittest` is a python [standard library](https://docs.python.org/3/library/) which means it is distributed with Python. `unittest` provides tons of tools for constructing and running tests. 18 | 19 | 2) [pytest](https://docs.pytest.org/en/latest/): `pytest` is a python library which I like to call is the superset of `unittest` which means you can run tests written in `unittest` with `pytest`. It makes writing tests easier and faster. 20 | 21 | In this tutorial, we are going to learn how to write tests using `unittest`, because it enables us to write our tests using [OOP](https://docs.python.org/3/tutorial/classes.html). 22 | 23 | Before we start, remove the line below from `app.py` 24 | 25 | ```py 26 | app.config['MONGODB_SETTINGS'] = { 27 | 'host': 'mongodb://localhost/movie-bag' 28 | } 29 | ``` 30 | and add 31 | ```bash 32 | MONGODB_SETTINGS = { 33 | 'host': 'mongodb://localhost/movie-bag' 34 | } 35 | ``` 36 | to our `.env`, this step is required because we want to use a different database for developing our application and running the tests. 37 | 38 | First of all, let's create an `env` file to store our test-related configurations, we should separate our test configs from our development and production configs. 39 | 40 | In the root directory create a file `.env.test` and add the following configs to it. 41 | 42 | ```bash 43 | touch .env.test 44 | ``` 45 | 46 | ```bash 47 | #~/movie-bag/.env.test 48 | 49 | JWT_SECRET_KEY = 'super-secret' 50 | MAIL_SERVER: "localhost" 51 | MAIL_PORT = "1025" 52 | MAIL_USERNAME = "support@movie-bag.com" 53 | MAIL_PASSWORD = "" 54 | MONGODB_SETTINGS = { 55 | 'host': 'mongodb://localhost/movie-bag-test' 56 | } 57 | ``` 58 | >*Notice that we have used different database for our test config, this is done because our tests and we want our tests and development database to be separated. We also want our test database to be empty before running the tests.* 59 | 60 | Now, let's create a new folder `tests` inside our root directory. Create a new file `__init__.py` inside the `tests` folder, also, create a new file `test_signup.py`. 61 | 62 | ```bash 63 | mkdir tests 64 | cd tests 65 | touch __init__.py 66 | touch test_signup.py 67 | ``` 68 | 69 | ```python 70 | #~/movie-bag/tests/test_signup.py 71 | 72 | import unittest 73 | import json 74 | 75 | from app import app 76 | from database.db import db 77 | 78 | 79 | class SignupTest(unittest.TestCase): 80 | 81 | def setUp(self): 82 | self.app = app.test_client() 83 | self.db = db.get_db() 84 | 85 | def test_successful_signup(self): 86 | # Given 87 | payload = json.dumps({ 88 | "email": "paurakh011@gmail.com", 89 | "password": "mycoolpassword" 90 | }) 91 | 92 | # When 93 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 94 | 95 | # Then 96 | self.assertEqual(str, type(response.json['id'])) 97 | self.assertEqual(200, response.status_code) 98 | 99 | def tearDown(self): 100 | # Delete Database collections after the test is complete 101 | for collection in self.db.list_collection_names(): 102 | self.db.drop_collection(collection) 103 | ``` 104 | 105 | 106 | Let's go step by step to understand what is actually going on. 107 | 108 | First of all, we define `SignupTest` class which extends `unittest.TestCase`. `TestCase` provides us with useful methods such as `setUp` and `tearDown` and also the assertation methods. 109 | 110 | `setUp()` method runs each time before running each method defined on the `SignupTest` class. `setUp()` as the name suggests is used to set up our test infrastructure before running the tests.
111 | Here you can see we define `this.app` and `this.db` in this method. We use `app.test_client()` instead of `app` because it makes testing our flask application easier. Also, we get our `Database` instance with `db.get_db()` and set it to `this.db`. 112 | 113 | 114 | Similarly, `test_successful_signup()` is the method that is actually testing the `Signup` feature. Here we have defined a payload which should be a `JSON` value. And we send a `POST` request to `/api/auth/signup`. 115 | 116 | The response from the request is used to finally assert that our `Signup` feature actually sent the user id and successful status code which is `200`. 117 | 118 | Finally, after each test methods the `tearDown()` method runs each time. This method is responsible for clearing our infrastructure setup. This includes deleting our database collection for `test isolation`. 119 | 120 | ### Test Isolation 121 | Test isolation is one of the most important concepts in testing. Usually, when we are writing tests, we test one business logic. The idea of test isolation is that one of your tests should not in any way affect another test.
122 | Suppose that you have created a user in one test and you are testing login on another test. To follow test isolation you cannot depend on the user-created in a user creation test, but should create the user right in the test where you are going to test login. Why? Because your login test might run before your user creation test this makes your test fail. 123 | 124 | Also, if we do not delete our user which we created on the previous test run, and we try to run the test again, our test fails because the user is already there. 125 | So, we should always test a feature from an empty state and for that easiest way is to delete all the collections in our database. 126 | 127 | 128 | Before running our first test make sure to export environment variable `ENV_FILE_LOCATION` with the location to the test env file. 129 | 130 | To set this value mac/linux can run the command: 131 | ``` 132 | export ENV_FILE_LOCATION=./.env.test 133 | ``` 134 | 135 | and windows user can run the command: 136 | ``` 137 | set ENV_FILE_LOCATION=./.env.test 138 | ``` 139 | 140 | Make sure you have activated your virtual environment with `pipenv shell`. 141 | 142 | To run the test enter this command in your terminal. 143 | ``` 144 | python -m unittest tests/test_signup.py 145 | ``` 146 | 147 | You should be able to see the output like this: 148 | ``` 149 | . 150 | ---------------------------------------------------------------------- 151 | Ran 1 test in 1.023s 152 | 153 | OK 154 | ``` 155 | This means our test run successfully. 156 | 157 | **If you run into any error feel free to comment down, I am always ready to help you out** 158 | 159 | As you can see we are going to need this `setUp()` and `tearDown()` in our ever TestCase. So, let's move this logic to a new file, let's call it `BaseCase.py`. 160 | 161 | ```python 162 | #~/movie-bag/tests/BaseCase.py 163 | 164 | import unittest 165 | 166 | from app import app 167 | from database.db import db 168 | 169 | 170 | class BaseCase(unittest.TestCase): 171 | 172 | def setUp(self): 173 | self.app = app.test_client() 174 | self.db = db.get_db() 175 | 176 | 177 | def tearDown(self): 178 | # Delete Database collections after the test is complete 179 | for collection in self.db.list_collection_names(): 180 | self.db.drop_collection(collection) 181 | ``` 182 | 183 | Now update your `test_signup.py` to look like this: 184 | 185 | ```diff 186 | 187 | import json 188 | 189 | -from app import app 190 | -from database.db import db 191 | +from tests.BaseCase import BaseCase 192 | 193 | - 194 | class SignupTest(unittest.TestCase): 195 | - 196 | - def setUp(self): 197 | - self.app = app.test_client() 198 | - self.db = db.get_db() 199 | 200 | def test_successful_signup(self): 201 | # Given 202 | ... 203 | - 204 | - def tearDown(self): 205 | - # Delete Database collections after the test is complete 206 | - for collection in self.db.list_collection_names(): 207 | - self.db.drop_collection(collection) 208 | ``` 209 | 210 | Now let's add test for our `Login` feature, create a new file `test_login.py` inside `tests` folder with the following code. 211 | 212 | ```python 213 | #~/movie-bag/tests/test_login.py 214 | 215 | import json 216 | 217 | from tests.BaseCase import BaseCase 218 | 219 | class TestUserLogin(BaseCase): 220 | 221 | def test_successful_login(self): 222 | # Given 223 | email = "paurakh011@gmail.com" 224 | password = "mycoolpassword" 225 | payload = json.dumps({ 226 | "email": email, 227 | "password": password 228 | }) 229 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 230 | 231 | # When 232 | response = self.app.post('/api/auth/login', headers={"Content-Type": "application/json"}, data=payload) 233 | 234 | # Then 235 | self.assertEqual(str, type(response.json['token'])) 236 | self.assertEqual(200, response.status_code) 237 | ``` 238 | 239 | 240 | Here we first created the user with `/api/auth/signup` endpoint and login using the same email and password and assert that the `/api/auth/login` endpoint returns the token. 241 | 242 | Now, let's add tests to check the creation of the movie. 243 | Create `test_create_movie.py` with the code below. 244 | 245 | ```python 246 | #movie-bag/tests/test_create_movie.py 247 | 248 | import json 249 | 250 | from tests.BaseCase import BasicTest 251 | 252 | class TestUserLogin(BasicTest): 253 | 254 | def test_successful_login(self): 255 | # Given 256 | email = "paurakh011@gmail.com" 257 | password = "mycoolpassword" 258 | user_payload = json.dumps({ 259 | "email": email, 260 | "password": password 261 | }) 262 | 263 | self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=user_payload) 264 | response = self.app.post('/api/auth/login', headers={"Content-Type": "application/json"}, data=user_payload) 265 | login_token = response.json['token'] 266 | 267 | movie_payload = { 268 | "name": "Star Wars: The Rise of Skywalker", 269 | "casts": ["Daisy Ridley", "Adam Driver"], 270 | "genres": ["Fantasy", "Sci-fi"] 271 | } 272 | # When 273 | response = self.app.post('/api/movies', 274 | headers={"Content-Type": "application/json", "Authorization": f"Bearer {login_token}"}, 275 | data=json.dumps(movie_payload)) 276 | 277 | # Then 278 | self.assertEqual(str, type(response.json['id'])) 279 | self.assertEqual(200, response.status_code) 280 | ``` 281 | 282 | To run all the tests at once use the command: 283 | 284 | ``` 285 | python -m unittest --buffer 286 | ``` 287 | 288 | Here `--buffer` or `-b` is used to discard the output on a successful test run. 289 | 290 | 291 | Here we first signup for the user account, log in as the user to get the login token and then use the login token to create a movie. Finally, we check to see if the movie creating endpoint returns the `id` to the created movie. 292 | 293 | You might have noticed in this test we only check if the movie creation works but do not check if the user creation worked or user login worked. This is because we already have separate tests that are testing these things so, we don't have to repeat the same tests. 294 | 295 | 296 | >*We have only created happy path tests but it is crucial for us to test that our application response is expected even in the case when the user enters invalid input. For instance, the user doesn't send the password while signing up or sends an invalid format email.* 297 | 298 | **I have not included these tests in the tutorial itself, but I will be sure to include them in the Github repo.** 299 | 300 | You can find all the code we have written till now and **more tests** [here](https://github.com/paurakhsharma/flask-rest-api-blog-series/tree/master/Part%20-%206) 301 | 302 | ### What we learned from this part of the series? 303 | - Why we should write tests for our application 304 | - What test isolation is and why we should isolate our tests cases 305 | - How to test Flask REST APIs with `unittest` 306 | 307 | 308 | Until then, Happy Coding 😊 309 | -------------------------------------------------------------------------------- /Part - 6/movie-bag/.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss' 2 | MAIL_SERVER: "localhost" 3 | MAIL_PORT = "1025" 4 | MAIL_USERNAME = "support@movie-bag.com" 5 | MAIL_PASSWORD = "" 6 | MONGODB_SETTINGS = { 7 | 'host': 'mongodb://localhost/movie-bag' 8 | } -------------------------------------------------------------------------------- /Part - 6/movie-bag/.env.test: -------------------------------------------------------------------------------- 1 | JWT_SECRET_KEY = 'super-secret' 2 | MAIL_SERVER: "localhost" 3 | MAIL_PORT = "1025" 4 | MAIL_USERNAME = "support@movie-bag.com" 5 | MAIL_PASSWORD = "" 6 | MONGODB_SETTINGS = { 7 | 'host': 'mongodb://localhost/movie-bag-test' 8 | } -------------------------------------------------------------------------------- /Part - 6/movie-bag/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | 7 | [dev-packages] 8 | 9 | [packages] 10 | flask = "*" 11 | flask-mongoengine = "*" 12 | flask-restful = "*" 13 | flask-bcrypt = "*" 14 | flask-jwt-extended = "*" 15 | flask-mail = "*" 16 | 17 | [requires] 18 | python_version = "3.7" 19 | -------------------------------------------------------------------------------- /Part - 6/movie-bag/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_bcrypt import Bcrypt 3 | from flask_jwt_extended import JWTManager 4 | from flask_mail import Mail 5 | 6 | from database.db import initialize_db 7 | from flask_restful import Api 8 | from resources.errors import errors 9 | 10 | app = Flask(__name__) 11 | app.config.from_envvar('ENV_FILE_LOCATION') 12 | mail = Mail(app) 13 | 14 | # imports requiring app and mail 15 | from resources.routes import initialize_routes 16 | 17 | api = Api(app, errors=errors) 18 | bcrypt = Bcrypt(app) 19 | jwt = JWTManager(app) 20 | 21 | initialize_db(app) 22 | initialize_routes(api) 23 | -------------------------------------------------------------------------------- /Part - 6/movie-bag/database/db.py: -------------------------------------------------------------------------------- 1 | from flask_mongoengine import MongoEngine 2 | 3 | db = MongoEngine() 4 | 5 | def initialize_db(app): 6 | db.init_app(app) -------------------------------------------------------------------------------- /Part - 6/movie-bag/database/models.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from flask_bcrypt import generate_password_hash, check_password_hash 3 | 4 | class Movie(db.Document): 5 | name = db.StringField(required=True, unique=True) 6 | casts = db.ListField(db.StringField(), required=True) 7 | genres = db.ListField(db.StringField(), required=True) 8 | added_by = db.ReferenceField('User') 9 | 10 | class User(db.Document): 11 | email = db.EmailField(required=True, unique=True) 12 | password = db.StringField(required=True, min_length=6) 13 | movies = db.ListField(db.ReferenceField('Movie', reverse_delete_rule=db.PULL)) 14 | 15 | def hash_password(self): 16 | self.password = generate_password_hash(self.password).decode('utf8') 17 | 18 | def check_password(self, password): 19 | return check_password_hash(self.password, password) 20 | 21 | User.register_delete_rule(Movie, 'added_by', db.CASCADE) -------------------------------------------------------------------------------- /Part - 6/movie-bag/resources/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from flask_jwt_extended import create_access_token 3 | from database.models import User 4 | from flask_restful import Resource 5 | import datetime 6 | from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist 7 | from resources.errors import SchemaValidationError, EmailAlreadyExistsError, UnauthorizedError, \ 8 | InternalServerError 9 | 10 | class SignupApi(Resource): 11 | def post(self): 12 | try: 13 | body = request.get_json() 14 | user = User(**body) 15 | user.hash_password() 16 | user.save() 17 | id = user.id 18 | return {'id': str(id)}, 200 19 | except FieldDoesNotExist: 20 | raise SchemaValidationError 21 | except NotUniqueError: 22 | raise EmailAlreadyExistsError 23 | except Exception as e: 24 | raise InternalServerError 25 | 26 | class LoginApi(Resource): 27 | def post(self): 28 | try: 29 | body = request.get_json() 30 | user = User.objects.get(email=body.get('email')) 31 | authorized = user.check_password(body.get('password')) 32 | if not authorized: 33 | raise UnauthorizedError 34 | 35 | expires = datetime.timedelta(days=7) 36 | access_token = create_access_token(identity=str(user.id), expires_delta=expires) 37 | return {'token': access_token}, 200 38 | except (UnauthorizedError, DoesNotExist): 39 | raise UnauthorizedError 40 | except Exception as e: 41 | raise InternalServerError -------------------------------------------------------------------------------- /Part - 6/movie-bag/resources/errors.py: -------------------------------------------------------------------------------- 1 | class InternalServerError(Exception): 2 | pass 3 | 4 | class SchemaValidationError(Exception): 5 | pass 6 | 7 | class MovieAlreadyExistsError(Exception): 8 | pass 9 | 10 | class UpdatingMovieError(Exception): 11 | pass 12 | 13 | class DeletingMovieError(Exception): 14 | pass 15 | 16 | class MovieNotExistsError(Exception): 17 | pass 18 | 19 | class EmailAlreadyExistsError(Exception): 20 | pass 21 | 22 | class UnauthorizedError(Exception): 23 | pass 24 | 25 | class EmailDoesnotExistsError(Exception): 26 | pass 27 | 28 | class BadTokenError(Exception): 29 | pass 30 | 31 | errors = { 32 | "InternalServerError": { 33 | "message": "Something went wrong", 34 | "status": 500 35 | }, 36 | "SchemaValidationError": { 37 | "message": "Request is missing required fields", 38 | "status": 400 39 | }, 40 | "MovieAlreadyExistsError": { 41 | "message": "Movie with given name already exists", 42 | "status": 400 43 | }, 44 | "UpdatingMovieError": { 45 | "message": "Updating movie added by other is forbidden", 46 | "status": 403 47 | }, 48 | "DeletingMovieError": { 49 | "message": "Deleting movie added by other is forbidden", 50 | "status": 403 51 | }, 52 | "MovieNotExistsError": { 53 | "message": "Movie with given id doesn't exists", 54 | "status": 400 55 | }, 56 | "EmailAlreadyExistsError": { 57 | "message": "User with given email address already exists", 58 | "status": 400 59 | }, 60 | "UnauthorizedError": { 61 | "message": "Invalid username or password", 62 | "status": 401 63 | }, 64 | "EmailDoesnotExistsError": { 65 | "message": "Couldn't find the user with given email address", 66 | "status": 400 67 | }, 68 | "BadTokenError": { 69 | "message": "Invalid token", 70 | "status": 403 71 | } 72 | } -------------------------------------------------------------------------------- /Part - 6/movie-bag/resources/movie.py: -------------------------------------------------------------------------------- 1 | from flask import Response, request 2 | from database.models import Movie, User 3 | from flask_jwt_extended import jwt_required, get_jwt_identity 4 | from flask_restful import Resource 5 | from mongoengine.errors import FieldDoesNotExist, NotUniqueError, DoesNotExist, ValidationError, InvalidQueryError 6 | from resources.errors import SchemaValidationError, MovieAlreadyExistsError, InternalServerError, \ 7 | UpdatingMovieError, DeletingMovieError, MovieNotExistsError 8 | 9 | 10 | class MoviesApi(Resource): 11 | def get(self): 12 | query = Movie.objects() 13 | movies = Movie.objects().to_json() 14 | return Response(movies, mimetype="application/json", status=200) 15 | 16 | @jwt_required 17 | def post(self): 18 | try: 19 | user_id = get_jwt_identity() 20 | body = request.get_json() 21 | user = User.objects.get(id=user_id) 22 | movie = Movie(**body, added_by=user) 23 | movie.save() 24 | user.update(push__movies=movie) 25 | user.save() 26 | id = movie.id 27 | return {'id': str(id)}, 200 28 | except (FieldDoesNotExist, ValidationError): 29 | raise SchemaValidationError 30 | except NotUniqueError: 31 | raise MovieAlreadyExistsError 32 | except Exception as e: 33 | raise InternalServerError 34 | 35 | 36 | class MovieApi(Resource): 37 | @jwt_required 38 | def put(self, id): 39 | try: 40 | user_id = get_jwt_identity() 41 | movie = Movie.objects.get(id=id, added_by=user_id) 42 | body = request.get_json() 43 | Movie.objects.get(id=id).update(**body) 44 | return '', 200 45 | except InvalidQueryError: 46 | raise SchemaValidationError 47 | except DoesNotExist: 48 | raise UpdatingMovieError 49 | except Exception: 50 | raise InternalServerError 51 | 52 | @jwt_required 53 | def delete(self, id): 54 | try: 55 | user_id = get_jwt_identity() 56 | movie = Movie.objects.get(id=id, added_by=user_id) 57 | movie.delete() 58 | return '', 200 59 | except DoesNotExist: 60 | raise DeletingMovieError 61 | except Exception: 62 | raise InternalServerError 63 | 64 | def get(self, id): 65 | try: 66 | movies = Movie.objects.get(id=id).to_json() 67 | return Response(movies, mimetype="application/json", status=200) 68 | except DoesNotExist: 69 | raise MovieNotExistsError 70 | except Exception: 71 | raise InternalServerError 72 | -------------------------------------------------------------------------------- /Part - 6/movie-bag/resources/reset_password.py: -------------------------------------------------------------------------------- 1 | from flask import request, render_template 2 | from flask_jwt_extended import create_access_token, decode_token 3 | from database.models import User 4 | from flask_restful import Resource 5 | import datetime 6 | from resources.errors import SchemaValidationError, InternalServerError, \ 7 | EmailDoesnotExistsError, BadTokenError 8 | from jwt.exceptions import ExpiredSignatureError, DecodeError, \ 9 | InvalidTokenError 10 | from services.mail_service import send_email 11 | 12 | class ForgotPassword(Resource): 13 | def post(self): 14 | url = request.host_url + 'reset/' 15 | try: 16 | body = request.get_json() 17 | email = body.get('email') 18 | if not email: 19 | raise SchemaValidationError 20 | 21 | user = User.objects.get(email=email) 22 | if not user: 23 | raise EmailDoesnotExistsError 24 | 25 | expires = datetime.timedelta(hours=24) 26 | reset_token = create_access_token(str(user.id), expires_delta=expires) 27 | 28 | return send_email('[Movie-bag] Reset Your Password', 29 | sender='support@movie-bag.com', 30 | recipients=[user.email], 31 | text_body=render_template('email/reset_password.txt', 32 | url=url + reset_token), 33 | html_body=render_template('email/reset_password.html', 34 | url=url + reset_token)) 35 | except SchemaValidationError: 36 | raise SchemaValidationError 37 | except EmailDoesnotExistsError: 38 | raise EmailDoesnotExistsError 39 | except Exception as e: 40 | raise InternalServerError 41 | 42 | 43 | class ResetPassword(Resource): 44 | def post(self): 45 | url = request.host_url + 'reset/' 46 | try: 47 | body = request.get_json() 48 | reset_token = body.get('reset_token') 49 | password = body.get('password') 50 | 51 | if not reset_token or not password: 52 | raise SchemaValidationError 53 | 54 | user_id = decode_token(reset_token)['identity'] 55 | 56 | user = User.objects.get(id=user_id) 57 | 58 | user.modify(password=password) 59 | user.hash_password() 60 | user.save() 61 | 62 | return send_email('[Movie-bag] Password reset successful', 63 | sender='support@movie-bag.com', 64 | recipients=[user.email], 65 | text_body='Password reset was successful', 66 | html_body='

Password reset was successful

') 67 | 68 | except SchemaValidationError: 69 | raise SchemaValidationError 70 | except ExpiredSignatureError: 71 | raise ExpiredTokenError 72 | except (DecodeError, InvalidTokenError): 73 | raise BadTokenError 74 | except Exception as e: 75 | raise InternalServerError -------------------------------------------------------------------------------- /Part - 6/movie-bag/resources/routes.py: -------------------------------------------------------------------------------- 1 | from .movie import MoviesApi, MovieApi 2 | from .auth import SignupApi, LoginApi 3 | from .reset_password import ForgotPassword, ResetPassword 4 | 5 | def initialize_routes(api): 6 | api.add_resource(MoviesApi, '/api/movies') 7 | api.add_resource(MovieApi, '/api/movies/') 8 | 9 | api.add_resource(SignupApi, '/api/auth/signup') 10 | api.add_resource(LoginApi, '/api/auth/login') 11 | 12 | api.add_resource(ForgotPassword, '/api/auth/forgot') 13 | api.add_resource(ResetPassword, '/api/auth/reset') 14 | -------------------------------------------------------------------------------- /Part - 6/movie-bag/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | app.run(port=3500) -------------------------------------------------------------------------------- /Part - 6/movie-bag/services/mail_service.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from flask_mail import Message 3 | 4 | from app import app 5 | from app import mail 6 | 7 | 8 | def send_async_email(app, msg): 9 | with app.app_context(): 10 | try: 11 | mail.send(msg) 12 | except ConnectionRefusedError: 13 | raise InternalServerError("[MAIL SERVER] not working") 14 | 15 | 16 | def send_email(subject, sender, recipients, text_body, html_body): 17 | msg = Message(subject, sender=sender, recipients=recipients) 18 | msg.body = text_body 19 | msg.html = html_body 20 | Thread(target=send_async_email, args=(app, msg)).start() -------------------------------------------------------------------------------- /Part - 6/movie-bag/templates/email/reset_password.html: -------------------------------------------------------------------------------- 1 |

Dear, User

2 |

3 | To reset your password 4 | 5 | click here 6 | . 7 |

8 |

Alternatively, you can paste the following link in your browser's address bar:

9 |

{{ url }}

10 |

If you have not requested a password reset simply ignore this message.

11 |

Sincerely

12 |

Movie-bag Support Team

-------------------------------------------------------------------------------- /Part - 6/movie-bag/templates/email/reset_password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paurakhsharma/flask-rest-api-blog-series/3ffdbfa303455b0b1044735f74dab36f08f0d3ca/Part - 6/movie-bag/templates/email/reset_password.txt -------------------------------------------------------------------------------- /Part - 6/movie-bag/tests/BaseCase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from app import app 4 | from database.db import db 5 | 6 | 7 | class BaseCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.app = app.test_client() 11 | self.db = db.get_db() 12 | 13 | 14 | def tearDown(self): 15 | # Delete Database collections after the test is complete 16 | for collection in self.db.list_collection_names(): 17 | self.db.drop_collection(collection) -------------------------------------------------------------------------------- /Part - 6/movie-bag/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paurakhsharma/flask-rest-api-blog-series/3ffdbfa303455b0b1044735f74dab36f08f0d3ca/Part - 6/movie-bag/tests/__init__.py -------------------------------------------------------------------------------- /Part - 6/movie-bag/tests/test_create_movie.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from tests.BaseCase import BaseCase 4 | 5 | class TestUserLogin(BaseCase): 6 | 7 | def test_successful_login(self): 8 | # Given 9 | email = "paurakh011@gmail.com" 10 | password = "mycoolpassword" 11 | user_payload = json.dumps({ 12 | "email": email, 13 | "password": password 14 | }) 15 | 16 | self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=user_payload) 17 | response = self.app.post('/api/auth/login', headers={"Content-Type": "application/json"}, data=user_payload) 18 | login_token = response.json['token'] 19 | 20 | movie_payload = { 21 | "name": "Star Wars: The Rise of Skywalker", 22 | "casts": ["Daisy Ridley", "Adam Driver"], 23 | "genres": ["Fantasy", "Sci-fi"] 24 | } 25 | # When 26 | response = self.app.post('/api/movies', 27 | headers={"Content-Type": "application/json", "Authorization": f"Bearer {login_token}"}, 28 | data=json.dumps(movie_payload)) 29 | 30 | # Then 31 | self.assertEqual(str, type(response.json['id'])) 32 | self.assertEqual(200, response.status_code) -------------------------------------------------------------------------------- /Part - 6/movie-bag/tests/test_get_movies.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | 4 | from tests.BaseCase import BaseCase 5 | 6 | class TestGetMovies(BaseCase): 7 | 8 | def test_empty_response(self): 9 | response = self.app.get('/api/movies') 10 | self.assertListEqual(response.json, []) 11 | self.assertEqual(response.status_code, 200) 12 | 13 | def test_movie_response(self): 14 | # Given 15 | email = "paurakh011@gmail.com" 16 | password = "mycoolpassword" 17 | user_payload = json.dumps({ 18 | "email": email, 19 | "password": password 20 | }) 21 | 22 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=user_payload) 23 | user_id = response.json['id'] 24 | response = self.app.post('/api/auth/login', headers={"Content-Type": "application/json"}, data=user_payload) 25 | login_token = response.json['token'] 26 | 27 | movie_payload = { 28 | "name": "Star Wars: The Rise of Skywalker", 29 | "casts": ["Daisy Ridley", "Adam Driver"], 30 | "genres": ["Fantasy", "Sci-fi"] 31 | } 32 | response = self.app.post('/api/movies', 33 | headers={"Content-Type": "application/json", "Authorization": f"Bearer {login_token}"}, 34 | data=json.dumps(movie_payload)) 35 | 36 | # When 37 | response = self.app.get('/api/movies') 38 | added_movie = response.json[0] 39 | 40 | # Then 41 | self.assertEqual(movie_payload['name'], added_movie['name']) 42 | self.assertEqual(movie_payload['casts'], added_movie['casts']) 43 | self.assertEqual(movie_payload['genres'], added_movie['genres']) 44 | self.assertEqual(user_id, added_movie['added_by']['$oid']) 45 | self.assertEqual(200, response.status_code) -------------------------------------------------------------------------------- /Part - 6/movie-bag/tests/test_login.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from tests.BaseCase import BaseCase 4 | 5 | class TestUserLogin(BaseCase): 6 | 7 | def test_successful_login(self): 8 | # Given 9 | email = "paurakh011@gmail.com" 10 | password = "mycoolpassword" 11 | payload = json.dumps({ 12 | "email": email, 13 | "password": password 14 | }) 15 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 16 | 17 | # When 18 | response = self.app.post('/api/auth/login', headers={"Content-Type": "application/json"}, data=payload) 19 | 20 | # Then 21 | self.assertEqual(str, type(response.json['token'])) 22 | self.assertEqual(200, response.status_code) 23 | 24 | def test_login_with_invalid_email(self): 25 | # Given 26 | email = "paurakh011@gmail.com" 27 | password = "mycoolpassword" 28 | payload = { 29 | "email": email, 30 | "password": password 31 | } 32 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=json.dumps(payload)) 33 | 34 | # When 35 | payload['email'] = "paurakh012@gmail.com" 36 | response = self.app.post('/api/auth/login', headers={"Content-Type": "application/json"}, data=json.dumps(payload)) 37 | 38 | # Then 39 | self.assertEqual("Invalid username or password", response.json['message']) 40 | self.assertEqual(401, response.status_code) 41 | 42 | def test_login_with_invalid_password(self): 43 | # Given 44 | email = "paurakh011@gmail.com" 45 | password = "mycoolpassword" 46 | payload = { 47 | "email": email, 48 | "password": password 49 | } 50 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=json.dumps(payload)) 51 | 52 | # When 53 | payload['password'] = "myverycoolpassword" 54 | response = self.app.post('/api/auth/login', headers={"Content-Type": "application/json"}, data=json.dumps(payload)) 55 | 56 | # Then 57 | self.assertEqual("Invalid username or password", response.json['message']) 58 | self.assertEqual(401, response.status_code) -------------------------------------------------------------------------------- /Part - 6/movie-bag/tests/test_signup.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from tests.BaseCase import BaseCase 4 | 5 | class TestUserSignup(BaseCase): 6 | 7 | def test_successful_signup(self): 8 | # Given 9 | payload = json.dumps({ 10 | "email": "paurakh011@gmail.com", 11 | "password": "mycoolpassword" 12 | }) 13 | 14 | # When 15 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 16 | 17 | # Then 18 | self.assertEqual(str, type(response.json['id'])) 19 | self.assertEqual(200, response.status_code) 20 | 21 | def test_signup_with_non_existing_field(self): 22 | #Given 23 | payload = json.dumps({ 24 | "username": "mycoolusername", 25 | "email": "paurakh011@gmail.com", 26 | "password": "mycoolpassword" 27 | }) 28 | 29 | #When 30 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 31 | 32 | # Then 33 | self.assertEqual('Request is missing required fields', response.json['message']) 34 | self.assertEqual(400, response.status_code) 35 | 36 | def test_signup_without_email(self): 37 | #Given 38 | payload = json.dumps({ 39 | "password": "mycoolpassword", 40 | }) 41 | 42 | #When 43 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 44 | 45 | # Then 46 | self.assertEqual('Something went wrong', response.json['message']) 47 | self.assertEqual(500, response.status_code) 48 | 49 | def test_signup_without_password(self): 50 | #Given 51 | payload = json.dumps({ 52 | "email": "paurakh011@gmail.com", 53 | }) 54 | 55 | #When 56 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 57 | 58 | # Then 59 | self.assertEqual('Something went wrong', response.json['message']) 60 | self.assertEqual(500, response.status_code) 61 | 62 | def test_creating_already_existing_user(self): 63 | #Given 64 | payload = json.dumps({ 65 | "email": "paurakh011@gmail.com", 66 | "password": "mycoolpassword" 67 | }) 68 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 69 | 70 | # When 71 | response = self.app.post('/api/auth/signup', headers={"Content-Type": "application/json"}, data=payload) 72 | 73 | # Then 74 | self.assertEqual('User with given email address already exists', response.json['message']) 75 | self.assertEqual(400, response.status_code) --------------------------------------------------------------------------------