Skip to content

Commit 6719c86

Browse files
Merge pull request #27 from piyush-jaiswal/feature/app-factory-pattern
Feature/app factory pattern
2 parents 41750b1 + e22acd1 commit 6719c86

File tree

12 files changed

+248
-225
lines changed

12 files changed

+248
-225
lines changed

api/vercel_function.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
from app import app
1+
from app import create_app
2+
from config import ProductionConfig
3+
4+
5+
app = create_app(ProductionConfig)

app/__init__.py

Lines changed: 17 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,28 @@
1-
import os
2-
from datetime import timedelta
1+
from flask import Flask
32

4-
from flask import Flask, jsonify
5-
from flask_jwt_extended import JWTManager
6-
from flask_migrate import Migrate
7-
from flask_sqlalchemy import SQLAlchemy
8-
from dotenv import load_dotenv
9-
from sqlalchemy import MetaData
10-
from flask_smorest import Api
3+
from app.extensions import api, db, jwt, migrate
4+
from config import DevelopmentConfig
115

126

13-
def register_blueprints():
7+
def create_app(config_class=DevelopmentConfig):
8+
app = Flask(__name__)
9+
app.config.from_object(config_class)
10+
11+
# initialize extensions
12+
db.init_app(app)
13+
migrate.init_app(app, db)
14+
jwt.init_app(app)
15+
api.init_app(app)
16+
17+
# register blueprints
18+
from app.routes.auth import bp as auth_bp
1419
from app.routes.category import bp as category_bp
15-
from app.routes.subcategory import bp as subcategory_bp
1620
from app.routes.product import bp as product_bp
17-
from app.routes.auth import bp as auth_bp
21+
from app.routes.subcategory import bp as subcategory_bp
1822

1923
api.register_blueprint(category_bp, url_prefix="/categories")
2024
api.register_blueprint(subcategory_bp, url_prefix="/subcategories")
2125
api.register_blueprint(product_bp, url_prefix="/products")
2226
api.register_blueprint(auth_bp, url_prefix="/auth")
2327

24-
25-
app = Flask(__name__)
26-
27-
load_dotenv()
28-
29-
# sqlalchemy
30-
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("SQLALCHEMY_DATABASE_URI")
31-
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
32-
33-
# jwt
34-
app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY")
35-
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=3)
36-
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=3)
37-
38-
# flask-smorest
39-
app.config["API_TITLE"] = "Ecommerce REST API"
40-
app.config["API_VERSION"] = "v1"
41-
app.config["OPENAPI_VERSION"] = "3.0.2"
42-
43-
# flask-smorest openapi swagger
44-
app.config["OPENAPI_URL_PREFIX"] = "/"
45-
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/"
46-
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
47-
48-
# flask-smorest Swagger UI top level authorize dialog box
49-
app.config["API_SPEC_OPTIONS"] = {
50-
"components": {
51-
"securitySchemes": {
52-
"access_token": {
53-
"type": "http",
54-
"scheme": "bearer",
55-
"bearerFormat": "JWT",
56-
"description": "Enter your JWT access token",
57-
},
58-
"refresh_token": {
59-
"type": "http",
60-
"scheme": "bearer",
61-
"bearerFormat": "JWT",
62-
"description": "Enter your JWT refresh token",
63-
},
64-
}
65-
}
66-
}
67-
68-
# PostgreSQL-compatible naming convention (to follow the naming convention already used in the DB)
69-
# https://stackoverflow.com/questions/4107915/postgresql-default-constraint-names
70-
naming_convention = {
71-
"ix": "%(table_name)s_%(column_0_name)s_idx", # Indexes
72-
"uq": "%(table_name)s_%(column_0_name)s_key", # Unique constraints
73-
"ck": "%(table_name)s_%(constraint_name)s_check", # Check constraints
74-
"fk": "%(table_name)s_%(column_0_name)s_fkey", # Foreign keys
75-
"pk": "%(table_name)s_pkey" # Primary keys
76-
}
77-
metadata = MetaData(naming_convention=naming_convention)
78-
db = SQLAlchemy(app, metadata=metadata)
79-
migrate = Migrate(app, db)
80-
jwt = JWTManager(app)
81-
api = Api(app)
82-
83-
register_blueprints()
84-
85-
86-
@jwt.expired_token_loader
87-
def expired_token_callback(jwt_header, jwt_payload):
88-
err = "Access token expired. Use your refresh token to get a new one."
89-
if jwt_payload['type'] == 'refresh':
90-
err = "Refresh token expired. Please login again."
91-
return jsonify(code="token_expired", error=err), 401
92-
93-
@jwt.invalid_token_loader
94-
def invalid_token_callback(error):
95-
return jsonify(code="invalid_token", error="Invalid token provided."), 401
96-
97-
@jwt.unauthorized_loader
98-
def missing_token_callback(error):
99-
return jsonify(code="authorization_required", error="JWT needed for this operation. Login, if needed."), 401
28+
return app

