diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a25664 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +test/test.py +test/test.db \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7e7f74b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/usr/sbin/python3.9" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3661f3e --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# REST API application made with flask. +Contains <> branches + +Final and stable application is in the master branch + +NOTE: Use a virtual environment with python 3.9 to run the application + +## INSATLLATION + +Run the following command to install dependencies: + +``` +pip install -r requirements.txt +``` + +## RUN + +Run the following command to start the application: + +``` +python app.py +``` \ No newline at end of file diff --git a/__pycache__/database.cpython-39.pyc b/__pycache__/database.cpython-39.pyc new file mode 100644 index 0000000..ee98920 Binary files /dev/null and b/__pycache__/database.cpython-39.pyc differ diff --git a/__pycache__/item.cpython-39.pyc b/__pycache__/item.cpython-39.pyc new file mode 100644 index 0000000..19f4cec Binary files /dev/null and b/__pycache__/item.cpython-39.pyc differ diff --git a/__pycache__/security.cpython-39.pyc b/__pycache__/security.cpython-39.pyc new file mode 100644 index 0000000..211b469 Binary files /dev/null and b/__pycache__/security.cpython-39.pyc differ diff --git a/__pycache__/user.cpython-39.pyc b/__pycache__/user.cpython-39.pyc new file mode 100644 index 0000000..5c46dce Binary files /dev/null and b/__pycache__/user.cpython-39.pyc differ diff --git a/app.py b/app.py index 5fbfd70..e475db3 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,42 @@ -from flask import Flask -# creating an app from Flask class -app = Flask(__name__) -# letting the application know what requests it will understand -@app.route('/') -def home(): - return "Hello World!" - -# run the app in specified folder -app.run(port=5000) \ No newline at end of file +from flask import Flask +from flask_restful import Api +from flask_jwt import JWT + +from security import authenticate, identity +from resources.user import UserRegister +from resources.item import Item, ItemList +from resources.store import Store, StoreList +from database import db + +app = Flask(__name__) + +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database/data.db" + +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # turns of flask_sqlalchemy modification tracker +app.secret_key = "komraishumtirkomchuri" + +api = Api(app) + +@app.before_first_request +def create_tables(): + db.create_all() + # above function creates all the tables before the 1st request is made + # unless they exist alraedy + +# JWT() creates a new endpoint: /auth +# we send an username and password to /auth +# JWT() gets the username and password, and sends it to authenticate function +# the authenticate function maps the username and checks the password +# if all goes well, the authenticate function returns user +# which is the identity or jwt(or token) +jwt = JWT(app, authenticate, identity) + +api.add_resource(Item, '/item/') +api.add_resource(Store, '/store/') +api.add_resource(ItemList, '/items') +api.add_resource(StoreList, '/stores') +api.add_resource(UserRegister, '/register') + +if __name__ == "__main__": + db.init_app(app) + app.run(port=5000, debug=True) \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..2e1eeb6 --- /dev/null +++ b/database.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/database/data.db b/database/data.db new file mode 100644 index 0000000..b6cbb4b Binary files /dev/null and b/database/data.db differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/__pycache__/__init__.cpython-39.pyc b/models/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..1463237 Binary files /dev/null and b/models/__pycache__/__init__.cpython-39.pyc differ diff --git a/models/__pycache__/item.cpython-39.pyc b/models/__pycache__/item.cpython-39.pyc new file mode 100644 index 0000000..dd94649 Binary files /dev/null and b/models/__pycache__/item.cpython-39.pyc differ diff --git a/models/__pycache__/store.cpython-39.pyc b/models/__pycache__/store.cpython-39.pyc new file mode 100644 index 0000000..b2aa641 Binary files /dev/null and b/models/__pycache__/store.cpython-39.pyc differ diff --git a/models/__pycache__/user.cpython-39.pyc b/models/__pycache__/user.cpython-39.pyc new file mode 100644 index 0000000..de3c6aa Binary files /dev/null and b/models/__pycache__/user.cpython-39.pyc differ diff --git a/models/item.py b/models/item.py new file mode 100644 index 0000000..987a584 --- /dev/null +++ b/models/item.py @@ -0,0 +1,37 @@ +from database import db + +class ItemModel(db.Model): # tells SQLAlchemy that it is something that will be saved to database and will be retrieved from database + + __tablename__ = "items" + + # Columns + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80)) + price = db.Column(db.Float(precision=2)) # precision: numbers after decimal point + + store_id = db.Column(db.Integer, db.ForeignKey("stores.id")) + store = db.relationship("StoreModel") + + def __init__(self, name, price, store_id): + self.name = name + self.price = price + self.store_id = store_id + + def json(self): + return {"id": self.id,"store_id":self.store_id, "name": self.name, "price": self.price} + + # searches the database for items using name + @classmethod + def find_item_by_name(cls, name): + # return cls.query.filter_by(name=name) # SELECT name FROM __tablename__ WHERE name=name + # this function would return a ItemModel object + return cls.query.filter_by(name=name).first() # SELECT name FROM __tablename__ WHERE name=name LIMIT 1 + + # method to insert or update an item into database + def save_to_database(self): + db.session.add(self) # session here is a collection of objects that wil be written to database + db.session.commit() + + def delete_from_database(self): + db.session.delete(self) + db.session.commit() diff --git a/models/store.py b/models/store.py new file mode 100644 index 0000000..97d8a04 --- /dev/null +++ b/models/store.py @@ -0,0 +1,33 @@ +from database import db + +class StoreModel(db.Model): # tells SQLAlchemy that it is something that will be saved to database and will be retrieved from database + + __tablename__ = "stores" + + # Columns + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80)) + + items = db.relationship("ItemModel", lazy="dynamic") # this will allow us to check which item is in store whose id is equal to it's id. + # lazy="dynamic" tells sqlalchemy to not create seperate objects for each item that is created + def __init__(self, name): + self.name = name + + def json(self): + return {"id": self.id, "name": self.name, "items": [item.json() for item in self.items.all()]} + + # searches the database for items using name + @classmethod + def find_item_by_name(cls, name): + # return cls.query.filter_by(name=name) # SELECT name FROM __tablename__ WHERE name=name + # this function would return a StoreModel object + return cls.query.filter_by(name=name).first() # SELECT name FROM __tablename__ WHERE name=name LIMIT 1 + + # method to insert or update an item into database + def save_to_database(self): + db.session.add(self) # session here is a collection of objects that wil be written to database + db.session.commit() + + def delete_from_database(self): + db.session.delete(self) + db.session.commit() diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..f5bde4f --- /dev/null +++ b/models/user.py @@ -0,0 +1,27 @@ +import sqlite3 +from database import db + +class UserModel(db.Model): + + __tablename__ = "users" # will be used to tell sqlalchemy the table name for users + + # table columns for users table + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80)) + password = db.Column(db.String(80)) + + def __init__(self, username, password): + self.username = username + self.password = password + + @classmethod + def find_by_username(cls, username): + return cls.query.filter_by(username=username).first() + + @classmethod + def find_by_id(cls, _id): + return cls.query.filter_by(id=_id).first() + + def save_to_database(self): + db.session.add(self) + db.session.commit() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..df44585 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask +Flask-RESTful +Flask-JWT +Flask-SQLAlchemy \ No newline at end of file diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/__pycache__/__init__.cpython-39.pyc b/resources/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..fbe2f60 Binary files /dev/null and b/resources/__pycache__/__init__.cpython-39.pyc differ diff --git a/resources/__pycache__/item.cpython-39.pyc b/resources/__pycache__/item.cpython-39.pyc new file mode 100644 index 0000000..7791975 Binary files /dev/null and b/resources/__pycache__/item.cpython-39.pyc differ diff --git a/resources/__pycache__/store.cpython-39.pyc b/resources/__pycache__/store.cpython-39.pyc new file mode 100644 index 0000000..7e6d780 Binary files /dev/null and b/resources/__pycache__/store.cpython-39.pyc differ diff --git a/resources/__pycache__/user.cpython-39.pyc b/resources/__pycache__/user.cpython-39.pyc new file mode 100644 index 0000000..571f457 Binary files /dev/null and b/resources/__pycache__/user.cpython-39.pyc differ diff --git a/resources/item.py b/resources/item.py new file mode 100644 index 0000000..1ecaecf --- /dev/null +++ b/resources/item.py @@ -0,0 +1,84 @@ +import sqlite3 + +from flask import Flask, request +from flask_restful import Resource, reqparse +from flask_jwt import jwt_required + +from models.item import ItemModel + +class Item(Resource): + + parser = reqparse.RequestParser() + parser.add_argument("price", + type=float, + required=True, + help="This field cannot be blank" + ) + parser.add_argument("store_id", + type=int, + required=True, + help="Every item needs a store id." + ) + + # TO GET ITEM WITH NAME + @jwt_required() + def get(self, name): + item = ItemModel.find_item_by_name(name) + if item: + return item.json() + + return {"message": "item not found."}, 404 + + # TO POST AN ITEM + def post(self, name): + # if there already exists an item with "name", show a messege, and donot add the item + if ItemModel.find_item_by_name(name): + return {"messege": f"item {name} already exists"} ,400 + + data = Item.parser.parse_args() + # data = request.get_json() # get_json(force=True) means, we don't need a content type header + item = ItemModel(name, **data) + + try: + item.save_to_database() + except: + return {"messege": "An error occured."}, 500 + + return item.json(), 201 # 201 is for CREATED status + + # TO DELETE AN ITEM + @jwt_required() + def delete(self, name): + item = ItemModel.find_item_by_name(name) + if item: + item.delete_from_database() + return {"messege": "Item deleted"} + + # if doesn't exist, skip deleting + return {"messege": "Item don't exist"}, 400 + + # TO ADD OR UPDATE AN ITEM + def put(self, name): + data = Item.parser.parse_args() + # data = request.get_json() + item = ItemModel.find_item_by_name(name) + + # if item is not available, add it + if item is None: + item = ItemModel(name, **data) + # if item exists, update it + else: + item.price = data['price'] + item.store_id = data['store_id'] + + # whether item is changed or inserted, it has to be saved to db + item.save_to_database() + return item.json() + + +class ItemList(Resource): + + # TO GET ALL ITEMS + def get(self): + # return {"item": list(map(lambda x: x.json(), ItemModel.query.all()))} + return {"items": [item.json() for item in ItemModel.query.all()]} \ No newline at end of file diff --git a/resources/store.py b/resources/store.py new file mode 100644 index 0000000..9dad423 --- /dev/null +++ b/resources/store.py @@ -0,0 +1,38 @@ +from flask_restful import Resource +from models.store import StoreModel + +class Store(Resource): + + def get(self, name): + store = StoreModel.find_item_by_name(name) + if store: + return store.json() + + def post(self, name): + if StoreModel.find_item_by_name(name): + return {"message": "Store alrady exists."}, 400 + + store = StoreModel(name) + + try: + store.save_to_database() + except: + return {"message": "An error occured while creating the store."}, 500 + + return store.json(), 201 + + + + def delete(self, name): + store = StoreModel.find_item_by_name(name) + if store: + store.delete_from_database() + return {"message": "store deleetd."} + + return {"message": "store don't exist"} + + +class StoreList(Resource): + def get(self): + # return {"item": list(map(lambda x: x.json(), ItemModel.query.all()))} + return {"stores": [store.json() for store in StoreModel.query.all()]} \ No newline at end of file diff --git a/resources/user.py b/resources/user.py new file mode 100644 index 0000000..3f6eed1 --- /dev/null +++ b/resources/user.py @@ -0,0 +1,34 @@ +import sqlite3 +from flask_restful import Resource, reqparse +from models.user import UserModel +# New user registraction class +class UserRegister(Resource): + # Creating a request parser + parser = reqparse.RequestParser() + # Adding username argument to parser + parser.add_argument( + "username", + type=str, + required=True, + help="This field cannot be empty" + ) + # Adding password argument to parser + parser.add_argument( + "password", + type=str, + required=True, + help="This field cannot be empty" + ) + + # calls to post a new user (new user registration) + def post(self): + data = UserRegister.parser.parse_args() + # First check if that user is present or not + if UserModel.find_by_username(data["username"]): + # if exists, then don't add + return {"message": "An user with that username already exists."}, 400 + + user = UserModel(**data) # since parser only takes in username and password, only those two will be added. + user.save_to_database() + + return {"messege": "User added successfully."}, 201 \ No newline at end of file diff --git a/security.py b/security.py new file mode 100644 index 0000000..9fb76aa --- /dev/null +++ b/security.py @@ -0,0 +1,12 @@ +from werkzeug.security import safe_str_cmp +from models.user import UserModel + +def authenticate(username, password): + user = UserModel.find_by_username(username) + # if user and user.password == password: + if user and safe_str_cmp(user.password, password): + return user + +def identity(payload): # payload is the contents of jwt + user_id = payload["identity"] + return UserModel.find_by_id(user_id) diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..fb11bf2 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,28 @@ + + + + + + + + + +
+ Hello, world! +
+ + \ No newline at end of file