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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ 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] `/category/<category_id: int>/products` - Get product with within category `category`. Returns first page of the paginated results. <br/><br/>
- [GET] `/category/<category_id: int>/products?page=<page_no>` - Get product with within category `category`. 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/>


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

#### Category
- [GET] `/categories` - Get all categories
- [GET] `/category/(int: category_id)` - Get category with category_id
- [GET] `/category/(int: category_id)/subcategories` - Get subcategories within a category_id.
- [DELETE] `/category/(int: category_id)` (Protected) - Delete category with category_id
- [GET] `/categories/(int: category_id)` - Get category with category_id
- [GET] `/categories/(int: category_id)/subcategories` - Get subcategories within a category_id.
- [DELETE] `/categories/(int: category_id)` (Protected) - Delete category with category_id

- [POST] `/category/create` (Protected) - Create a new category
- [POST] `/categories` (Protected) - Create a new category
```
{
"name": "name",
"subcategories": [<subcategory ids>] //optional
}
```

- [PUT] `/category/(int: category_id)/update` (Protected) - Update category with category_id
- [PUT] `/categories/(int: category_id)` (Protected) - Update category with category_id
```
{
"name": "name",
Expand Down
17 changes: 17 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,32 @@
from dotenv import load_dotenv
from flasgger import Swagger
from sqlalchemy import MetaData
from flask_smorest import Api


def register_blueprints():
from app.migrated_routes.category import bp as category_bp
api.register_blueprint(category_bp, url_prefix="/categories")


app = Flask(__name__)

load_dotenv()

# sqlalchemy
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("SQLALCHEMY_DATABASE_URI")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# jwt
app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY")
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=3)
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=3)

# flask-smorest
app.config["API_TITLE"] = "Ecommerce REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"

# PostgreSQL-compatible naming convention (to follow the naming convention already used in the DB)
# https://stackoverflow.com/questions/4107915/postgresql-default-constraint-names
naming_convention = {
Expand All @@ -33,6 +47,9 @@
db = SQLAlchemy(app, metadata=metadata)
migrate = Migrate(app, db)
jwt = JWTManager(app)
api = Api(app)

register_blueprints()


@jwt.expired_token_loader
Expand Down
Empty file added app/migrated_routes/__init__.py
Empty file.
292 changes: 292 additions & 0 deletions app/migrated_routes/category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
from flask.views import MethodView
from flask_jwt_extended import jwt_required
from flask_smorest import Blueprint, abort
from sqlalchemy import exists

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

bp = Blueprint("category", __name__)


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

@bp.response(200, CategoriesOut)
def get(self):
"""
Get All Categories
---
tags:
- Category
description: Get all categories.
responses:
200:
description: A list of categories.
"""
return {"categories": Category.query.all()}

@jwt_required()
@bp.arguments(CategoryIn)
@bp.response(201, CategoryOut)
def post(self, data):
"""
Create Category
---
tags:
- Category
description: Create a new category.
security:
- access_token: []
requestBody:
required: true
description: name - Name of the category <br> subcategories - Array of subcategory ids (optional)
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
subcategories:
type: array
items:
type: integer
responses:
201:
description: Category created successfully.
400:
description: Invalid input.
401:
description: Token expired, missing or invalid.
500:
description: Error occurred.
"""
category = Category(name=data["name"])

if sc_ids := data.get("subcategories"):
subcategories = Subcategory.query.filter(Subcategory.id.in_(sc_ids)).all()
if len(subcategories) != len(sc_ids):
abort(422, message="One or more subcategories not present")

category.subcategories = subcategories

db.session.add(category)
db.session.commit()

return category


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

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

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

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

if sc_ids := data.get("subcategories"):
subcategories = Subcategory.query.filter(Subcategory.id.in_(sc_ids)).all()
if len(subcategories) != len(sc_ids):
abort(422, message="One or more subcategories not present")

category.subcategories.extend(subcategories)

db.session.commit()
return category

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


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

@bp.response(200, SubcategoriesOut)
def get(self, id):
"""
Get Subcategories within a Category.
---
tags:
- Category
description: Get Subcategories within a Category.
parameters:
- in: path
name: id
required: true
type: integer
description: Category ID
responses:
200:
description: Subcategories retrieved successfully.
404:
description: Category not found.
500:
description: Error occurred.
"""
category = Category.query.get_or_404(id)
return {"subcategories": category.subcategories}


@bp.route("/<int:id>/products")
class CategoryProducts(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 Category.
---
tags:
- Category
description: Get Products for a Category.
parameters:
- in: path
name: id
required: true
type: integer
description: Category ID
- in: query
name: page
type: integer
default: 1
description: Page number
responses:
200:
description: Products retrieved successfully.
404:
description: Category not found.
500:
description: Error occurred.
"""
category_exists = db.session.query(exists().where(Category.id == id)).scalar()
if not category_exists:
abort(404)

products = (
Product.query.join(subcategory_product)
.join(
category_subcategory,
onclause=subcategory_product.c.subcategory_id
== category_subcategory.c.subcategory_id,
)
.filter(category_subcategory.c.category_id == id)
.distinct()
.order_by(Product.id.asc())
.paginate(page=page, per_page=CategoryProducts._PER_PAGE, error_out=False)
)

return {"products": products}
Loading