app/extensions.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from flask import jsonify
2+
from flask_jwt_extended import JWTManager
3+
from flask_migrate import Migrate
4+
from flask_smorest import Api
5+
from flask_sqlalchemy import SQLAlchemy
6+
from sqlalchemy import MetaData
7+
8+
9+
# PostgreSQL-compatible naming convention (to follow the naming convention already used in the DB)
10+
# https://stackoverflow.com/questions/4107915/postgresql-default-constraint-names
11+
naming_convention = {
12+
"ix": "%(table_name)s_%(column_0_name)s_idx", # Indexes
13+
"uq": "%(table_name)s_%(column_0_name)s_key", # Unique constraints
14+
"ck": "%(table_name)s_%(constraint_name)s_check", # Check constraints
15+
"fk": "%(table_name)s_%(column_0_name)s_fkey", # Foreign keys
16+
"pk": "%(table_name)s_pkey", # Primary keys
17+
}
18+
metadata = MetaData(naming_convention=naming_convention)
19+
db = SQLAlchemy(metadata=metadata)
20+
migrate = Migrate(db)
21+
jwt = JWTManager()
22+
api = Api()
23+
24+
25+
@jwt.expired_token_loader
26+
def expired_token_callback(jwt_header, jwt_payload):
27+
err = "Access token expired. Use your refresh token to get a new one."
28+
if jwt_payload["type"] == "refresh":
29+
err = "Refresh token expired. Please login again."
30+
return jsonify(code="token_expired", error=err), 401
31+
32+
33+
@jwt.invalid_token_loader
34+
def invalid_token_callback(error):
35+
return jsonify(code="invalid_token", error="Invalid token provided."), 401
36+
37+
38+
@jwt.unauthorized_loader
39+
def missing_token_callback(error):
40+
return jsonify(
41+
code="authorization_required",
42+
error="JWT needed for this operation. Login, if needed.",
43+
), 401

config.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import os
2+
from datetime import timedelta
3+
4+
from dotenv import load_dotenv
5+
6+
7+
load_dotenv()
8+
9+
10+
class Config:
11+
# sqlalchemy
12+
SQLALCHEMY_TRACK_MODIFICATIONS = False
13+
14+
# jwt
15+
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
16+
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=3)
17+
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=3)
18+
19+
# flask-smorest
20+
API_TITLE = "Ecommerce REST API"
21+
API_VERSION = "v1"
22+
OPENAPI_VERSION = "3.0.2"
23+
24+
# flask-smorest openapi swagger
25+
OPENAPI_URL_PREFIX = "/"
26+
OPENAPI_SWAGGER_UI_PATH = "/"
27+
OPENAPI_SWAGGER_UI_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
28+
29+
# flask-smorest Swagger UI top level authorize dialog box
30+
API_SPEC_OPTIONS = {
31+
"components": {
32+
"securitySchemes": {
33+
"access_token": {
34+
"type": "http",
35+
"scheme": "bearer",
36+
"bearerFormat": "JWT",
37+
"description": "Enter your JWT access token",
38+
},
39+
"refresh_token": {
40+
"type": "http",
41+
"scheme": "bearer",
42+
"bearerFormat": "JWT",
43+
"description": "Enter your JWT refresh token",
44+
},
45+
}
46+
}
47+
}
48+
49+
50+
class DevelopmentConfig(Config):
51+
DEBUG = True
52+
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI")
53+
54+
55+
class TestingConfig(Config):
56+
TESTING = True
57+
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
58+
JWT_SECRET_KEY = os.urandom(24).hex()
59+
60+
61+
class ProductionConfig(Config):
62+
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI")

