Skip to content

Commit 3be44fe

Browse files
Merge pull request #20 from piyush-jaiswal/feature/flask-smorest-subcategory
Feature/flask smorest subcategory
2 parents 95f1931 + 56a82a5 commit 3be44fe

File tree

8 files changed

+454
-329
lines changed

8 files changed

+454
-329
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Test the API using Swagger UI (`/` route), Postman, cURL or your preferred HTTP
6464

6565
#### Fetch products using name, category, subcategory
6666
- [GET] `/product/<name: string>` - Get product with name: `name` <br/><br/>
67-
- [GET] `/subcategory/<subcategory_id: int>/products?page=<page_no>` - Get product with within subcategory `subcategory`. Returns `page_no` of the paginated results. <br/><br/>
67+
- [GET] `/subcategories/<subcategory_id: int>/products?page=<page_no>` - Get product with within subcategory `subcategory`. Returns `page_no` of the paginated results. <br/><br/>
6868
- [GET] `/categories/<category_id: int>/products` - Get product with within category `category`. Returns first page of the paginated results. <br/><br/>
6969
- [GET] `/categories/<category_id: int>/products?page=<page_no>` - Get product with within category `category`. Returns `page_no` of the paginated results. <br/><br/>
7070

@@ -124,11 +124,11 @@ Test the API using Swagger UI (`/` route), Postman, cURL or your preferred HTTP
124124

125125
#### Subcategory
126126
- [GET] `/subcategories` - Get all subcategories
127-
- [GET] `/subcategory/(int: subcategory_id)` - Get subcategory with subcategory_id
128-
- [GET] `/subcategory/(int: subcategory_id)/categories` - Get categories related to subcategory_id
129-
- [DELETE] `/subcategory/(int: subcategory_id)` (Protected) - Delete subcategory with subcategory_id
127+
- [GET] `/subcategories/(int: subcategory_id)` - Get subcategory with subcategory_id
128+
- [GET] `/subcategories/(int: subcategory_id)/categories` - Get categories related to subcategory_id
129+
- [DELETE] `/subcategories/(int: subcategory_id)` (Protected) - Delete subcategory with subcategory_id
130130

131-
- [POST] `/subcategory/create` (Protected) - Create a new subcategory
131+
- [POST] `/subcategories` (Protected) - Create a new subcategory
132132
```
133133
{
134134
"name": "name",
@@ -137,7 +137,7 @@ Test the API using Swagger UI (`/` route), Postman, cURL or your preferred HTTP
137137
}
138138
```
139139

140-
- [PUT] `/subcategory/(int: subcategory_id)/update` (Protected) - Update subcategory with subcategory_id
140+
- [PUT] `/subcategories/(int: subcategory_id)` (Protected) - Update subcategory with subcategory_id
141141
```
142142
{
143143
"name": "name",

app/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
def register_blueprints():
1515
from app.migrated_routes.category import bp as category_bp
16+
from app.migrated_routes.subcategory import bp as subcategory_bp
17+
1618
api.register_blueprint(category_bp, url_prefix="/categories")
19+
api.register_blueprint(subcategory_bp, url_prefix="/subcategories")
1720

1821

1922
app = Flask(__name__)

app/migrated_routes/subcategory.py

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

0 commit comments

Comments
 (0)