Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7f6f246
Added get and post methods
Subhadeep0506 Jan 15, 2022
ee3d121
Added README
Subhadeep0506 Jan 15, 2022
0e38618
Initial Commit
Subhadeep0506 Jan 16, 2022
335d0a8
added requirements.txt
Subhadeep0506 Jan 18, 2022
c85794b
added authentication to app
Subhadeep0506 Jan 18, 2022
5773e1c
added delete and put methods
Subhadeep0506 Jan 18, 2022
6976738
modified README
Subhadeep0506 Jan 18, 2022
1b65a98
minor changes
Subhadeep0506 Jan 20, 2022
c8007f1
added database and user registration
Subhadeep0506 Jan 20, 2022
191924f
added database integration to get and post
Subhadeep0506 Jan 20, 2022
281696e
implemeted delete method
Subhadeep0506 Jan 21, 2022
933e106
minor changes
Subhadeep0506 Jan 21, 2022
71f90dc
implemented database into put and get items
Subhadeep0506 Jan 21, 2022
453932f
minor changes
Subhadeep0506 Jan 21, 2022
6858880
made seperate folders for item and user
Subhadeep0506 Jan 24, 2022
815d0b0
Added Store functions
Subhadeep0506 Jan 25, 2022
f235ca9
initial commit
Subhadeep0506 Jan 25, 2022
02cb51e
created login endpoint
Subhadeep0506 Jan 26, 2022
8ea1275
Added blocklist and other jwt configs
Subhadeep0506 Jan 27, 2022
60d5cf8
added logout feature
Subhadeep0506 Jan 27, 2022
e7c80af
files exclusion
Subhadeep0506 Jan 27, 2022
f4b1c83
minor changes
Subhadeep0506 Feb 3, 2022
447a3ec
added type-hinying to all methods
Subhadeep0506 Feb 23, 2022
b251732
added marshmallow serialization and deserialization
Subhadeep0506 Feb 25, 2022
a4eaa65
added flask-marshmallow to user resource
Subhadeep0506 Feb 25, 2022
1005ec1
removed unnecessary directories
Subhadeep0506 Feb 25, 2022
8ac4006
added marshmallow to item and store
Subhadeep0506 Feb 26, 2022
c430faa
changed ModelSchema to SQLAlchemyAutoSchema
Subhadeep0506 Feb 26, 2022
e9c4b97
modified gitignore
Subhadeep0506 Feb 26, 2022
d6bc3b0
Added user confirmation
Subhadeep0506 Feb 28, 2022
c29a1c0
Added email confirmation
Subhadeep0506 Mar 1, 2022
5d4d5a9
Added environment variables
Subhadeep0506 Mar 2, 2022
af8d3cb
Minor changes
Subhadeep0506 Mar 5, 2022
12480d9
minor unfortunate changes
Subhadeep0506 Mar 5, 2022
a7ff629
minor changes
Subhadeep0506 Mar 5, 2022
b2f02e8
Re-added confirmation page
Subhadeep0506 Mar 5, 2022
3daf439
minor changes
Subhadeep0506 Mar 5, 2022
ee46742
minor changes
Subhadeep0506 Mar 6, 2022
d8788c9
Modified gitignore
Subhadeep0506 Mar 8, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MAILGUN_DOMAIN=
MAILGUN_API_KEY=
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
test/test.py
test/test.db
__pycache__
.vscode
.idea
/.env
database/data.db
resources/__pycache__
model/__pycache__
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/PythonRESTapi.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"python.pythonPath": "/usr/sbin/python3.9",
"python.formatting.provider": "black"
}
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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
```

## IMPORTANT

Make sure to delete the `data.db` file from `database` directory before fresh run.
Binary file added __pycache__/blacklist.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/database.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/item.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/security.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/user.cpython-39.pyc
Binary file not shown.
147 changes: 137 additions & 10 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,137 @@
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)
from flask import Flask, jsonify
from flask_restful import Api
from flask_jwt_extended import JWTManager
from marshmallow import ValidationError

from ma import ma
from resources.user import (
UserConfirm,
UserRegister,
User,
UserLogin,
UserLogout,
TokenRefresh,
)
from resources.item import Item, ItemList
from resources.store import Store, StoreList
from blacklist import BLACKLIST
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.config["PROPAGATE_EXCEPTIONS"] = True # if flask_jwt raises an error, the flask app will check the error if this is set to true
app.config["JWT_BLACKLIST_ENABLED"] = True
app.config["JWT_BLACKLIST_TOKEN_CHECKS"] = [
"access",
"refresh",
] # both access and refresh tokens will be denied for the user ids

app.secret_key = "komraishumtirkomchuri"
# app.config["JWT_SECRET_KEY"] = "YOUR KEY HERE"

api = Api(app)


@app.errorhandler(ValidationError)
def handle_marshmallow_validation(err):
return jsonify(err.messages), 400


@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 already


# JWT() creates a new endpoint: /auth
# we send 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)
jwt = JWTManager(app) # JwtManager links up to the application, doesn't create /auth point


# below function returns True, if the token that is sent is in the blacklist
@jwt.token_in_blocklist_loader
def check_if_token_in_blacklist(jwt_header, jwt_data):
print("Log Message:", jwt_data)
return jwt_data["jti"] in BLACKLIST


@jwt.additional_claims_loader # modifies the below function, and links it with JWTManager, which in turn is linked with our app
def add_claims_to_jwt(identity):
if identity == 1: # instead of hard-coding this, we should read it from a config file or database
return {"is_admin": True}

return {"is_admin": False}


# JWT Configurations
@jwt.expired_token_loader
def expired_token_callback():
return (
jsonify({"description": "The token has expired.", "error": "token_expired"}),
401,
)


@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify({"description": "Signature verification failed.", "error": "invalid_token"}),
401,
)


@jwt.unauthorized_loader
def missing_token_callback(error): # when no jwt is sent
return (
jsonify(
{
"description": "Request doesn't contain a access token.",
"error": "authorization_required",
}
),
401,
)


@jwt.needs_fresh_token_loader
def token_not_fresh_callback(self, callback):
# print("Log:", callback)
return (
jsonify({"description": "The token is not fresh.", "error": "fresh_token_required"}),
401,
)


@jwt.revoked_token_loader
def revoked_token_callback(self, callback):
# print("Log:", callback)
return (
jsonify({"description": "The token has been revoked.", "error": "token_revoked"}),
401,
)


api.add_resource(Item, "/item/<string:name>")
api.add_resource(Store, "/store/<string:name>")
api.add_resource(ItemList, "/items")
api.add_resource(StoreList, "/stores")
api.add_resource(UserRegister, "/register")
api.add_resource(UserConfirm, "/activate/<int:user_id>")
api.add_resource(User, "/user/<int:user_id>")
api.add_resource(UserLogin, "/login")
api.add_resource(UserLogout, "/logout")
api.add_resource(TokenRefresh, "/refresh")

if __name__ == "__main__":
db.init_app(app)
ma.init_app(app) # tells marshmallow that it should be communicating with this flask app
app.run(port=5000, debug=True)
2 changes: 2 additions & 0 deletions blacklist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
BLACKLIST = set() # user ids that will be denied access

3 changes: 3 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
Binary file added database/data.db
Binary file not shown.
Empty file added libs/__init__.py
Empty file.
51 changes: 51 additions & 0 deletions libs/mailgun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os
from typing import List
from requests import Response, post


FAILED_LOAD_API_KEY = "Falied to load Mailgun API key."
FAILED_LOAD_DOMAIN_NAME = "Failde to load Mailgun domain."
FAILED_SEND_CONFIRMATION_MAIL = "Error in sending confirmation email, user registration failed."


class MailgunException(Exception):
def __init__(self, message: str):
super().__init__(message)


class Mailgun:

MAILGUN_DOMAIN = os.environ.get("MAILGUN_DOMAIN") # can be None
MAILGUN_API_KEY = os.environ.get("MAILGUN_API_KEY") # can be None
FROM_TITLE = "Stores RestAPI"
# FROM_EMAIL = "Your Mailgun Email"
FROM_EMAIL = "postmaster@sandboxb3ef8f2c0e20406cb3f834bc39735ab4.mailgun.org"

# This method will interact with Mailgun API and return the response sent
@classmethod
def send_email(cls, email: List[str], subject: str, text: str, html: str) -> Response:
if cls.MAILGUN_API_KEY is None:
raise MailgunException(FAILED_LOAD_API_KEY)

if cls.MAILGUN_DOMAIN is None:
raise MailgunException(FAILED_LOAD_DOMAIN_NAME)

response = post(
f"https://api.mailgun.net/v3/{cls.MAILGUN_DOMAIN}/messages",
auth=(
"api",
cls.MAILGUN_API_KEY,
),
data={
"from": f"{cls.FROM_TITLE} <{cls.FROM_EMAIL}>",
"to": email,
"subject": subject,
"text": text,
"html": html,
},
)

if response.status_code != 200:
raise MailgunException(FAILED_SEND_CONFIRMATION_MAIL)

return response
3 changes: 3 additions & 0 deletions ma.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_marshmallow import Marshmallow

ma = Marshmallow()
Empty file added models/__init__.py
Empty file.
Binary file added models/__pycache__/__init__.cpython-39.pyc
Binary file not shown.
Binary file added models/__pycache__/item.cpython-39.pyc
Binary file not shown.
Binary file added models/__pycache__/store.cpython-39.pyc
Binary file not shown.
Binary file added models/__pycache__/user.cpython-39.pyc
Binary file not shown.
36 changes: 36 additions & 0 deletions models/item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import List

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), nullable=False, unique=True)
price = db.Column(db.Float(precision=2), nullable=False) # precision: numbers after decimal point

store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="items")

# searches the database for items using name
@classmethod
def find_item_by_name(cls, name: str) -> "ItemModel":
# 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

@classmethod
def find_all(cls) -> List["ItemModel"]:
return cls.query.all()

# method to insert or update an item into database
def save_to_database(self) -> None:
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) -> None:
db.session.delete(self)
db.session.commit()
35 changes: 35 additions & 0 deletions models/store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import List

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), nullable=False, unique=True)

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

# searches the database for items using name
@classmethod
def find_store_by_name(cls, name: str) -> "StoreModel":
# 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

@classmethod
def find_all(cls) -> List["StoreModel"]:
return cls.query.all()

# method to insert or update an item into database
def save_to_database(self) -> None:
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) -> None:
db.session.delete(self)
db.session.commit()
Loading