├── .gitignore ├── app ├── Dockerfile ├── __init__.py ├── app.py ├── config.py ├── db.py ├── models │ ├── __init__.py │ ├── item.py │ ├── store.py │ └── user.py ├── requirements.txt ├── resources │ ├── __init__.py │ ├── item.py │ ├── store.py │ └── user.py └── util │ ├── __init__.py │ ├── encoder.py │ └── logz.py ├── data.db ├── docker-compose.yml ├── postgres.env └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .idea 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | .static_storage/ 57 | .media/ 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | # using ubuntu LTS version 2 | FROM ubuntu:20.04 AS builder-image 3 | 4 | # avoid stuck build due to user prompt 5 | ARG DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3.9-dev python3.9-venv python3-pip python3-wheel build-essential && \ 8 | apt-get clean && rm -rf /var/lib/apt/lists/* 9 | 10 | # create and activate virtual environment 11 | # using final folder name to avoid path issues with packages 12 | RUN python3.9 -m venv /home/myuser/venv 13 | ENV PATH="/home/myuser/venv/bin:$PATH" 14 | 15 | # install requirements 16 | COPY requirements.txt . 17 | RUN pip3 install --no-cache-dir wheel 18 | RUN pip3 install --no-cache-dir -r requirements.txt 19 | 20 | FROM ubuntu:20.04 AS runner-image 21 | RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3-venv && \ 22 | apt-get clean && rm -rf /var/lib/apt/lists/* 23 | 24 | RUN useradd --create-home myuser 25 | COPY --from=builder-image /home/myuser/venv /home/myuser/venv 26 | 27 | USER myuser 28 | RUN mkdir /home/myuser/code 29 | WORKDIR /home/myuser/code/app 30 | COPY . . 31 | 32 | EXPOSE 5000 33 | 34 | # make sure all messages always reach console 35 | ENV PYTHONUNBUFFERED=1 36 | 37 | # activate virtual environment 38 | ENV VIRTUAL_ENV=/home/myuser/venv 39 | ENV PATH="/home/myuser/venv/bin:$PATH" 40 | ENV FLASK_APP=app:app.py 41 | ENV FLASK_ENV=development 42 | ENV FLASK_DEBUG=1 43 | # /dev/shm is mapped to shared memory and should be used for gunicorn heartbeat 44 | # this will improve performance and avoid random freezes 45 | CMD ["flask", "run"] 46 | 47 | # CMD ["gunicorn","-b", "0.0.0.0:5000", "-w", "4", "-k", "gevent", "--worker-tmp-dir", "/dev/shm", "app:app"] 48 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryaneaton/flask-restful/63a538fd07c447e872e2b138f5082f7b0907f0ae/app/__init__.py -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | from flask import Flask 6 | from flask_jwt_extended import JWTManager 7 | from flask_restful import Api 8 | 9 | from app.resources.item import Item, ItemList 10 | from app.resources.store import Store, StoreList 11 | from app.resources.user import UserRegister, User 12 | from app.config import postgresqlConfig 13 | 14 | app = Flask(__name__) 15 | 16 | app.config['SQLALCHEMY_DATABASE_URI'] = postgresqlConfig 17 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 18 | # Setup the Flask-JWT-Extended extension 19 | app.config["JWT_SECRET_KEY"] = "Dese.Decent.Pups.BOOYO0OST" # Change this! 20 | jwt = JWTManager(app) 21 | api = Api(app) 22 | 23 | 24 | @app.before_first_request 25 | def create_tables(): 26 | from app.db import db 27 | db.init_app(app) 28 | db.create_all() 29 | 30 | 31 | # jwt = JWT(app, authenticate, identity) # Auto Creates /auth endpoint 32 | 33 | api.add_resource(Item, '/item/') 34 | api.add_resource(ItemList, '/items') 35 | api.add_resource(UserRegister, '/register') 36 | api.add_resource(User, '/user') 37 | api.add_resource(Store, '/store/') 38 | api.add_resource(StoreList, '/stores') 39 | 40 | if __name__ == '__main__': 41 | # TODO: Add swagger integration 42 | app.run(debug=True) # important to mention debug=True 43 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | mssql = {'host': 'dbhost', 6 | 'user': 'dbuser', 7 | 'passwd': 'dbPwd', 8 | 'db': 'db'} 9 | 10 | postgresql = {'host': '0.0.0.0', 11 | 'user': 'postgres', 12 | 'passwd': 'magical_password', 13 | 'db': 'db'} 14 | 15 | 16 | mssqlConfig = "mssql+pyodbc://{}:{}@{}:1433/{}?driver=SQL+Server+Native+Client+10.0".format(mssql['user'], mssql['passwd'], mssql['host'], mssql['db']) 17 | postgresqlConfig = "postgresql+psycopg2://{}:{}@{}/{}".format(postgresql['user'], postgresql['passwd'], postgresql['host'], postgresql['db']) 18 | 19 | -------------------------------------------------------------------------------- /app/db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | db = SQLAlchemy() 8 | 9 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryaneaton/flask-restful/63a538fd07c447e872e2b138f5082f7b0907f0ae/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/item.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | from app.db import db 6 | 7 | 8 | class ItemModel(db.Model): 9 | __tablename__ = 'items' 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | name = db.Column(db.String(80)) 13 | price = db.Column(db.Float(precision=2)) 14 | 15 | store_id = db.Column(db.Integer, db.ForeignKey('stores.id')) 16 | store = db.relationship('StoreModel') 17 | 18 | def __init__(self, name, price, store_id): 19 | self.name = name 20 | self.price = price 21 | self.store_id = store_id 22 | 23 | def json(self): 24 | return {'name': self.name, 'price': self.price, 'store_id': self.store_id} 25 | 26 | @classmethod 27 | def find_by_name(cls, name): 28 | return cls.query.filter_by(name=name).first() # simple TOP 1 select 29 | 30 | def save_to_db(self): # Upserting data 31 | db.session.add(self) 32 | db.session.commit() # Balla 33 | 34 | def delete_from_db(self): 35 | db.session.delete(self) 36 | db.session.commit() 37 | -------------------------------------------------------------------------------- /app/models/store.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | from app.db import db 6 | 7 | 8 | class StoreModel(db.Model): 9 | __tablename__ = 'stores' 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | name = db.Column(db.String(80)) 13 | 14 | items = db.relationship('ItemModel', lazy='dynamic') 15 | 16 | def __init__(self, name): 17 | self.name = name 18 | 19 | def json(self): 20 | return {'name': self.name, 'items': [item.json() for item in self.items.all()]} 21 | 22 | @classmethod 23 | def find_by_name(cls, name): 24 | return cls.query.filter_by(name=name).first() 25 | 26 | def save_to_db(self): 27 | db.session.add(self) 28 | db.session.commit() 29 | 30 | def delete_from_db(self): 31 | db.session.delete(self) 32 | db.session.commit() 33 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | from app.db import db 6 | from werkzeug.security import hmac 7 | 8 | 9 | class UserModel(db.Model): 10 | __tablename__ = 'users' 11 | 12 | id = db.Column(db.Integer, primary_key=True) 13 | username = db.Column(db.String(80)) 14 | password = db.Column(db.String(80)) 15 | 16 | def __init__(self, username, password): 17 | self.username = username 18 | self.password = password 19 | 20 | def save_to_db(self): 21 | db.session.add(self) 22 | db.session.commit() 23 | 24 | def check_password(self, password): 25 | return hmac.compare_digest(self.password, password) 26 | 27 | @classmethod 28 | def find_by_username(cls, username): 29 | return cls.query.filter_by(username=username).first() 30 | 31 | @classmethod 32 | def find_by_id(cls, _id): 33 | return cls.query.filter_by(id=_id).first() 34 | 35 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | Flask-RESTful==0.3.9 3 | Flask-SQLAlchemy==2.5.1 4 | Jinja2==3.1.3 5 | Flask-JWT-Extended==4.2.1 6 | python-dateutil==2.8.1 7 | pytz==2021.1 8 | SQLAlchemy==1.4.19 9 | Werkzeug==3.0.1 10 | psycopg2-binary 11 | rich -------------------------------------------------------------------------------- /app/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryaneaton/flask-restful/63a538fd07c447e872e2b138f5082f7b0907f0ae/app/resources/__init__.py -------------------------------------------------------------------------------- /app/resources/item.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | from flask_restful import Resource, reqparse 5 | from flask_jwt_extended import jwt_required 6 | from app.models.item import ItemModel 7 | from app.util.logz import create_logger 8 | 9 | 10 | class Item(Resource): 11 | parser = reqparse.RequestParser() # only allow price changes, no name changes allowed 12 | parser.add_argument('price', type=float, required=True, 13 | help='This field cannot be left blank') 14 | parser.add_argument('store_id', type=int, required=True, 15 | help='Must enter the store id') 16 | 17 | def __init__(self): 18 | self.logger = create_logger() 19 | 20 | @jwt_required() # Requires dat token 21 | def get(self, name): 22 | item = ItemModel.find_by_name(name) 23 | self.logger.info(f'returning item: {item.json()}') 24 | if item: 25 | return item.json() 26 | return {'message': 'Item not found'}, 404 27 | 28 | @jwt_required() 29 | def post(self, name): 30 | self.logger.info(f'parsed args: {Item.parser.parse_args()}') 31 | 32 | if ItemModel.find_by_name(name): 33 | return {'message': "An item with name '{}' already exists.".format( 34 | name)}, 400 35 | data = Item.parser.parse_args() 36 | item = ItemModel(name, data['price'], data['store_id']) 37 | 38 | try: 39 | item.save_to_db() 40 | except: 41 | return {"message": "An error occurred inserting the item."}, 500 42 | return item.json(), 201 43 | 44 | @jwt_required() 45 | def delete(self, name): 46 | 47 | item = ItemModel.find_by_name(name) 48 | if item: 49 | item.delete_from_db() 50 | 51 | return {'message': 'item has been deleted'} 52 | 53 | @jwt_required() 54 | def put(self, name): 55 | # Create or Update 56 | data = Item.parser.parse_args() 57 | item = ItemModel.find_by_name(name) 58 | 59 | if item is None: 60 | item = ItemModel(name, data['price']) 61 | else: 62 | item.price = data['price'] 63 | 64 | item.save_to_db() 65 | 66 | return item.json() 67 | 68 | 69 | class ItemList(Resource): 70 | @jwt_required() 71 | def get(self): 72 | return { 73 | 'items': [item.json() for item in ItemModel.query.all()]} # More pythonic 74 | ##return {'items': list(map(lambda x: x.json(), ItemModel.query.all()))} #Alternate Lambda way 75 | -------------------------------------------------------------------------------- /app/resources/store.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | #!/usr/bin/env python3 4 | # -*- coding: utf-8 -*- 5 | # standard python imports 6 | 7 | from flask_restful import Resource 8 | from app.models.store import StoreModel 9 | from flask_jwt_extended import jwt_required 10 | from app.util.logz import create_logger 11 | 12 | 13 | class Store(Resource): 14 | 15 | def __init__(self): 16 | self.logger = create_logger() 17 | 18 | def get(self, name): 19 | store = StoreModel.find_by_name(name) 20 | if store: 21 | return store.json() 22 | return {'message': 'Store not found'}, 404 23 | 24 | @jwt_required() # Requires dat token 25 | def post(self, name): 26 | if StoreModel.find_by_name(name): 27 | return {'message': "A store with name '{}' already exists.".format(name)}, 400 28 | 29 | store = StoreModel(name) 30 | try: 31 | store.save_to_db() 32 | except: 33 | return {"message": "An error occurred creating the store."}, 500 34 | 35 | return store.json(), 201 36 | 37 | @jwt_required() # Requires dat token 38 | def delete(self, name): 39 | store = StoreModel.find_by_name(name) 40 | if store: 41 | store.delete_from_db() 42 | 43 | return {'message': 'Store deleted'} 44 | 45 | 46 | class StoreList(Resource): 47 | def get(self): 48 | return {'stores': [store.json() for store in StoreModel.query.all()]} 49 | # return {'stores': list(map(lambda x: x.json(), StoreModel.query.all()))} #Alternate Lambda way 50 | -------------------------------------------------------------------------------- /app/resources/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | from flask_restful import Resource, reqparse 6 | from flask import jsonify 7 | from flask_jwt_extended import create_access_token, jwt_required 8 | from flask_jwt_extended import current_user 9 | from app.models.user import UserModel 10 | from app.util.encoder import AlchemyEncoder 11 | import json 12 | from app.util.logz import create_logger 13 | 14 | 15 | class User(Resource): 16 | def __init__(self): 17 | self.logger = create_logger() 18 | 19 | parser = reqparse.RequestParser() # only allow price changes, no name changes allowed 20 | parser.add_argument('username', type=str, required=True, 21 | help='This field cannot be left blank') 22 | parser.add_argument('password', type=str, required=True, 23 | help='This field cannot be left blank') 24 | 25 | def post(self): 26 | data = User.parser.parse_args() 27 | username = data['username'] 28 | password = data['password'] 29 | 30 | user = UserModel.query.filter_by(username=username).one_or_none() 31 | if not user or not user.check_password(password): 32 | return {'message': 'Wrong username or password.'}, 401 33 | # Notice that we are passing in the actual sqlalchemy user object here 34 | access_token = create_access_token( 35 | identity=json.dumps(user, cls=AlchemyEncoder)) 36 | return jsonify(access_token=access_token) 37 | 38 | @jwt_required() # Requires dat token 39 | def get(self): 40 | # We can now access our sqlalchemy User object via `current_user`. 41 | return jsonify( 42 | id=current_user.id, 43 | full_name=current_user.full_name, 44 | username=current_user.username, 45 | ) 46 | 47 | 48 | class UserRegister(Resource): 49 | def __init__(self): 50 | self.logger = create_logger() 51 | 52 | parser = reqparse.RequestParser() # only allow price changes, no name changes allowed 53 | parser.add_argument('username', type=str, required=True, 54 | help='This field cannot be left blank') 55 | parser.add_argument('password', type=str, required=True, 56 | help='This field cannot be left blank') 57 | 58 | def post(self): 59 | data = UserRegister.parser.parse_args() 60 | 61 | if UserModel.find_by_username(data['username']): 62 | return {'message': 'UserModel has already been created, aborting.'}, 400 63 | 64 | user = UserModel(**data) 65 | user.save_to_db() 66 | 67 | return {'message': 'user has been created successfully.'}, 201 68 | -------------------------------------------------------------------------------- /app/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryaneaton/flask-restful/63a538fd07c447e872e2b138f5082f7b0907f0ae/app/util/__init__.py -------------------------------------------------------------------------------- /app/util/encoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | import json 6 | 7 | from sqlalchemy.ext.declarative import DeclarativeMeta 8 | 9 | 10 | class AlchemyEncoder(json.JSONEncoder): 11 | 12 | def default(self, obj): 13 | if isinstance(obj.__class__, DeclarativeMeta): 14 | # an SQLAlchemy class 15 | fields = {} 16 | for field in [x for x in dir(obj) if 17 | not x.startswith('_') and x != 'metadata']: 18 | data = obj.__getattribute__(field) 19 | try: 20 | json.dumps( 21 | data) # this will fail on non-encodable values, like other classes 22 | fields[field] = data 23 | except TypeError: 24 | fields[field] = None 25 | # a json-encodable dict 26 | return fields 27 | 28 | return json.JSONEncoder.default(self, obj) 29 | -------------------------------------------------------------------------------- /app/util/logz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # standard python imports 4 | 5 | import logging 6 | from rich.console import Console 7 | from rich.logging import RichHandler 8 | from rich.traceback import install 9 | import os 10 | install() 11 | 12 | 13 | def create_logger(): 14 | """Create a logger for use in all cases.""" 15 | loglevel = os.environ.get('LOGLEVEL', 'INFO').upper() 16 | rich_handler = RichHandler(rich_tracebacks=True, markup=True) 17 | logging.basicConfig(level=loglevel, format='%(message)s', 18 | datefmt="[%Y/%m/%d %H:%M;%S]", 19 | handlers=[rich_handler]) 20 | return logging.getLogger('rich') 21 | -------------------------------------------------------------------------------- /data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryaneaton/flask-restful/63a538fd07c447e872e2b138f5082f7b0907f0ae/data.db -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | 4 | app: 5 | build: ./app 6 | restart: always 7 | ports: 8 | - 5000:5000 9 | depends_on: 10 | - database 11 | 12 | database: 13 | image: postgres:latest # use latest official postgres version 14 | env_file: 15 | - postgres.env # configure postgres 16 | ports: 17 | - 5432:5432 18 | volumes: 19 | - database-data:/var/lib/postgresql/data/ # persist data even if container shuts down 20 | volumes: 21 | database-data: # named volumes can be managed easier using docker-compose 22 | -------------------------------------------------------------------------------- /postgres.env: -------------------------------------------------------------------------------- 1 | # database.env 2 | POSTGRES_USER=postgres 3 | POSTGRES_PASSWORD=magical_password 4 | POSTGRES_DB=db -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | A quick project i created while following Jose Salvatierra's excellent flask tutorial 3 | on Udemy. https://www.udemy.com/course/rest-api-flask-and-python/learn/lecture/6038434?start=0#overview 4 | 5 | ✨ Now updated with Flask 2.0 and Flask-Extended!! ✨ 6 | 7 | ### Start Postgres or SQL Server db and update credentials on `config.py` 8 | * update `SQLALCHEMY_DATABASE_URI` in app.py with db config name 9 | * SQL alchemy will create the database objects on app creation. 10 | 11 | 12 | ### Example endpoints 13 | #### Add user 14 | `curl -d "username=user1&password=abcd" -X POST http://localhost:5000/register` 15 | 16 | #### Login 17 | ###### _`(Returns Auth Token)`_ 18 | `curl -d "username=user1&password=abcd" -X POST http://localhost:5000/user` 19 | 20 | #### Add Item 21 | ###### _`(Replace with Auth Token)`_ 22 | `curl -XGET -d "store_id=1&price=2.309" \ 23 | -H "Authorization: Bearer paste_token_here http://localhost:5000/item/xyz` 24 | --------------------------------------------------------------------------------