├── app ├── api │ ├── __init__.py │ ├── movies.py │ └── users.py ├── __init__.py ├── models.py └── swagger │ └── swagger.json ├── screenshots ├── swagger.png └── Netflix_Microservices.png ├── run.py ├── Dockerfile ├── requirements.txt ├── tests ├── test_trending_now_api.py ├── test_user_api.py ├── test_movies_api.py └── test_mock_data.py ├── LICENSE ├── config.py └── README.md /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/python-flask-microservices/main/screenshots/swagger.png -------------------------------------------------------------------------------- /screenshots/Netflix_Microservices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/python-flask-microservices/main/screenshots/Netflix_Microservices.png -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from app import initialize_app 2 | 3 | app = initialize_app() 4 | if __name__=='__main__': 5 | app.run(debug=True) # Do not use debug=True in production -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | RUN mkdir /app 3 | WORKDIR /app 4 | ADD requirements.txt /app 5 | RUN pip3 install -r requirements.txt 6 | CMD ["python3", "run.py"] 7 | EXPOSE 5000 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.2 2 | certifi==2020.11.8 3 | chardet==3.0.4 4 | click==7.1.2 5 | Flask-JWT==0.3.2 6 | Flask-JWT-Extended==3.25.0 7 | flask-mongoengine==0.7.1 8 | Flask-WTF==0.14.3 9 | gunicorn==19.10.0 10 | idna==2.10 11 | itsdangerous==1.1.0 12 | Jinja2==2.11.2 13 | MarkupSafe==1.1.1 14 | mongoengine==0.19.1 15 | PyJWT==1.7.1 16 | pymongo==3.11.1 17 | python-dotenv==0.15.0 18 | requests==2.25.0 19 | six==1.15.0 20 | typing==3.7.4.3 21 | urllib3==1.26.2 22 | Werkzeug==1.0.1 23 | WTForms==2.3.3 24 | -------------------------------------------------------------------------------- /tests/test_trending_now_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import requests 3 | from config import Config 4 | from tests.test_mock_data import MockData 5 | 6 | class TestTrendingNowMicroServices(unittest.TestCase): 7 | 8 | def test_trending_now(self): 9 | trending_now_api_endpoint = '{}{}'.format( 10 | Config.BASE_URL, MockData.mock_trending_now_endpoint) 11 | response = requests.get(trending_now_api_endpoint, 12 | headers=MockData.mock_request_headers) 13 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anshuman Pattnaik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | # Mongo db connection url 3 | MONGO_DB_CONNECTION = 'mongodb://127.0.0.1:27017/netflix' 4 | 5 | # API Configs 6 | API_PATH = '/api/' 7 | API_VERSION = 'v1' 8 | BASE_URL = 'http://127.0.0.1:5000' 9 | 10 | # User API endpoint 11 | SIGN_IN = API_PATH+API_VERSION+'/sign_in' 12 | SIGN_UP = API_PATH+API_VERSION+'/sign_up' 13 | USER_PROFILE = API_PATH+API_VERSION+'/profile/' 14 | UPDATE_PROFILE = API_PATH+API_VERSION+'/update_profile/' 15 | CHANGE_PASSWORD = API_PATH+API_VERSION+'/change_password/' 16 | DELETE_ACCOUNT = API_PATH+API_VERSION+'/delete_account/' 17 | 18 | # Movies API endpoint 19 | TRENDING_NOW = API_PATH+API_VERSION+'/trending_now/' 20 | FETCH_MOVIES = API_PATH+API_VERSION+'/fetch_movies/' 21 | ADD_MOVIE = API_PATH+API_VERSION+'/add_movie/' 22 | SEARCH_MOVIE = API_PATH+API_VERSION+'/search_movie//' 23 | DELETE_MOVIE = API_PATH+API_VERSION+'/delete_movie//' 24 | ADD_TO_FAVOURITE = API_PATH+API_VERSION+'/add_to_favourite//' 25 | FAVOURITE_MOVIES = API_PATH+API_VERSION+'/favourite_movies/' 26 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect 2 | from flask.helpers import send_from_directory 3 | from config import Config 4 | from flask_mongoengine import MongoEngine 5 | from flask_swagger_ui import get_swaggerui_blueprint 6 | from flask_jwt_extended import JWTManager 7 | 8 | db = MongoEngine() 9 | 10 | # Flask Initialization 11 | app = Flask(__name__) 12 | 13 | # Initialize jwt manager using the secret key 14 | app.config['JWT_SECRET_KEY'] = 'python-flask-microservices' 15 | jwt = JWTManager(app) 16 | 17 | # Redirect to swagger-api-docs 18 | @app.route('/') 19 | def redirect_to_docs(): 20 | return redirect("http://127.0.0.1:5000/api/docs") 21 | 22 | # Swagger api docs route 23 | @app.route('/swagger/') 24 | def send_static(path): 25 | return send_from_directory('swagger', path) 26 | 27 | def initialize_app(): 28 | SWAGGER_URL = '/api/docs' 29 | API_URL = '/swagger/swagger.json' 30 | 31 | swaggerui_blueprint = get_swaggerui_blueprint( 32 | SWAGGER_URL, 33 | API_URL, 34 | config={ 35 | 'app_name': "Python-Flask Netflix Microservices" 36 | }) 37 | app.register_blueprint(swaggerui_blueprint) 38 | 39 | app.config['MONGODB_SETTINGS'] = { 40 | 'host': Config.MONGO_DB_CONNECTION, 41 | } 42 | db.init_app(app) 43 | 44 | from app.api.movies import movies_bp 45 | app.register_blueprint(movies_bp) 46 | 47 | from app.api.users import users_bp 48 | app.register_blueprint(users_bp) 49 | 50 | return app 51 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from . import db 2 | 3 | class Thumbnails(db.Document): 4 | type = db.StringField(required=True) 5 | client = db.StringField(required=True) 6 | size = db.StringField(required=True) 7 | path = db.StringField(required=True) 8 | 9 | 10 | class Media(db.Document): 11 | type = db.StringField(required=True) 12 | client = db.StringField(required=True) 13 | path = db.StringField(required=True) 14 | 15 | 16 | class Subtitles(db.Document): 17 | language = db.StringField(required=True) 18 | path = db.StringField(required=True) 19 | 20 | 21 | class Movies(db.Document): 22 | title = db.StringField(required=True) 23 | movie_type = db.StringField(required=True) 24 | ratings = db.IntField(required=True) 25 | duration = db.IntField(required=True) 26 | age_restriction = db.StringField(required=True) 27 | timestamp = db.IntField(required=True) 28 | thumbnails = db.ListField(db.ReferenceField(Thumbnails)) 29 | media = db.ListField(db.ReferenceField(Media)) 30 | subtitles = db.ListField(db.ReferenceField(Subtitles)) 31 | description = db.StringField(required=True) 32 | cast = db.ListField(db.StringField(required=True)) 33 | genres = db.ListField(db.StringField(required=True)) 34 | category = db.StringField(required=True) 35 | production = db.StringField(required=True) 36 | country = db.StringField(required=True) 37 | is_favourite = db.BooleanField(required=True) 38 | 39 | 40 | class Users(db.Document): 41 | username = db.StringField(required=True) 42 | password = db.StringField(required=True) 43 | name = db.StringField(required=True) 44 | email = db.StringField(required=True) 45 | dob = db.StringField(required=True) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Python Flask Microservices 2 | The idea behind this application is to demonstrate the microservices architecture of today's modern system. In this demo, I have tried to show the basic microservices REST-API concepts of a tech & entertainment industry (i.e [Netflix](https://netflix.com)), and if we look at the system design of Netflix, it runs around more than 10,000+ microservices to manage the entire system, so in system-design, it's quite important to understand this concept. 3 | 4 | 5 | 6 | ## Technical Overview 7 | The Proof of Concept written using python and it uses a flask web framework to define the routes and to store the data in the server it uses mongodb database, and for authentication, it uses [JWT Token](https://jwt.io/) framework. 8 | 9 | In this project, you will find three different types of microservices. 10 | 11 | 1. Users 12 | 2. Movies 13 | 3. Trending Now 14 | 15 | ### Swagger API Documentation 16 | The Swagger API docs can be accessible via [http://127.0.0.1:5000/api/docs](http://127.0.0.1:5000/api/docs) and to test the API endpoints you need to authorize yourself using your jwt access token. 17 | 18 | 19 | 20 | ### Installation 21 | `````````````````````````````````````````````````````````````````````````````````` 22 | git clone https://github.com/anshumanpattnaik/python-flask-microservices 23 | cd python-flask-microservices 24 | pip install -r requirements.txt 25 | source venv/bin/activate 26 | python3 run.py 27 | 28 | Open http://127.0.0.1:5000 to view in the browser. 29 | ``````````````````````````````````````````````````````````````````````````````````` 30 | 31 | ### Build and run docker image 32 | 33 | ``````````````````````````````````````````````````````` 34 | docker build -t python-flask-microservices . 35 | ``````````````````````````````````````````````````````` 36 | 37 | ``````````````````````````````````````````````````````````````````````````````` 38 | docker run -it --name python-container -p 5000:5000 python-flask-microservices 39 | ``````````````````````````````````````````````````````````````````````````````` 40 | 41 | ### License 42 | This project is licensed under the [MIT License](LICENSE) 43 | -------------------------------------------------------------------------------- /tests/test_user_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import requests 3 | from config import Config 4 | from tests.test_mock_data import MockData 5 | 6 | class TestUserMicroServices(unittest.TestCase): 7 | 8 | def test_sign_up(self): 9 | sign_up_api_endpoint = '{}{}'.format(Config.BASE_URL, Config.SIGN_UP) 10 | response = requests.post(sign_up_api_endpoint, 11 | json=MockData.mock_user_data) 12 | self.assertEqual(response.status_code, 201) 13 | 14 | def test_sign_in(self): 15 | sign_in_api_endpoint = '{}{}'.format(Config.BASE_URL, Config.SIGN_IN) 16 | response = requests.post(sign_in_api_endpoint, 17 | json=MockData.mock_sign_in_data) 18 | self.assertEqual(response.status_code, 200) 19 | 20 | def test_profile(self): 21 | profile_api_endpoint = '{}{}'.format( 22 | Config.BASE_URL, MockData.mock_profile_endpoint) 23 | response = requests.get(profile_api_endpoint, 24 | headers=MockData.mock_request_headers) 25 | self.assertEqual(response.status_code, 200) 26 | 27 | def test_update_profile(self): 28 | update_profile_api_endpoint = '{}{}'.format( 29 | Config.BASE_URL, MockData.mock_update_profile_endpoint) 30 | 31 | response = requests.put(update_profile_api_endpoint, 32 | headers=MockData.mock_request_headers, json=MockData.mock_update_user_data) 33 | self.assertEqual(response.status_code, 200) 34 | 35 | def test_change_password(self): 36 | change_password_api_endpoint = '{}{}'.format( 37 | Config.BASE_URL, MockData.mock_change_password_endpoint) 38 | 39 | response = requests.put(change_password_api_endpoint, 40 | headers=MockData.mock_request_headers, json=MockData.mock_change_password_data) 41 | self.assertEqual(response.status_code, 200) 42 | 43 | def test_delete_account(self): 44 | delete_account_api_endpoint = '{}{}'.format( 45 | Config.BASE_URL, MockData.mock_delete_account_endpoint) 46 | 47 | response = requests.delete(delete_account_api_endpoint, 48 | headers=MockData.mock_request_headers) 49 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /tests/test_movies_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import requests 3 | from config import Config 4 | from tests.test_mock_data import MockData 5 | 6 | class TestMoviesMicroServices(unittest.TestCase): 7 | 8 | def test_add_movie(self): 9 | add_movie_api_endpoint = '{}{}'.format( 10 | Config.BASE_URL, MockData.mock_add_movie_endpoint) 11 | response = requests.post(add_movie_api_endpoint, 12 | headers=MockData.mock_request_headers, 13 | json=MockData.mock_movie_data) 14 | self.assertEqual(response.status_code, 201) 15 | 16 | def test_fetch_movies(self): 17 | fetch_movies_api_endpoint = '{}{}'.format( 18 | Config.BASE_URL, MockData.mock_fetch_movies_endpoint) 19 | response = requests.get(fetch_movies_api_endpoint, 20 | headers=MockData.mock_request_headers) 21 | self.assertEqual(response.status_code, 200) 22 | 23 | def test_search_movie(self): 24 | search_movie_api_endpoint = '{}{}'.format( 25 | Config.BASE_URL, MockData.mock_search_movie_endpoint) 26 | response = requests.get(search_movie_api_endpoint, 27 | headers=MockData.mock_request_headers) 28 | self.assertEqual(response.status_code, 200) 29 | 30 | def test_delete_movie(self): 31 | delete_movie_api_endpoint = '{}{}'.format( 32 | Config.BASE_URL, MockData.mock_delete_movie_endpoint) 33 | response = requests.delete(delete_movie_api_endpoint, 34 | headers=MockData.mock_request_headers) 35 | self.assertEqual(response.status_code, 200) 36 | 37 | def test_favourite_movies(self): 38 | favourite_movie_api_endpoint = '{}{}'.format( 39 | Config.BASE_URL, MockData.mock_favourite_movies_endpoint) 40 | response = requests.get(favourite_movie_api_endpoint, 41 | headers=MockData.mock_request_headers) 42 | self.assertEqual(response.status_code, 200) 43 | 44 | def test_add_to_favourite(self): 45 | add_to_favourite_movie_api_endpoint = '{}{}'.format( 46 | Config.BASE_URL, MockData.mock_add_to_favourite_movie_endpoint) 47 | response = requests.put(add_to_favourite_movie_api_endpoint, 48 | headers=MockData.mock_request_headers, 49 | json=MockData.mock_favourite_data) 50 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /tests/test_mock_data.py: -------------------------------------------------------------------------------- 1 | class MockData: 2 | mock_jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MDY2NTI0NTgsIm5iZiI6MTYwNjY1MjQ1OCwianRpIjoiMzIyYTg5ZWEtNjcyMC00YmY0LTg4ODQtNzI1Mzk2YzQ4ZTE3IiwiZXhwIjoxNjA2NjUzMzU4LCJpZGVudGl0eSI6InRlc3QxMTEiLCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.82uDJ3F-5-6ub-uTOQ4YI4r8RZMaTfHcd_zxMYIRNG0" 3 | mock_request_headers = { 4 | 'Content-type': 'application/json', 5 | 'Authorization': 'Bearer '+mock_jwt_token 6 | } 7 | 8 | mock_sign_in_data = { 9 | "username": "test111", 10 | "password": "Test@#123" 11 | } 12 | 13 | mock_user_data = { 14 | "username": "test111", 15 | "password": "Test@#123", 16 | "name": "Test Account", 17 | "email": "test@gmail.com", 18 | "dob": "14/05/1995" 19 | } 20 | 21 | mock_update_user_data = { 22 | "name": "Updated Test Account" 23 | } 24 | 25 | mock_change_password_data = { 26 | "old_password": "Test@#123", 27 | "new_password": "Test@#456" 28 | } 29 | 30 | mock_movie_data = { 31 | "age_restriction": "16+", 32 | "cast": [ 33 | "Harrison Ford", 34 | "Sean Connery", 35 | "Denholm Elliott" 36 | ], 37 | "category": "Action", 38 | "country": "US", 39 | "description": "This time he's after the Holy Grail. But the Nazis and snakes are back, and his dad's not making things any easier.", 40 | "duration": 7620000, 41 | "genres": [ 42 | "Harrison Ford", 43 | "Sean Connery", 44 | "Denholm Elliott" 45 | ], 46 | "is_favourite": False, 47 | "media": [{ 48 | "client": "Desktop", 49 | "path": "https://x.mp4", 50 | "type": ".mp4" 51 | }], 52 | "movie_type": "Exciting", 53 | "production": "Hollywood", 54 | "ratings": 96, 55 | "subtitles": [{ 56 | "language": "eng", 57 | "path": "https://x.txt" 58 | }], 59 | "thumbnails": [{ 60 | "client": "Desktop", 61 | "path": "https://images-na.ssl-images-amazon.com/images/I/81UOBSDQh0L._AC_SY741_.jpg", 62 | "size": "273x109", 63 | "type": "JPEG" 64 | }], 65 | "timestamp": 628193339, 66 | "title": "Test Indiana Jones" 67 | } 68 | 69 | mock_favourite_data = { 70 | "is_favourite": True 71 | } 72 | 73 | mock_user = 'test111' 74 | mock_api_version = '/api/v1' 75 | 76 | # Mock User API endpoints 77 | mock_profile_endpoint = mock_api_version+'/profile/'+mock_user 78 | mock_update_profile_endpoint = mock_api_version+'/update_profile/'+mock_user 79 | mock_change_password_endpoint = mock_api_version+'/change_password/'+mock_user 80 | mock_delete_account_endpoint = mock_api_version+'/delete_account/'+mock_user 81 | 82 | # Mock Movies api endpoints 83 | mock_movie_title = 'Test Indiana Jones' 84 | mock_add_movie_endpoint = mock_api_version+'/add_movie/'+mock_user 85 | mock_fetch_movies_endpoint = mock_api_version+'/fetch_movies/'+mock_user 86 | mock_search_movie_endpoint = mock_api_version +'/search_movie/'+mock_movie_title+'/'+mock_user 87 | mock_delete_movie_endpoint = mock_api_version +'/delete_movie/'+mock_movie_title+'/'+mock_user 88 | mock_favourite_movies_endpoint = mock_api_version +'/favourite_movies/'+mock_user 89 | mock_add_to_favourite_movie_endpoint = mock_api_version +'/add_to_favourite/'+mock_movie_title+'/'+mock_user 90 | mock_trending_now_endpoint = mock_api_version +'/trending_now/'+mock_user -------------------------------------------------------------------------------- /app/api/movies.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, make_response, jsonify, request 2 | from flask_jwt_extended import (jwt_required, get_jwt_identity) 3 | from werkzeug.exceptions import abort 4 | from ..models import Movies 5 | from config import Config 6 | 7 | movies_bp = Blueprint('movies', __name__) 8 | 9 | # Fetches list of movies based on trendings which has (> 95%) users ratings 10 | @movies_bp.route(Config.TRENDING_NOW, methods=['GET']) 11 | @jwt_required 12 | def trending_now(username): 13 | if get_jwt_identity() == username: 14 | movies = Movies.objects() 15 | trending = [] 16 | for movie in movies: 17 | # Checking if movie has more than 95% ratings 18 | if movie.ratings > 95: 19 | trending.append(movie) 20 | return make_response(jsonify(trending), 200) 21 | else: 22 | abort(401) 23 | 24 | # Fetches list of movies based on username 25 | @movies_bp.route(Config.FETCH_MOVIES, methods=['GET']) 26 | @jwt_required 27 | def fetch_movies(username): 28 | if get_jwt_identity() == username: 29 | return make_response(jsonify(Movies.objects()), 200) 30 | else: 31 | abort(401) 32 | 33 | # User can search for movie based on the title 34 | @movies_bp.route(Config.SEARCH_MOVIE, methods=['GET']) 35 | @jwt_required 36 | def search_movie(username, title): 37 | if get_jwt_identity() == username: 38 | try: 39 | return make_response(jsonify(Movies.objects.get(title=title)), 200) 40 | except Movies.DoesNotExist: 41 | abort(404) 42 | else: 43 | abort(401) 44 | 45 | # User can delete movie based on the title 46 | @movies_bp.route(Config.DELETE_MOVIE, methods=['DELETE']) 47 | @jwt_required 48 | def delete_movie(username, title): 49 | if get_jwt_identity() == username: 50 | movie = Movies.objects(title=title).first() 51 | # Abort if no movie 52 | if movie == None: 53 | abort(404) 54 | 55 | movie.delete() 56 | return make_response(jsonify({ 57 | "success": 'Movie Deleted Successfully' 58 | }), 200) 59 | else: 60 | abort(401) 61 | 62 | # User can add/remove movies as per their favourites 63 | @movies_bp.route(Config.ADD_TO_FAVOURITE, methods=['PUT']) 64 | @jwt_required 65 | def add_to_favourite(username, title): 66 | if get_jwt_identity() == username: 67 | movie = Movies.objects(title=title).first() 68 | # Abort if no movie found 69 | if movie == None: 70 | abort(404) 71 | 72 | movie.update(is_favourite=request.json['is_favourite']) 73 | 74 | if request.json['is_favourite']: 75 | message = title+' has been added to your favourite' 76 | else: 77 | message = title+' has been removed from your favourite' 78 | 79 | return make_response(jsonify({ 80 | "success": message 81 | }), 200) 82 | else: 83 | abort(401) 84 | 85 | # Fetches list of favourite movies based on the username 86 | @movies_bp.route(Config.FAVOURITE_MOVIES, methods=['GET']) 87 | @jwt_required 88 | def favourite_movies(username): 89 | if get_jwt_identity() == username: 90 | return make_response(jsonify(Movies.objects(is_favourite=True)), 200) 91 | else: 92 | abort(401) 93 | 94 | # User can add new movie into the database 95 | @movies_bp.route(Config.ADD_MOVIE, methods=['POST']) 96 | @jwt_required 97 | def add_movie(username): 98 | if get_jwt_identity() == username: 99 | try: 100 | movies = Movies(title=request.json['title'], 101 | movie_type=request.json['movie_type'], 102 | ratings=request.json['ratings'], 103 | duration=request.json['duration'], 104 | age_restriction=request.json['age_restriction'], 105 | timestamp=request.json['timestamp'], 106 | thumbnails=request.json['thumbnails'], 107 | media=request.json['media'], 108 | subtitles=request.json['subtitles'], 109 | description=request.json['description'], 110 | cast=request.json['cast'], 111 | genres=request.json['genres'], 112 | category=request.json['category'], 113 | production=request.json['production'], 114 | country=request.json['country'], 115 | is_favourite=request.json['is_favourite']) 116 | movies.save() 117 | except KeyError: 118 | abort(400) 119 | 120 | return make_response(jsonify({ 121 | "success": 'Movies Addedd Successfully' 122 | }), 201) 123 | else: 124 | abort(401) 125 | 126 | 127 | @movies_bp.errorhandler(400) 128 | def invalid_request(error): 129 | return make_response(jsonify({'error': 'Invalid Request '+error})) 130 | 131 | 132 | @movies_bp.errorhandler(404) 133 | def not_found(error): 134 | return make_response(jsonify({'error': 'Sorry movie not found'}), 404) 135 | 136 | 137 | @movies_bp.errorhandler(401) 138 | def unauthorized(error): 139 | return make_response(jsonify({'error': 'Unauthorized Access'}), 401) 140 | -------------------------------------------------------------------------------- /app/api/users.py: -------------------------------------------------------------------------------- 1 | import re 2 | from flask import Blueprint, make_response, jsonify, request 3 | from flask_jwt_extended import ( 4 | jwt_required, create_access_token, get_jwt_identity) 5 | from werkzeug.exceptions import abort 6 | from ..models import Users 7 | from config import Config 8 | 9 | users_bp = Blueprint('users', __name__) 10 | 11 | error_pwd_validation_msg = 'Password must contain at least 6 characters, including Upper/Lowercase, special characters and numbers' 12 | 13 | # Fetches user profile details based on the username 14 | @users_bp.route(Config.USER_PROFILE, methods=['GET']) 15 | @jwt_required 16 | def user_profile(username): 17 | if get_jwt_identity() == username: 18 | user = Users.objects.get(username=username) 19 | return make_response(jsonify({ 20 | 'name': user.name, 21 | 'email': user.email, 22 | 'dob': user.dob 23 | }), 200) 24 | else: 25 | abort(401) 26 | 27 | # User can update their user profile based on their username 28 | @users_bp.route(Config.UPDATE_PROFILE, methods=['PUT']) 29 | @jwt_required 30 | def update_profile(username): 31 | if get_jwt_identity() == username: 32 | user = Users.objects(username=username).first() 33 | if 'name' in request.json: 34 | user.update(name=request.json['name']) 35 | 36 | if 'email' in request.json: 37 | user.update(name=request.json['email']) 38 | 39 | if 'dob' in request.json: 40 | user.update(dob=request.json['dob']) 41 | 42 | return make_response(jsonify({ 43 | 'success': 'User profile updated successfully' 44 | }), 200) 45 | else: 46 | abort(401) 47 | 48 | # User can change password based on their username 49 | @users_bp.route(Config.CHANGE_PASSWORD, methods=['PUT']) 50 | @jwt_required 51 | def change_password(username): 52 | if get_jwt_identity() == username: 53 | if 'old_password' in request.json and 'new_password' in request.json: 54 | user = Users.objects(username=username).first() 55 | 56 | old_password = request.json['old_password'] 57 | new_password = request.json['new_password'] 58 | 59 | if old_password == user.password: 60 | if password_validation(new_password) == None: 61 | return make_response(jsonify({"password_validation": error_pwd_validation_msg}), 400) 62 | user.update(password=new_password) 63 | else: 64 | return make_response(jsonify({'success': "Old password doesn't matched with the current password"}), 200) 65 | return make_response(jsonify({'success': 'Password changed successfully'}), 200) 66 | else: 67 | return make_response(jsonify({'error': 'Missing Fields'}), 400) 68 | else: 69 | abort(401) 70 | 71 | # User can delete their account based on their username 72 | @users_bp.route(Config.DELETE_ACCOUNT, methods=['DELETE']) 73 | @jwt_required 74 | def delete_account(username): 75 | if get_jwt_identity() == username: 76 | user = Users.objects(username=username).first() 77 | # Abort if no movie 78 | if user == None: 79 | abort(404) 80 | 81 | user.delete() 82 | return make_response(jsonify({ 83 | "success": 'Account Deleted Successfully' 84 | }), 200) 85 | else: 86 | abort(401) 87 | 88 | # User authenication as per their credentials 89 | @users_bp.route(Config.SIGN_IN, methods=['POST']) 90 | def sign_in(): 91 | try: 92 | username = request.json['username'] 93 | password = request.json['password'] 94 | 95 | user = Users.objects.get(username=username, password=password) 96 | access_token = create_access_token(identity=username) 97 | 98 | return make_response(jsonify({ 99 | 'access_token': access_token, 100 | 'name': user.name, 101 | 'email': user.email, 102 | 'dob': user.dob 103 | }), 200) 104 | except Users.DoesNotExist: 105 | return make_response(jsonify({ 106 | 'error': 'Incorrect Username or Password' 107 | }), 401) 108 | 109 | # Create new account with the user details 110 | @users_bp.route(Config.SIGN_UP, methods=['POST']) 111 | def sign_up(): 112 | try: 113 | username = request.json['username'] 114 | try: 115 | if Users.objects.get(username=username): 116 | return make_response(jsonify({"username": username+' username already exists'}), 400) 117 | except Users.DoesNotExist: 118 | pass 119 | 120 | email = request.json['email'] 121 | if email_validation(email) == None: 122 | return make_response(jsonify({"email_validation": email+' is not a valid email address'}), 400) 123 | 124 | password = request.json['password'] 125 | if password_validation(password) == None: 126 | return make_response(jsonify({"password_validation": error_pwd_validation_msg}), 400) 127 | 128 | users = Users(username=username, 129 | password=password, 130 | name=request.json['name'], 131 | email=email, 132 | dob=request.json['dob']) 133 | users.save() 134 | except KeyError: 135 | abort(400) 136 | return make_response(jsonify({ 137 | "success": 'User Created Successfully' 138 | }), 201) 139 | 140 | @users_bp.errorhandler(400) 141 | def invalid_request(error): 142 | return make_response(jsonify({'error': 'Invalid Request '+error}), 400) 143 | 144 | 145 | @users_bp.errorhandler(404) 146 | def not_found(error): 147 | return make_response(jsonify({'error': 'Sorry user not found'}), 404) 148 | 149 | 150 | @users_bp.errorhandler(401) 151 | def unauthorized(error): 152 | return make_response(jsonify({'error': 'Unauthorized Access'}), 401) 153 | 154 | # Utility method to validate the password 155 | def password_validation(password): 156 | pwd_regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!#%*?&]{6,20}$" 157 | pwd_pattern = re.compile(pwd_regex) 158 | password_regex_match = re.search(pwd_pattern, password) 159 | return password_regex_match 160 | 161 | # Utility method to validate the email address 162 | def email_validation(email): 163 | email_regex = '^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$' 164 | email_pattern = re.compile(email_regex) 165 | email_regex_match = re.search(email_pattern, email) 166 | return email_regex_match 167 | -------------------------------------------------------------------------------- /app/swagger/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "0.0.1", 5 | "title": "Python Flask Microservices", 6 | "description": "This is the sample demonstration of python flask microservices which follows Netflix api architecture.", 7 | "contact": { 8 | "name": "Anshuman Pattnaik", 9 | "url": "https://hackbotone.com" 10 | } 11 | }, 12 | "servers": [ 13 | { 14 | "url": "http://127.0.0.1:5000/api/v1" 15 | } 16 | ], 17 | "tags": [ 18 | { 19 | "name": "User", 20 | "description": "Create your profile" 21 | }, 22 | { 23 | "name": "Movies", 24 | "description": "Access to your movies" 25 | }, 26 | { 27 | "name": "Trending Now", 28 | "description": "Popular Movies based on trending" 29 | } 30 | ], 31 | "paths": { 32 | "/sign_up": { 33 | "post": { 34 | "tags": [ 35 | "User" 36 | ], 37 | "summary": "Create a user account and get access to the millions of movies database", 38 | "requestBody": { 39 | "description": "", 40 | "required": true, 41 | "content": { 42 | "application/json": { 43 | "schema": { 44 | "$ref": "#/definitions/User" 45 | } 46 | } 47 | } 48 | }, 49 | "responses": { 50 | "201": { 51 | "description": "User Created Successfully" 52 | }, 53 | "400": { 54 | "description": "Invalid Request" 55 | } 56 | } 57 | } 58 | }, 59 | "/sign_in": { 60 | "post": { 61 | "tags": [ 62 | "User" 63 | ], 64 | "summary": "Authenticate yourself by using your username and password", 65 | "requestBody": { 66 | "description": "", 67 | "required": true, 68 | "content": { 69 | "application/json": { 70 | "schema": { 71 | "$ref": "#/definitions/UserLogin" 72 | } 73 | } 74 | } 75 | }, 76 | "responses": { 77 | "201": { 78 | "schema": { 79 | "type": "object", 80 | "items": { 81 | "$ref": "#/definitions/UserLoggedIn" 82 | } 83 | } 84 | }, 85 | "401": { 86 | "description": "Incorrect Username or Password" 87 | } 88 | } 89 | } 90 | }, 91 | "/profile/{username}": { 92 | "get": { 93 | "tags": [ 94 | "User" 95 | ], 96 | "security": [ 97 | { 98 | "bearerAuth": [] 99 | } 100 | ], 101 | "summary": "Find your profile by username", 102 | "produces": [ 103 | "application/json" 104 | ], 105 | "parameters": [ 106 | { 107 | "name": "username", 108 | "in": "path", 109 | "description": "Username of the current logged in user", 110 | "required": true, 111 | "type": "string" 112 | } 113 | ], 114 | "responses": { 115 | "200": { 116 | "description": "User Found" 117 | }, 118 | "401": { 119 | "description": "Unauthorized Access" 120 | } 121 | } 122 | } 123 | }, 124 | "/update_profile/{username}": { 125 | "put": { 126 | "tags": [ 127 | "User" 128 | ], 129 | "security": [ 130 | { 131 | "bearerAuth": [] 132 | } 133 | ], 134 | "summary": "Update your profile by your username", 135 | "requestBody": { 136 | "description": "", 137 | "required": true, 138 | "content": { 139 | "application/json": { 140 | "schema": { 141 | "$ref": "#/definitions/Profile" 142 | } 143 | } 144 | } 145 | }, 146 | "parameters": [ 147 | { 148 | "name": "username", 149 | "in": "path", 150 | "description": "Username of the current logged in user", 151 | "required": true, 152 | "type": "string" 153 | } 154 | ], 155 | "responses": { 156 | "200": { 157 | "description": "User profile updated successfully" 158 | }, 159 | "400": { 160 | "description": "Invalid Request" 161 | } 162 | } 163 | } 164 | }, 165 | "/change_password/{username}": { 166 | "put": { 167 | "tags": [ 168 | "User" 169 | ], 170 | "security": [ 171 | { 172 | "bearerAuth": [] 173 | } 174 | ], 175 | "summary": "Change your password by your username", 176 | "requestBody": { 177 | "description": "", 178 | "required": true, 179 | "content": { 180 | "application/json": { 181 | "schema": { 182 | "$ref": "#/definitions/ChangePassword" 183 | } 184 | } 185 | } 186 | }, 187 | "parameters": [ 188 | { 189 | "name": "username", 190 | "in": "path", 191 | "description": "Username of the current logged in user", 192 | "required": true, 193 | "type": "string" 194 | } 195 | ], 196 | "responses": { 197 | "200": { 198 | "description": "Password changed successfully" 199 | }, 200 | "400": { 201 | "description": "Missing Fields" 202 | } 203 | } 204 | } 205 | }, 206 | "/delete_account/{username}": { 207 | "delete": { 208 | "tags": [ 209 | "User" 210 | ], 211 | "security": [ 212 | { 213 | "bearerAuth": [] 214 | } 215 | ], 216 | "summary": "Delete your account by using your username", 217 | "parameters": [ 218 | { 219 | "name": "username", 220 | "in": "path", 221 | "description": "Username of the current logged in user", 222 | "required": true, 223 | "type": "string" 224 | } 225 | ], 226 | "responses": { 227 | "200": { 228 | "description": "Account Deleted Successfully" 229 | }, 230 | "401": { 231 | "description": "Unauthorized Access" 232 | } 233 | } 234 | } 235 | }, 236 | "/add_movie/{username}": { 237 | "post": { 238 | "tags": [ 239 | "Movies" 240 | ], 241 | "security": [ 242 | { 243 | "bearerAuth": [] 244 | } 245 | ], 246 | "summary": "Add new movie to the database", 247 | "requestBody": { 248 | "description": "", 249 | "required": true, 250 | "content": { 251 | "application/json": { 252 | "schema": { 253 | "$ref": "#/definitions/Movie" 254 | } 255 | } 256 | } 257 | }, 258 | "parameters": [ 259 | { 260 | "name": "username", 261 | "in": "path", 262 | "description": "Username of the current logged in user", 263 | "required": true, 264 | "type": "string" 265 | } 266 | ], 267 | "responses": { 268 | "201": { 269 | "description": "Movies Added Successfully" 270 | }, 271 | "400": { 272 | "description": "Invalid Request" 273 | } 274 | } 275 | } 276 | }, 277 | "/fetch_movies/{username}": { 278 | "get": { 279 | "tags": [ 280 | "Movies" 281 | ], 282 | "security": [ 283 | { 284 | "bearerAuth": [] 285 | } 286 | ], 287 | "summary": "Fetch all movies", 288 | "produces": [ 289 | "application/json" 290 | ], 291 | "parameters": [ 292 | { 293 | "name": "username", 294 | "in": "path", 295 | "description": "Username of the current logged in user", 296 | "required": true, 297 | "type": "string" 298 | } 299 | ], 300 | "responses": { 301 | "200": { 302 | "schema": { 303 | "type": "array", 304 | "items": { 305 | "$ref": "#/definitions/Movie" 306 | } 307 | } 308 | }, 309 | "404": { 310 | "description": "Movies not found" 311 | } 312 | } 313 | } 314 | }, 315 | "/search_movie/{title}/{username}": { 316 | "get": { 317 | "tags": [ 318 | "Movies" 319 | ], 320 | "security": [ 321 | { 322 | "bearerAuth": [] 323 | } 324 | ], 325 | "summary": "Search movie", 326 | "produces": [ 327 | "application/json" 328 | ], 329 | "parameters": [ 330 | { 331 | "name": "title", 332 | "in": "path", 333 | "description": "title of movie to return", 334 | "required": true, 335 | "type": "string" 336 | }, 337 | { 338 | "name": "username", 339 | "in": "path", 340 | "description": "Username of the current logged in user", 341 | "required": true, 342 | "type": "string" 343 | } 344 | ], 345 | "responses": { 346 | "200": { 347 | "schema": { 348 | "$ref": "#/definitions/Movie" 349 | } 350 | }, 351 | "404": { 352 | "description": "Movie not found" 353 | } 354 | } 355 | } 356 | }, 357 | "/delete_movie/{title}/{username}": { 358 | "delete": { 359 | "tags": [ 360 | "Movies" 361 | ], 362 | "security": [ 363 | { 364 | "bearerAuth": [] 365 | } 366 | ], 367 | "summary": "Delete movie", 368 | "produces": [ 369 | "application/json" 370 | ], 371 | "parameters": [ 372 | { 373 | "name": "title", 374 | "in": "path", 375 | "description": "title of movie to return", 376 | "required": true, 377 | "type": "string" 378 | }, 379 | { 380 | "name": "username", 381 | "in": "path", 382 | "description": "Username of the current logged in user", 383 | "required": true, 384 | "type": "string" 385 | } 386 | ], 387 | "responses": { 388 | "200": { 389 | "description": "Movie Deleted Successfully" 390 | }, 391 | "404": { 392 | "description": "Movie not found" 393 | } 394 | } 395 | } 396 | }, 397 | "/favourite_movies/{username}": { 398 | "get": { 399 | "tags": [ 400 | "Movies" 401 | ], 402 | "security": [ 403 | { 404 | "bearerAuth": [] 405 | } 406 | ], 407 | "summary": "Favourite movies", 408 | "produces": [ 409 | "application/json" 410 | ], 411 | "parameters": [ 412 | { 413 | "name": "username", 414 | "in": "path", 415 | "description": "Username of the current logged in user", 416 | "required": true, 417 | "type": "string" 418 | } 419 | ], 420 | "responses": { 421 | "200": { 422 | "schema": { 423 | "type": "array", 424 | "items": { 425 | "$ref": "#/definitions/Movie" 426 | } 427 | } 428 | }, 429 | "404": { 430 | "description": "Movies not found" 431 | } 432 | } 433 | } 434 | }, 435 | "/add_to_favourite/{title}/{username}": { 436 | "put": { 437 | "tags": [ 438 | "Movies" 439 | ], 440 | "security": [ 441 | { 442 | "bearerAuth": [] 443 | } 444 | ], 445 | "summary": "Add movie to your favourite", 446 | "produces": [ 447 | "application/json" 448 | ], 449 | "requestBody": { 450 | "description": "", 451 | "required": true, 452 | "content": { 453 | "application/json": { 454 | "schema": { 455 | "$ref": "#/definitions/Favourite" 456 | } 457 | } 458 | } 459 | }, 460 | "parameters": [ 461 | { 462 | "name": "title", 463 | "in": "path", 464 | "description": "title of movie to return", 465 | "required": true, 466 | "type": "string" 467 | }, 468 | { 469 | "name": "username", 470 | "in": "path", 471 | "description": "Username of the current logged in user", 472 | "required": true, 473 | "type": "string" 474 | } 475 | ], 476 | "responses": { 477 | "200": { 478 | "description": "Movie has been added to your favourite" 479 | }, 480 | "404": { 481 | "description": "Movie not found" 482 | } 483 | } 484 | } 485 | }, 486 | "/trending_now/{username}": { 487 | "get": { 488 | "tags": [ 489 | "Trending Now" 490 | ], 491 | "security": [ 492 | { 493 | "bearerAuth": [] 494 | } 495 | ], 496 | "summary": "Trending Now Movies", 497 | "produces": [ 498 | "application/json" 499 | ], 500 | "parameters": [ 501 | { 502 | "name": "username", 503 | "in": "path", 504 | "description": "Username of the current logged in user", 505 | "required": true, 506 | "type": "string" 507 | } 508 | ], 509 | "responses": { 510 | "200": { 511 | "schema": { 512 | "type": "array", 513 | "items": { 514 | "$ref": "#/definitions/Movie" 515 | } 516 | } 517 | }, 518 | "404": { 519 | "description": "Movies not found" 520 | } 521 | } 522 | } 523 | } 524 | }, 525 | "components": { 526 | "securitySchemes": { 527 | "bearerAuth": { 528 | "type": "http", 529 | "scheme": "bearer", 530 | "bearerFormat": "JWT" 531 | } 532 | } 533 | }, 534 | "definitions": { 535 | "User": { 536 | "type": "object", 537 | "properties": { 538 | "username": { 539 | "type": "string" 540 | }, 541 | "password": { 542 | "type": "string" 543 | }, 544 | "name": { 545 | "type": "string" 546 | }, 547 | "email": { 548 | "type": "string" 549 | }, 550 | "dob": { 551 | "type": "string" 552 | } 553 | } 554 | }, 555 | "UserLogin": { 556 | "type": "object", 557 | "properties": { 558 | "username": { 559 | "type": "string" 560 | }, 561 | "password": { 562 | "type": "string" 563 | } 564 | } 565 | }, 566 | "UserLoggedIn": { 567 | "type": "object", 568 | "properties": { 569 | "access_token": { 570 | "type": "string" 571 | }, 572 | "name": { 573 | "type": "string" 574 | }, 575 | "email": { 576 | "type": "string" 577 | }, 578 | "dob": { 579 | "type": "string" 580 | } 581 | } 582 | }, 583 | "ChangePassword": { 584 | "type": "object", 585 | "properties": { 586 | "old_password": { 587 | "type": "string" 588 | }, 589 | "new_password": { 590 | "type": "string" 591 | } 592 | } 593 | }, 594 | "Profile": { 595 | "type": "object", 596 | "properties": { 597 | "name": { 598 | "type": "string" 599 | }, 600 | "email": { 601 | "type": "string" 602 | }, 603 | "dob": { 604 | "type": "string" 605 | } 606 | } 607 | }, 608 | "Favourite": { 609 | "type": "object", 610 | "properties": { 611 | "is_favourite": { 612 | "type": "boolean", 613 | "default": false 614 | } 615 | } 616 | }, 617 | "Thumbnails": { 618 | "type": "object", 619 | "properties": { 620 | "type": { 621 | "type": "string" 622 | }, 623 | "client": { 624 | "type": "string" 625 | }, 626 | "size": { 627 | "type": "string" 628 | }, 629 | "path": { 630 | "type": "string" 631 | } 632 | } 633 | }, 634 | "Media": { 635 | "type": "object", 636 | "properties": { 637 | "type": { 638 | "type": "string" 639 | }, 640 | "client": { 641 | "type": "string" 642 | }, 643 | "path": { 644 | "type": "string" 645 | } 646 | } 647 | }, 648 | "Subtitles": { 649 | "type": "object", 650 | "properties": { 651 | "language": { 652 | "type": "string" 653 | }, 654 | "path": { 655 | "type": "string" 656 | } 657 | } 658 | }, 659 | "Movie": { 660 | "type": "object", 661 | "properties": { 662 | "title": { 663 | "type": "string" 664 | }, 665 | "movie_type": { 666 | "type": "string" 667 | }, 668 | "ratings": { 669 | "type": "string" 670 | }, 671 | "duration": { 672 | "type": "integer" 673 | }, 674 | "age_restriction": { 675 | "type": "string" 676 | }, 677 | "timestamp": { 678 | "type": "integer" 679 | }, 680 | "description": { 681 | "type": "string" 682 | }, 683 | "thumbnails": { 684 | "type": "array", 685 | "items": { 686 | "$ref": "#/definitions/Thumbnails" 687 | } 688 | }, 689 | "media": { 690 | "type": "array", 691 | "items": { 692 | "$ref": "#/definitions/Media" 693 | } 694 | }, 695 | "subtitles": { 696 | "type": "array", 697 | "items": { 698 | "$ref": "#/definitions/Subtitles" 699 | } 700 | }, 701 | "cast": { 702 | "type": "array", 703 | "items": { 704 | "type": "string" 705 | } 706 | }, 707 | "genres": { 708 | "type": "array", 709 | "items": { 710 | "type": "string" 711 | } 712 | }, 713 | "category": { 714 | "type": "string" 715 | }, 716 | "production": { 717 | "type": "string" 718 | }, 719 | "country": { 720 | "type": "string" 721 | }, 722 | "is_favourite": { 723 | "type": "boolean", 724 | "default": false 725 | } 726 | } 727 | } 728 | } 729 | } --------------------------------------------------------------------------------