Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Test the API using Swagger UI (`/` route), Postman, cURL or your preferred HTTP

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

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

#### Subcategory
- [GET] `/subcategories` - Get all subcategories
- [GET] `/subcategory/(int: subcategory_id)` - Get subcategory with subcategory_id
- [GET] `/subcategory/(int: subcategory_id)/categories` - Get categories related to subcategory_id
- [DELETE] `/subcategory/(int: subcategory_id)` (Protected) - Delete subcategory with subcategory_id
- [GET] `/subcategories/(int: subcategory_id)` - Get subcategory with subcategory_id
- [GET] `/subcategories/(int: subcategory_id)/categories` - Get categories related to subcategory_id
- [DELETE] `/subcategories/(int: subcategory_id)` (Protected) - Delete subcategory with subcategory_id

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

- [PUT] `/subcategory/(int: subcategory_id)/update` (Protected) - Update subcategory with subcategory_id
- [PUT] `/subcategories/(int: subcategory_id)` (Protected) - Update subcategory with subcategory_id
```
{
"name": "name",
Expand Down
3 changes: 3 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@

def register_blueprints():
from app.migrated_routes.category import bp as category_bp
from app.migrated_routes.subcategory import bp as subcategory_bp

api.register_blueprint(category_bp, url_prefix="/categories")
api.register_blueprint(subcategory_bp, url_prefix="/subcategories")


app = Flask(__name__)
Expand Down
339 changes: 339 additions & 0 deletions app/migrated_routes/subcategory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
from flask.views import MethodView
from flask_jwt_extended import jwt_required
from flask_smorest import Blueprint, abort
from psycopg2.errors import UniqueViolation
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hard dependency on psycopg2 in SQLite/dev environments.

Importing psycopg2.errors will crash if psycopg2 isn’t installed. Guard the import and feature-detect at runtime.

Apply:

-from psycopg2.errors import UniqueViolation
+try:
+    from psycopg2.errors import UniqueViolation as PGUniqueViolation
+except Exception:  # psycopg2 not installed or different driver
+    PGUniqueViolation = ()  # so isinstance(..., PGUniqueViolation) is always False

And update usages:

-            if (
-                isinstance(ie.orig, UniqueViolation)
+            if (
+                isinstance(ie.orig, PGUniqueViolation)
                 and getattr(ie.orig, "diag", None)
                 and ie.orig.diag.constraint_name
                 == SubcategoryCollection._NAME_UNIQUE_CONSTRAINT.name
             ):

…and similarly in put().

🤖 Prompt for AI Agents
In app/migrated_routes/subcategory.py around line 4, the direct import "from
psycopg2.errors import UniqueViolation" creates a hard dependency that will
crash in environments without psycopg2; wrap the import in a try/except
ImportError (or ModuleNotFoundError) and assign a fallback (e.g.,
UniqueViolation = None or a generic Exception subclass) so the module can import
in SQLite/dev setups. Then, in the post() and put() handlers, change the error
handling to first check if UniqueViolation is not None before using it in an
except clause (or catch Exception and inspect getattr(err, '__class__', None) or
err.pgcode/name to detect unique-violation semantics), so you only rely on
psycopg2-specific exception classes when psycopg2 is actually available. Ensure
behavior is unchanged when psycopg2 is present and that code paths gracefully
handle the fallback in dev.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already discussed in #18

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch PR #18 details and comments to understand the prior discussion
gh pr view 18 --json title,body,comments --jq '{title, body, comments: [.comments[] | {author: .author.login, body: .body}]}'

Length of output: 13165


@piyush-jaiswal, understood! Since you've already addressed this pattern in #18 and are applying it consistently here, I'll note that decision. Thanks for the clarification.

from sqlalchemy import UniqueConstraint
from sqlalchemy.exc import IntegrityError

from app import db
from app.models import (
Category,
Product,
Subcategory,
category_subcategory,
subcategory_product,
)
from app.schemas import (
CategoriesOut,
PaginationArgs,
ProductsOut,
SubcategoriesOut,
SubcategoryIn,
SubcategoryOut,
)

bp = Blueprint("subcategory", __name__)


@bp.route("")
class SubcategoryCollection(MethodView):
init_every_request = False

@staticmethod
def _get_name_unique_constraint():
name_col = Subcategory.__table__.c.name
return next(
con
for con in Subcategory.__table__.constraints
if isinstance(con, UniqueConstraint)
and len(con.columns) == 1
and con.columns.contains_column(name_col)
)

_NAME_UNIQUE_CONSTRAINT = _get_name_unique_constraint()

@bp.response(200, SubcategoriesOut)
def get(self):
"""
Get All Subcategories
---
tags:
- Subcategory
description: Get all subcategories.
responses:
200:
description: A list of subcategories.
"""
return {"subcategories": Subcategory.query.all()}

@jwt_required()
@bp.arguments(SubcategoryIn)
@bp.response(201, SubcategoryOut)
def post(self, data):
"""
Create Subcategory
---
tags:
- Subcategory
description: Create a new subcategory.
security:
- access_token: []
requestBody:
required: true
description: name - Name of the subcategory <br> categories - Array of category ids (optional) <br> products - Array of product ids (optional)
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
categories:
type: array
items:
type: integer
products:
type: array
items:
type: integer
responses:
201:
description: Subcategory created successfully.
400:
description: Invalid input.
500:
description: Error occurred.
"""
subcategory = Subcategory(name=data["name"])

if c_ids := data.get("categories"):
categories = Category.query.filter(Category.id.in_(c_ids)).all()
if len(categories) != len(c_ids):
abort(422, message="One or more categories not present")
subcategory.categories = categories

if p_ids := data.get("products"):
products = Product.query.filter(Product.id.in_(p_ids)).all()
if len(products) != len(p_ids):
abort(422, message="One or more products not present")
subcategory.products = products

try:
db.session.add(subcategory)
db.session.commit()
except IntegrityError as ie:
db.session.rollback()
if (
isinstance(ie.orig, UniqueViolation)
and ie.orig.diag.constraint_name
== SubcategoryCollection._NAME_UNIQUE_CONSTRAINT.name
):
abort(409, message="Subcategory with this name already exists")
raise

return subcategory


@bp.route("/<int:id>")
class SubcategoryById(MethodView):
init_every_request = False

def _get(self, id):
return Subcategory.query.get_or_404(id)

@bp.response(200, SubcategoryOut)
def get(self, id):
"""
Get Subcategory
---
tags:
- Subcategory
description: Get a subcategory by ID.
parameters:
- in: path
name: id
required: true
type: integer
description: Subcategory ID
responses:
200:
description: Subcategory retrieved successfully.
404:
description: Subcategory not found.
"""
return self._get(id)

@jwt_required()
@bp.arguments(SubcategoryIn(partial=("name",)))
@bp.response(200, SubcategoryOut)
def put(self, data, id):
"""
Update Subcategory
---
tags:
- Subcategory
description: Update an existing subcategory.
security:
- access_token: []
parameters:
- in: path
name: id
required: true
type: integer
description: Subcategory ID
requestBody:
required: true
description: name - Name of the subcategory (optional) <br> categories - Array of category ids (optional) <br> products - Array of product ids (optional)
content:
application/json:
schema:
type: object
properties:
name:
type: string
categories:
type: array
items:
type: integer
products:
type: array
items:
type: integer
responses:
200:
description: Subcategory updated successfully.
400:
description: Invalid input.
404:
description: Subcategory not found.
500:
description: Error occurred.
"""
subcategory = self._get(id)
if name := data.get("name"):
subcategory.name = name

with db.session.no_autoflush:
if c_ids := data.get("categories"):
categories = Category.query.filter(Category.id.in_(c_ids)).all()
if len(categories) != len(c_ids):
abort(422, message="One or more categories not present")
subcategory.categories.extend(categories)

if p_ids := data.get("products"):
products = Product.query.filter(Product.id.in_(p_ids)).all()
if len(products) != len(p_ids):
abort(422, message="One or more products not present")
subcategory.products.extend(products)

try:
db.session.commit()
except IntegrityError as ie:
db.session.rollback()
if (
isinstance(ie.orig, UniqueViolation)
and ie.orig.diag.constraint_name
== category_subcategory.primary_key.name
):
abort(409, message="Subcategory and category already linked")
if (
isinstance(ie.orig, UniqueViolation)
and ie.orig.diag.constraint_name == subcategory_product.primary_key.name
):
abort(409, message="Subcategory and product already linked")
raise

return subcategory

@jwt_required()
@bp.response(204)
def delete(self, id):
"""
Delete Subcategory
---
tags:
- Subcategory
description: Delete a subcategory by ID.
security:
- access_token: []
parameters:
- in: path
name: id
required: true
type: integer
description: Subcategory ID
responses:
204:
description: Subcategory deleted successfully.
404:
description: Subcategory not found.
500:
description: Error occurred.
"""
subcategory = self._get(id)
db.session.delete(subcategory)
db.session.commit()


@bp.route("/<int:id>/categories")
class SubcategoryCategories(MethodView):
init_every_request = False

@bp.response(200, CategoriesOut)
def get(self, id):
"""
Get Categories related to a Subcategory.
---
tags:
- Subcategory
description: Get Categories related to a Subcategory.
parameters:
- in: path
name: id
required: true
type: integer
description: Subcategory ID
responses:
200:
description: Categories retrieved successfully.
404:
description: Subcategory not found.
500:
description: Error occurred.
"""
subcategory = Subcategory.query.get_or_404(id)
return {"categories": subcategory.categories}


@bp.route("/<int:id>/products")
class SubcategoryProducts(MethodView):
init_every_request = False
_PER_PAGE = 10

@bp.arguments(PaginationArgs, location="query", as_kwargs=True)
@bp.response(200, ProductsOut)
def get(self, id, page):
"""
Get Products within a Subcategory.
---
tags:
- Subcategory
description: Get products for a subcategory.
parameters:
- in: path
name: id
required: true
type: integer
description: Subcategory ID
- in: query
name: page
type: integer
default: 1
description: Page number
responses:
200:
description: Products retrieved successfully.
404:
description: Subcategory not found.
500:
description: Error occurred.
"""
subcategory = Subcategory.query.get_or_404(id)

products = subcategory.products.order_by(Product.id.asc()).paginate(
page=page, per_page=SubcategoryProducts._PER_PAGE, error_out=False
)

return {"products": products}
Comment on lines +333 to +339
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: returning Pagination object breaks schema; return items.

ProductsOut expects a list; Pagination isn’t a list/iterable for Marshmallow. Return products.items.

Apply:

-        return {"products": products}
+        return {"products": products.items}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subcategory = Subcategory.query.get_or_404(id)
products = subcategory.products.order_by(Product.id.asc()).paginate(
page=page, per_page=SubcategoryProducts._PER_PAGE, error_out=False
)
return {"products": products}
subcategory = Subcategory.query.get_or_404(id)
products = subcategory.products.order_by(Product.id.asc()).paginate(
page=page, per_page=SubcategoryProducts._PER_PAGE, error_out=False
)
return {"products": products.items}
🤖 Prompt for AI Agents
In app/migrated_routes/subcategory.py around lines 333 to 339, the handler
currently returns the Flask-SQLAlchemy Pagination object which breaks the
Marshmallow ProductsOut schema; instead return the actual list by replacing the
Pagination return with the paginated.items list (e.g., return {"products":
products.items}) so the serializer receives a plain list. Ensure any clients
that need pagination metadata still get it separately if required.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it works. tests pass and manually verified as well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@piyush-jaiswal Great to hear that the fix works correctly! Thanks for verifying both through tests and manual testing. 🎉


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Loading