diff --git a/app/__init__.py b/app/__init__.py
index 11e93a7..5f11444 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -6,7 +6,6 @@
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from dotenv import load_dotenv
-from flasgger import Swagger
from sqlalchemy import MetaData
from flask_smorest import Api
@@ -41,6 +40,31 @@ def register_blueprints():
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"
+# flask-smorest openapi swagger
+app.config["OPENAPI_URL_PREFIX"] = "/"
+app.config["OPENAPI_SWAGGER_UI_PATH"] = "/"
+app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
+
+# flask-smorest Swagger UI top level authorize dialog box
+app.config["API_SPEC_OPTIONS"] = {
+ "components": {
+ "securitySchemes": {
+ "access_token": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT",
+ "description": "Enter your JWT access token",
+ },
+ "refresh_token": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT",
+ "description": "Enter your JWT refresh token",
+ },
+ }
+ }
+}
+
# 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 = {
@@ -73,47 +97,3 @@ def invalid_token_callback(error):
@jwt.unauthorized_loader
def missing_token_callback(error):
return jsonify(code="authorization_required", error="JWT needed for this operation. Login, if needed."), 401
-
-
-swagger_config = {
- 'openapi': '3.0.0',
- 'title': 'Ecommerce REST API',
- 'version': None,
- 'termsOfService': None,
- 'description': None,
- 'specs': [
- {
- "endpoint": 'api_spec',
- "route": '/api_spec.json',
- "rule_filter": lambda rule: True, # all in
- "model_filter": lambda tag: True, # all in
- }
- ],
- 'components': {
- 'securitySchemes': {
- 'access_token': {
- 'type': 'http',
- 'scheme': 'bearer',
- 'bearerFormat': 'JWT',
- 'description': 'Enter your JWT access token'
- },
- 'refresh_token': {
- 'type': 'http',
- 'scheme': 'bearer',
- 'bearerFormat': 'JWT',
- 'description': 'Enter your JWT refresh token'
- }
- }
- },
- 'specs_route': '/'
-}
-
-template = {
- 'tags': [
- {'name': 'Category', 'description': 'Operations with categories'},
- {'name': 'Subcategory', 'description': 'Operations with subategories'},
- {'name': 'Product', 'description': 'Operations with products'},
- {'name': 'User', 'description': 'Operations with users'},
- ]
-}
-swagger = Swagger(app, template=template, config=swagger_config, merge=True)
diff --git a/app/routes/auth.py b/app/routes/auth.py
index e52c8ce..1a5b0d9 100644
--- a/app/routes/auth.py
+++ b/app/routes/auth.py
@@ -14,46 +14,15 @@
from app.models import User
from app.schemas import AuthIn, AuthOut
-bp = Blueprint("auth", __name__)
+bp = Blueprint("Auth", __name__)
@bp.route("/register")
class Register(MethodView):
+ @bp.doc(summary="Register a new user")
@bp.arguments(AuthIn)
@bp.response(201)
def post(self, data):
- """
- Register a new user.
- ---
- tags:
- - User
- description: Register a new user.
- requestBody:
- required: true
- description: email - Email id
password - Password
- content:
- application/json:
- schema:
- type: object
- required:
- - email
- - password
- properties:
- email:
- type: string
- password:
- type: string
- responses:
- 201:
- description: User registered successfully.
- 400:
- description: Invalid input.
- 409:
- description: Email already exists.
- 500:
- description: Internal Server Error.
- """
-
user = User()
user.set_password(data["password"])
@@ -74,42 +43,10 @@ def post(self, data):
@bp.route("/login")
class Login(MethodView):
- """Login a user and return access & refresh tokens."""
-
+ @bp.doc(summary="Login a user")
@bp.arguments(AuthIn)
@bp.response(200, AuthOut)
def post(self, data):
- """
- Login a user.
- ---
- tags:
- - User
- description: Login a user.
- requestBody:
- required: true
- description: email - Email id
password - Password
- content:
- application/json:
- schema:
- type: object
- required:
- - email
- - password
- properties:
- email:
- type: string
- password:
- type: string
- responses:
- 200:
- description: User logged in successfully.
- 400:
- description: Invalid input.
- 401:
- description: Invalid email or password.
- 500:
- description: Internal Server Error.
- """
user = User.get(email=data["email"])
if not user or not user.check_password(data["password"]):
return abort(
@@ -129,26 +66,12 @@ def post(self, data):
@bp.route("/refresh")
class Refresh(MethodView):
- """Get new access token using your refresh token."""
-
@jwt_required(refresh=True)
+ @bp.doc(
+ summary="Get new access token using your refresh token",
+ security=[{"refresh_token": []}],
+ )
@bp.response(200, AuthOut(only=("access_token",)))
def post(self):
- """
- Get new access token using your refresh token
- ---
- tags:
- - User
- description: Get new access token using your refresh token.
- security:
- - refresh_token: []
- responses:
- 200:
- description: New access token.
- 401:
- description: Token expired, missing or invalid.
- 500:
- description: Internal Server Error.
- """
identity = get_jwt_identity()
return {"access_token": create_access_token(identity=identity, fresh=False)}
diff --git a/app/routes/category.py b/app/routes/category.py
index 1be817c..d684825 100644
--- a/app/routes/category.py
+++ b/app/routes/category.py
@@ -21,7 +21,7 @@
SubcategoriesOut,
)
-bp = Blueprint("category", __name__)
+bp = Blueprint("Category", __name__)
@bp.route("")
@@ -41,58 +41,16 @@ def _get_name_unique_constraint():
_NAME_UNIQUE_CONSTRAINT = _get_name_unique_constraint()
+ @bp.doc(summary="Get All Categories")
@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.doc(summary="Create Category", security=[{"access_token": []}])
@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
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"):
@@ -125,70 +83,16 @@ class CategoryById(MethodView):
def _get(self, id):
return Category.query.get_or_404(id)
+ @bp.doc(summary="Get Category")
@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.doc(summary="Update Category", security=[{"access_token": []}])
@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)
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
@@ -221,30 +125,9 @@ def put(self, data, id):
return category
@jwt_required()
+ @bp.doc(summary="Delete Category", security=[{"access_token": []}])
@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()
@@ -254,28 +137,9 @@ def delete(self, id):
class CategorySubcategories(MethodView):
init_every_request = False
+ @bp.doc(summary="Get Subcategories within a Category")
@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}
@@ -285,34 +149,10 @@ class CategoryProducts(MethodView):
init_every_request = False
_PER_PAGE = 10
+ @bp.doc(summary="Get Products within a Category")
@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)
diff --git a/app/routes/product.py b/app/routes/product.py
index b0f3614..48a35af 100644
--- a/app/routes/product.py
+++ b/app/routes/product.py
@@ -20,7 +20,7 @@
SubcategoriesOut,
)
-bp = Blueprint("product", __name__)
+bp = Blueprint("Product", __name__)
@bp.route("")
@@ -44,30 +44,14 @@ def _get_name_unique_constraint():
def _get_by_name(self, name):
return Product.query.filter(Product.name == name)
+ @bp.doc(
+ summary="Get All Products",
+ description="If name is passed, filters the result to that single product, if present",
+ )
@bp.arguments(NameArgs, location="query", as_kwargs=True)
@bp.arguments(PaginationArgs, location="query", as_kwargs=True)
@bp.response(200, ProductsOut)
def get(self, name, page):
- """
- Get All Products
- ---
- tags:
- - Product
- description: Get all products.
- parameters:
- - in: query
- name: page
- type: integer
- default: 1
- description: Page number
- - in: query
- name: name
- type: string
- description: Name
- responses:
- 200:
- description: Product by name or a paginated list of all products.
- """
if name is not None:
products = self._get_by_name(name)
else:
@@ -78,45 +62,10 @@ def get(self, name, page):
return {"products": products}
@jwt_required()
+ @bp.doc(summary="Create Product", security=[{"access_token": []}])
@bp.arguments(ProductIn)
@bp.response(201, ProductOut)
def post(self, data):
- """
- Create Product
- ---
- tags:
- - Product
- description: Create a new product.
- security:
- - access_token: []
- requestBody:
- required: true
- description: name - Name of the product
description - Description of the product (optional)
subcategories - Array of subcategory ids (optional)
- content:
- application/json:
- schema:
- type: object
- required:
- - name
- properties:
- name:
- type: string
- description:
- type: string
- subcategories:
- type: array
- items:
- type: integer
- responses:
- 201:
- description: Product created successfully.
- 400:
- description: Invalid input.
- 401:
- description: Token expired, missing or invalid.
- 500:
- description: Error occurred.
- """
product = Product(name=data["name"], description=data.get("description"))
if sc_ids := data.get("subcategories"):
@@ -148,74 +97,16 @@ class ProductById(MethodView):
def _get(self, id):
return Product.query.get_or_404(id)
+ @bp.doc(summary="Get Product")
@bp.response(200, ProductOut)
def get(self, id):
- """
- Get Product
- ---
- tags:
- - Product
- description: Get a product by ID.
- parameters:
- - in: path
- name: id
- required: true
- type: integer
- description: Product ID
- responses:
- 200:
- description: Product retrieved successfully.
- 404:
- description: Product not found.
- """
return self._get(id)
@jwt_required()
+ @bp.doc(summary="Update Product", security=[{"access_token": []}])
@bp.arguments(ProductIn(partial=("name",)))
@bp.response(200, ProductOut)
def put(self, data, id):
- """
- Update Product
- ---
- tags:
- - Product
- description: Update an existing product.
- security:
- - access_token: []
- consumes:
- - application/json
- parameters:
- - in: path
- name: id
- required: true
- type: integer
- description: Product ID
- requestBody:
- required: true
- description: name - Name of the product (optional)
description = Description of the product (optional)
subcategories - Array of subcategory ids (optional)
- content:
- application/json:
- schema:
- type: object
- properties:
- name:
- type: string
- description:
- type: string
- subcategories:
- type: array
- items:
- type: integer
- responses:
- 201:
- description: Product updated successfully.
- 400:
- description: Invalid input.
- 404:
- description: Product not found.
- 500:
- description: Error occurred.
- """
product = self._get(id)
if name := data.get("name"):
@@ -252,30 +143,9 @@ def put(self, data, id):
return product
@jwt_required()
+ @bp.doc(summary="Delete Product", security=[{"access_token": []}])
@bp.response(204)
def delete(self, id):
- """
- Delete Product
- ---
- tags:
- - Product
- description: Delete a product by ID.
- security:
- - access_token: []
- parameters:
- - in: path
- name: id
- required: true
- type: integer
- description: Product ID
- responses:
- 200:
- description: Product deleted successfully.
- 404:
- description: Product not found.
- 500:
- description: Error occurred.
- """
product = self._get(id)
db.session.delete(product)
db.session.commit()
@@ -285,27 +155,8 @@ def delete(self, id):
class ProductSubcategories(MethodView):
init_every_request = False
+ @bp.doc(summary="Get Subcategories related to a Product")
@bp.response(200, SubcategoriesOut)
def get(self, id):
- """
- Get Subcategories related to a Product.
- ---
- tags:
- - Product
- description: Get Subcategories related to a Product.
- parameters:
- - in: path
- name: id
- required: true
- type: integer
- description: Product ID
- responses:
- 200:
- description: Subcategories retrieved successfully.
- 404:
- description: Product not found.
- 500:
- description: Error occurred.
- """
product = Product.query.get_or_404(id)
return {"subcategories": product.subcategories}
diff --git a/app/routes/subcategory.py b/app/routes/subcategory.py
index 635fab2..371b632 100644
--- a/app/routes/subcategory.py
+++ b/app/routes/subcategory.py
@@ -22,7 +22,7 @@
SubcategoryOut,
)
-bp = Blueprint("subcategory", __name__)
+bp = Blueprint("Subcategory", __name__)
@bp.route("")
@@ -42,60 +42,16 @@ def _get_name_unique_constraint():
_NAME_UNIQUE_CONSTRAINT = _get_name_unique_constraint()
+ @bp.doc(summary="Get All Subcategories")
@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.doc(summary="Create Subcategory", security=[{"access_token": []}])
@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
categories - Array of category ids (optional)
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"):
@@ -133,74 +89,16 @@ class SubcategoryById(MethodView):
def _get(self, id):
return Subcategory.query.get_or_404(id)
+ @bp.doc(summary="Get Subcategory")
@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.doc(summary="Update Subcategory", security=[{"access_token": []}])
@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)
categories - Array of category ids (optional)
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
@@ -244,30 +142,9 @@ def put(self, data, id):
return subcategory
@jwt_required()
+ @bp.doc(summary="Delete Subcategory", security=[{"access_token": []}])
@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()
@@ -277,28 +154,9 @@ def delete(self, id):
class SubcategoryCategories(MethodView):
init_every_request = False
+ @bp.doc(summary="Get Categories related to a Subcategory")
@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}
@@ -308,34 +166,10 @@ class SubcategoryProducts(MethodView):
init_every_request = False
_PER_PAGE = 10
+ @bp.doc(summary="Get Products within a Subcategory")
@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(
diff --git a/requirements.txt b/requirements.txt
index 886eff0..b7a4dae 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,6 @@ Flask-SQLAlchemy==3.1.1
# psycopg2==2.9.10 # vercel complains!
psycopg2-binary==2.9.10
python-dotenv==1.0.1
-flasgger==0.9.7.1
Flask-Migrate==4.1.0
email_validator==1.3.1
email-normalize==0.2.1