Skip to content

Commit 40bbefc

Browse files
authored
Merge pull request #115 from tecladocode/develop
2 parents 5d38a64 + 29e52cd commit 40bbefc

File tree

156 files changed

+4677
-129
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

156 files changed

+4677
-129
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ __pycache__/
77
venv/
88
.venv/
99
docs/docs/.nota/config.ini
10+
section-start-code.zip
11+
section-end-code.zip
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.venv
2+
*.pyc
3+
__pycache__
4+
data.db
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_APP=app
2+
FLASK_DEBUG=True
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM python:3.10
2+
EXPOSE 5000
3+
WORKDIR /app
4+
COPY ./requirements.txt requirements.txt
5+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
6+
COPY . .
7+
CMD ["flask", "run", "--host", "0.0.0.0"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from flask import Flask, jsonify
2+
from flask_smorest import Api
3+
from flask_jwt_extended import JWTManager
4+
5+
from db import db
6+
from blocklist import BLOCKLIST
7+
8+
from resources.user import blp as UserBlueprint
9+
from resources.item import blp as ItemBlueprint
10+
from resources.store import blp as StoreBlueprint
11+
from resources.tag import blp as TagBlueprint
12+
13+
14+
def create_app(db_url=None):
15+
app = Flask(__name__)
16+
app.config["API_TITLE"] = "Stores REST API"
17+
app.config["API_VERSION"] = "v1"
18+
app.config["OPENAPI_VERSION"] = "3.0.3"
19+
app.config["OPENAPI_URL_PREFIX"] = "/"
20+
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
21+
app.config[
22+
"OPENAPI_SWAGGER_UI_URL"
23+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
24+
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
25+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
26+
app.config["PROPAGATE_EXCEPTIONS"] = True
27+
db.init_app(app)
28+
api = Api(app)
29+
30+
app.config["JWT_SECRET_KEY"] = "jose"
31+
jwt = JWTManager(app)
32+
33+
# @jwt.additional_claims_loader
34+
# def add_claims_to_jwt(identity):
35+
# # TODO: Read from a config file instead of hard-coding
36+
# if identity == 1:
37+
# return {"is_admin": True}
38+
# return {"is_admin": False}
39+
40+
@jwt.token_in_blocklist_loader
41+
def check_if_token_in_blocklist(jwt_header, jwt_payload):
42+
return jwt_payload["jti"] in BLOCKLIST
43+
44+
@jwt.expired_token_loader
45+
def expired_token_callback(jwt_header, jwt_payload):
46+
return (
47+
jsonify({"message": "The token has expired.", "error": "token_expired"}),
48+
401,
49+
)
50+
51+
@jwt.invalid_token_loader
52+
def invalid_token_callback(error):
53+
return (
54+
jsonify(
55+
{"message": "Signature verification failed.", "error": "invalid_token"}
56+
),
57+
401,
58+
)
59+
60+
@jwt.unauthorized_loader
61+
def missing_token_callback(error):
62+
return (
63+
jsonify(
64+
{
65+
"description": "Request does not contain an access token.",
66+
"error": "authorization_required",
67+
}
68+
),
69+
401,
70+
)
71+
72+
@jwt.needs_fresh_token_loader
73+
def token_not_fresh_callback(jwt_header, jwt_payload):
74+
return (
75+
jsonify(
76+
{
77+
"description": "The token is not fresh.",
78+
"error": "fresh_token_required",
79+
}
80+
),
81+
401,
82+
)
83+
84+
@jwt.revoked_token_loader
85+
def revoked_token_callback(jwt_header, jwt_payload):
86+
return (
87+
jsonify(
88+
{"description": "The token has been revoked.", "error": "token_revoked"}
89+
),
90+
401,
91+
)
92+
93+
# JWT configuration ends
94+
95+
with app.app_context():
96+
import models # noqa: F401
97+
98+
db.create_all()
99+
100+
api.register_blueprint(UserBlueprint)
101+
api.register_blueprint(ItemBlueprint)
102+
api.register_blueprint(StoreBlueprint)
103+
api.register_blueprint(TagBlueprint)
104+
105+
return app
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
blocklist.py
3+
4+
This file just contains the blocklist of the JWT tokens. It will be imported by
5+
app and the logout resource so that tokens can be added to the blocklist when the
6+
user logs out.
7+
"""
8+
9+
BLOCKLIST = set()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from flask_sqlalchemy import SQLAlchemy
2+
3+
db = SQLAlchemy()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from models.user import UserModel
2+
from models.item import ItemModel
3+
from models.tag import TagModel
4+
from models.store import StoreModel
5+
from models.item_tags import ItemsTags
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from db import db
2+
3+
4+
class ItemModel(db.Model):
5+
__tablename__ = "items"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
name = db.Column(db.String(80), unique=False, nullable=False)
9+
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
10+
11+
store_id = db.Column(
12+
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
13+
)
14+
store = db.relationship("StoreModel", back_populates="items")
15+
16+
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from db import db
2+
3+
4+
class ItemsTags(db.Model):
5+
__tablename__ = "items_tags"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
9+
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from db import db
2+
3+
4+
class StoreModel(db.Model):
5+
__tablename__ = "stores"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
name = db.Column(db.String(80), unique=True, nullable=False)
9+
10+
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
11+
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from db import db
2+
3+
4+
class TagModel(db.Model):
5+
__tablename__ = "tags"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
name = db.Column(db.String(80), unique=False, nullable=False)
9+
store_id = db.Column(db.Integer(), db.ForeignKey("stores.id"), nullable=False)
10+
11+
store = db.relationship("StoreModel", back_populates="tags")
12+
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from db import db
2+
3+
4+
class UserModel(db.Model):
5+
__tablename__ = "users"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
username = db.Column(db.String(80), unique=True, nullable=False)
9+
password = db.Column(db.String(80), nullable=False)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Flask-JWT-Extended
2+
Flask-Smorest
3+
Flask-SQLAlchemy
4+
passlib
5+
marshmallow
6+
python-dotenv
7+
gunicorn

docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/resources/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from flask.views import MethodView
2+
from flask_smorest import Blueprint, abort
3+
from flask_jwt_extended import jwt_required, get_jwt
4+
from sqlalchemy.exc import SQLAlchemyError
5+
6+
from db import db
7+
from models import ItemModel
8+
from schemas import ItemSchema, ItemUpdateSchema
9+
10+
blp = Blueprint("Items", "items", description="Operations on items")
11+
12+
13+
@blp.route("/item/<string:item_id>")
14+
class Item(MethodView):
15+
@jwt_required()
16+
@blp.response(200, ItemSchema)
17+
def get(self, item_id):
18+
item = ItemModel.query.get_or_404(item_id)
19+
return item
20+
21+
@jwt_required()
22+
def delete(self, item_id):
23+
jwt = get_jwt()
24+
if not jwt.get("is_admin"):
25+
abort(401, message="Admin privilege required.")
26+
27+
item = ItemModel.query.get_or_404(item_id)
28+
db.session.delete(item)
29+
db.session.commit()
30+
return {"message": "Item deleted."}
31+
32+
@blp.arguments(ItemUpdateSchema)
33+
@blp.response(200, ItemSchema)
34+
def put(self, item_data, item_id):
35+
item = ItemModel.query.get(item_id)
36+
37+
if item:
38+
item.price = item_data["price"]
39+
item.name = item_data["name"]
40+
else:
41+
item = ItemModel(id=item_id, **item_data)
42+
43+
db.session.add(item)
44+
db.session.commit()
45+
46+
return item
47+
48+
49+
@blp.route("/item")
50+
class ItemList(MethodView):
51+
@jwt_required()
52+
@blp.response(200, ItemSchema(many=True))
53+
def get(self):
54+
return ItemModel.query.all()
55+
56+
@jwt_required(fresh=True)
57+
@blp.arguments(ItemSchema)
58+
@blp.response(201, ItemSchema)
59+
def post(self, item_data):
60+
item = ItemModel(**item_data)
61+
62+
try:
63+
db.session.add(item)
64+
db.session.commit()
65+
except SQLAlchemyError:
66+
abort(500, message="An error occurred while inserting the item.")
67+
68+
return item
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from flask.views import MethodView
2+
from flask_smorest import Blueprint, abort
3+
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
4+
5+
from db import db
6+
from models import StoreModel
7+
from schemas import StoreSchema
8+
9+
10+
blp = Blueprint("Stores", "stores", description="Operations on stores")
11+
12+
13+
@blp.route("/store/<string:store_id>")
14+
class Store(MethodView):
15+
@blp.response(200, StoreSchema)
16+
def get(self, store_id):
17+
store = StoreModel.query.get_or_404(store_id)
18+
return store
19+
20+
def delete(self, store_id):
21+
store = StoreModel.query.get_or_404(store_id)
22+
db.session.delete(store)
23+
db.session.commit()
24+
return {"message": "Store deleted"}, 200
25+
26+
27+
@blp.route("/store")
28+
class StoreList(MethodView):
29+
@blp.response(200, StoreSchema(many=True))
30+
def get(self):
31+
return StoreModel.query.all()
32+
33+
@blp.arguments(StoreSchema)
34+
@blp.response(201, StoreSchema)
35+
def post(self, store_data):
36+
store = StoreModel(**store_data)
37+
try:
38+
db.session.add(store)
39+
db.session.commit()
40+
except IntegrityError:
41+
abort(
42+
400,
43+
message="A store with that name already exists.",
44+
)
45+
except SQLAlchemyError:
46+
abort(500, message="An error occurred creating the store.")
47+
48+
return store

0 commit comments

Comments
 (0)