Skip to content

Commit 9b7043e

Browse files
migrate category endpoints to flask-smorest, switching to class based views, blueprints, and marshmallow schemas
1 parent a8638a5 commit 9b7043e

File tree

6 files changed

+367
-278
lines changed

6 files changed

+367
-278
lines changed

app/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,32 @@
88
from dotenv import load_dotenv
99
from flasgger import Swagger
1010
from sqlalchemy import MetaData
11+
from flask_smorest import Api
12+
13+
14+
def register_blueprints():
15+
from app.migrated_routes.category import bp as category_bp
16+
api.register_blueprint(category_bp, url_prefix="/categories")
1117

1218

1319
app = Flask(__name__)
1420

1521
load_dotenv()
22+
23+
# sqlalchemy
1624
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("SQLALCHEMY_DATABASE_URI")
1725
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
1826

27+
# jwt
1928
app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY")
2029
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=3)
2130
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=3)
2231

32+
# flask-smorest
33+
app.config["API_TITLE"] = "Ecommerce REST API"
34+
app.config["API_VERSION"] = "v1"
35+
app.config["OPENAPI_VERSION"] = "3.0.2"
36+
2337
# PostgreSQL-compatible naming convention (to follow the naming convention already used in the DB)
2438
# https://stackoverflow.com/questions/4107915/postgresql-default-constraint-names
2539
naming_convention = {
@@ -33,6 +47,9 @@
3347
db = SQLAlchemy(app, metadata=metadata)
3448
migrate = Migrate(app, db)
3549
jwt = JWTManager(app)
50+
api = Api(app)
51+
52+
register_blueprints()
3653

3754

3855
@jwt.expired_token_loader

app/migrated_routes/__init__.py

Whitespace-only changes.

app/migrated_routes/category.py

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
from flask.views import MethodView
2+
from flask_jwt_extended import jwt_required
3+
from flask_smorest import Blueprint, abort
4+
from sqlalchemy import exists
5+
6+
from app import db
7+
from app.models import (
8+
Category,
9+
Product,
10+
Subcategory,
11+
category_subcategory,
12+
subcategory_product,
13+
)
14+
from app.schemas import (
15+
CategoriesOut,
16+
CategoryIn,
17+
CategoryOut,
18+
PaginationArgs,
19+
ProductsOut,
20+
SubcategoriesOut,
21+
)
22+
23+
bp = Blueprint("category", __name__)
24+
25+
26+
@bp.route("")
27+
class CategoryCollection(MethodView):
28+
init_every_request = False
29+
30+
@bp.response(200, CategoriesOut)
31+
def get(self):
32+
"""
33+
Get All Categories
34+
---
35+
tags:
36+
- Category
37+
description: Get all categories.
38+
responses:
39+
200:
40+
description: A list of categories.
41+
"""
42+
return {"categories": Category.query.all()}
43+
44+
@jwt_required()
45+
@bp.arguments(CategoryIn)
46+
@bp.response(201, CategoryOut)
47+
def post(self, data):
48+
"""
49+
Create Category
50+
---
51+
tags:
52+
- Category
53+
description: Create a new category.
54+
security:
55+
- access_token: []
56+
requestBody:
57+
required: true
58+
description: name - Name of the category <br> subcategories - Array of subcategory ids (optional)
59+
content:
60+
application/json:
61+
schema:
62+
type: object
63+
required:
64+
- name
65+
properties:
66+
name:
67+
type: string
68+
subcategories:
69+
type: array
70+
items:
71+
type: integer
72+
responses:
73+
201:
74+
description: Category created successfully.
75+
400:
76+
description: Invalid input.
77+
401:
78+
description: Token expired, missing or invalid.
79+
500:
80+
description: Error occurred.
81+
"""
82+
category = Category(name=data["name"])
83+
84+
if sc_ids := data.get("subcategories"):
85+
subcategories = Subcategory.query.filter(Subcategory.id.in_(sc_ids)).all()
86+
if len(subcategories) != len(sc_ids):
87+
abort(422, message="One or more subcategories not present")
88+
89+
category.subcategories = subcategories
90+
91+
db.session.add(category)
92+
db.session.commit()
93+
94+
return category
95+
96+
97+
@bp.route("/<int:id>")
98+
class CategoryById(MethodView):
99+
init_every_request = False
100+
101+
def _get(self, id):
102+
return Category.query.get_or_404(id)
103+
104+
@bp.response(200, CategoryOut)
105+
def get(self, id):
106+
"""
107+
Get Category
108+
---
109+
tags:
110+
- Category
111+
description: Get a category by ID.
112+
parameters:
113+
- in: path
114+
name: id
115+
required: true
116+
type: integer
117+
description: Category ID
118+
responses:
119+
200:
120+
description: Category retrieved successfully.
121+
404:
122+
description: Category not found.
123+
"""
124+
return self._get(id)
125+
126+
@jwt_required()
127+
@bp.arguments(CategoryIn(partial=("name",)))
128+
@bp.response(200, CategoryOut)
129+
def put(self, data, id):
130+
"""
131+
Update Category
132+
---
133+
tags:
134+
- Category
135+
description: Update an existing category.
136+
security:
137+
- access_token: []
138+
parameters:
139+
- in: path
140+
name: id
141+
required: true
142+
type: integer
143+
description: Category ID
144+
requestBody:
145+
required: true
146+
description: name - Name of the category (optional) <br> subcategories - Array of subcategory ids
147+
content:
148+
application/json:
149+
schema:
150+
type: object
151+
properties:
152+
name:
153+
type: string
154+
subcategories:
155+
type: array
156+
items:
157+
type: integer
158+
responses:
159+
201:
160+
description: Category updated successfully.
161+
400:
162+
description: Invalid input.
163+
404:
164+
description: Category not found.
165+
500:
166+
description: Error occurred.
167+
"""
168+
category = self._get(id)
169+
if name := data.get("name"):
170+
category.name = name
171+
172+
if sc_ids := data.get("subcategories"):
173+
subcategories = Subcategory.query.filter(Subcategory.id.in_(sc_ids)).all()
174+
if len(subcategories) != len(sc_ids):
175+
abort(422, message="One or more subcategories not present")
176+
177+
category.subcategories.extend(subcategories)
178+
179+
db.session.commit()
180+
return category
181+
182+
@jwt_required()
183+
@bp.response(204)
184+
def delete(self, id):
185+
"""
186+
Delete Category
187+
---
188+
tags:
189+
- Category
190+
description: Delete a category by ID.
191+
security:
192+
- access_token: []
193+
parameters:
194+
- in: path
195+
name: id
196+
required: true
197+
type: integer
198+
description: Category ID
199+
responses:
200+
200:
201+
description: Category deleted successfully.
202+
404:
203+
description: Category not found.
204+
500:
205+
description: Error occurred.
206+
"""
207+
category = self._get(id)
208+
db.session.delete(category)
209+
db.session.commit()
210+
211+
212+
@bp.route("/<int:id>/subcategories")
213+
class CategorySubcategories(MethodView):
214+
init_every_request = False
215+
216+
@bp.response(200, SubcategoriesOut)
217+
def get(self, id):
218+
"""
219+
Get Subcategories within a Category.
220+
---
221+
tags:
222+
- Category
223+
description: Get Subcategories within a Category.
224+
parameters:
225+
- in: path
226+
name: id
227+
required: true
228+
type: integer
229+
description: Category ID
230+
responses:
231+
200:
232+
description: Subcategories retrieved successfully.
233+
404:
234+
description: Category not found.
235+
500:
236+
description: Error occurred.
237+
"""
238+
category = Category.query.get_or_404(id)
239+
return {"subcategories": category.subcategories}
240+
241+
242+
@bp.route("/<int:id>/products")
243+
class CategoryProducts(MethodView):
244+
init_every_request = False
245+
_PER_PAGE = 10
246+
247+
@bp.arguments(PaginationArgs, location="query", as_kwargs=True)
248+
@bp.response(200, ProductsOut)
249+
def get(self, id, page):
250+
"""
251+
Get Products within a Category.
252+
---
253+
tags:
254+
- Category
255+
description: Get Products for a Category.
256+
parameters:
257+
- in: path
258+
name: id
259+
required: true
260+
type: integer
261+
description: Category ID
262+
- in: query
263+
name: page
264+
type: integer
265+
default: 1
266+
description: Page number
267+
responses:
268+
200:
269+
description: Products retrieved successfully.
270+
404:
271+
description: Category not found.
272+
500:
273+
description: Error occurred.
274+
"""
275+
category_exists = db.session.query(exists().where(Category.id == id)).scalar()
276+
if not category_exists:
277+
abort(404)
278+
279+
products = (
280+
Product.query.join(subcategory_product)
281+
.join(
282+
category_subcategory,
283+
onclause=subcategory_product.c.subcategory_id
284+
== category_subcategory.c.subcategory_id,
285+
)
286+
.filter(category_subcategory.c.category_id == id)
287+
.distinct()
288+
.order_by(Product.id.asc())
289+
.paginate(page=page, per_page=CategoryProducts._PER_PAGE, error_out=False)
290+
)
291+
292+
return {"products": products}

0 commit comments

Comments
 (0)