populate_db.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from faker import Faker
2-
from app import app, db
2+
from app import create_app, db
33
from app.models import Category, Subcategory, Product, category_subcategory, subcategory_product
44
import random
55

66

7+
app = create_app()
78
fake = Faker()
89

10+
911
def create_categories(num=5):
1012
categories = []
1113
for _ in range(num):

tests/conftest.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1-
import os
2-
31
import pytest
42

5-
# TODO: Fix hack. Changes the env var before initializing the db for testing
6-
os.environ["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
7-
os.environ["JWT_SECRET_KEY"] = os.urandom(24).hex()
8-
9-
from app import app, db
3+
from app import create_app, db
4+
from config import TestingConfig
105
from tests import utils
116

127

138
@pytest.fixture
14-
def client():
15-
app.config["TESTING"] = True
16-
with app.test_client() as client:
17-
with app.app_context():
18-
db.create_all()
19-
yield client
20-
with app.app_context():
21-
db.drop_all()
9+
def app():
10+
app = create_app(TestingConfig)
11+
12+
# setup
13+
app_context = app.app_context()
14+
app_context.push()
15+
db.create_all()
16+
17+
yield app
18+
19+
# teardown
20+
db.session.remove()
21+
db.drop_all()
22+
app_context.pop()
23+
24+
25+
@pytest.fixture
26+
def client(app):
27+
return app.test_client()
2228

2329

2430
@pytest.fixture

tests/test_auth.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,19 @@ class TestAuth:
1616
@pytest.fixture(autouse=True)
1717
def setup(self, client):
1818
self.client = client
19-
with client.application.app_context():
20-
assert User.query.count() == 0
19+
assert User.query.count() == 0
2120

2221
def _verify_user_in_db(self, email, should_exist=True):
23-
with self.client.application.app_context():
24-
user = User.get(email=email)
25-
if should_exist:
26-
assert user is not None
27-
assert user.email == email
28-
return user
29-
else:
30-
assert user is None
22+
user = User.get(email=email)
23+
if should_exist:
24+
assert user is not None
25+
assert user.email == email
26+
return user
27+
else:
28+
assert user is None
3129

3230
def _count_users(self):
33-
with self.client.application.app_context():
34-
return User.query.count()
31+
return User.query.count()
3532

3633
def _test_invalid_request_data(self, endpoint, expected_status=422):
3734
response = self.client.post(endpoint, json={})
@@ -47,9 +44,7 @@ def _test_invalid_request_data(self, endpoint, expected_status=422):
4744
assert response.status_code == expected_status
4845

4946
def _decode_token(self, token):
50-
# Needs Flask app context for secret/algorithms from current_app.config
51-
with self.client.application.app_context():
52-
return decode_token(token, allow_expired=False)
47+
return decode_token(token, allow_expired=False)
5348

5449
def _assert_jwt_structure(self, token, expected_sub, expected_type, fresh=False):
5550
assert token.count(".") == 2, f"Token does not have three segments: {token}"
@@ -169,8 +164,6 @@ def test_refresh_token_missing_auth(self):
169164
utils.verify_token_error_response(response, "authorization_required")
170165

171166
def test_refresh_token_expired(self):
172-
expired_headers = utils.get_expired_token_headers(
173-
self.client.application.app_context()
174-
)
167+
expired_headers = utils.get_expired_token_headers()
175168
response = self.client.post("/auth/refresh", headers=expired_headers)
176169
utils.verify_token_error_response(response, "token_expired")

0 commit comments

Comments
 (0)