Skip to content

Commit 8e9ab0a

Browse files
migrate product endpoints to flask-smorest
1 parent 1697146 commit 8e9ab0a

File tree

4 files changed

+341
-276
lines changed

4 files changed

+341
-276
lines changed

app/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
def register_blueprints():
1515
from app.migrated_routes.category import bp as category_bp
1616
from app.migrated_routes.subcategory import bp as subcategory_bp
17+
from app.migrated_routes.product import bp as product_bp
1718

1819
api.register_blueprint(category_bp, url_prefix="/categories")
1920
api.register_blueprint(subcategory_bp, url_prefix="/subcategories")
21+
api.register_blueprint(product_bp, url_prefix="/products")
2022

2123

2224
app = Flask(__name__)

app/migrated_routes/product.py

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

0 commit comments

Comments
 (0)