Skip to content

Commit

Permalink
set up flask_restx and refactor LIST CREATE READ
Browse files Browse the repository at this point in the history
  • Loading branch information
ZzSteven-Wang committed Dec 8, 2023
1 parent 7711642 commit 811a972
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 83 deletions.
2 changes: 1 addition & 1 deletion features/steps/product_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def step_impl(context):
"""Delete all Products and load new ones"""

# List all of the products and delete them one by one
rest_endpoint = f"{context.base_url}/products"
rest_endpoint = f"{context.base_url}/api/products"
context.resp = requests.get(rest_endpoint)
assert context.resp.status_code == HTTP_200_OK
for products in context.resp.json():
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ SQLAlchemy==2.0.0

# Runtime dependencies
Flask==2.3.2
flask-restx==1.1.0
cloudant==2.15.0
retry2==0.9.5
Flask-SQLAlchemy==3.0.2
psycopg2==2.9.5
# psycopg2-binary==2.9.5
Expand Down
13 changes: 13 additions & 0 deletions service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,26 @@
"""
import sys
from flask import Flask
from flask_restx import Api
from service import config
from service.common import log_handlers

# Create Flask application
app = Flask(__name__)
app.config.from_object(config)

api = Api(
app,
version="1.0.0",
title="Product Demo REST API Service",
description="This is a sample Product server.",
default="products",
default_label="Product operations",
doc="/apidocs", # default also could use doc='/apidocs/'
prefix="/api",
)


# Dependencies require we import the routes AFTER the Flask app is created
# pylint: disable=wrong-import-position, wrong-import-order, cyclic-import
from service import routes, models # noqa: E402, E261
Expand Down
292 changes: 217 additions & 75 deletions service/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
Describe what your service does here
"""

from flask import jsonify, request, abort, url_for
from flask import jsonify, request, abort
from flask_restx import Resource, fields, reqparse, inputs
from service.common import status # HTTP Status Codes
from service.models import Product, Category, db

# Import Flask application
from . import app
from . import app, api


############################################################
Expand Down Expand Up @@ -65,71 +66,212 @@ def index():
# }
return app.send_static_file("index.html")


# Define the model so that the docs reflect what can be sent
create_model = api.model(
"Product",
{
"name": fields.String(required=True, description="The name of the Product"),
"description": fields.String(
required=True,
description="The description of Product",
),
"price": fields.Float(
required=True,
description="The price of Product",
),
"available": fields.Boolean(
required=True, description="Is the Product available for purchase?"
),
"image_url": fields.String(
required=True,
description="The image url of Product",
),
# pylint: disable=protected-access
"category": fields.String(
enum=Category._member_names_,
description="he category of Product (e.g., ELECTRONICS, FOOD, etc.)"
),
},
)

product_model = api.inherit(
"ProductModel",
create_model,
{
"id": fields.String(
readOnly=True, description="The unique id assigned internally by service"
),
},
)

# query string arguments
product_args = reqparse.RequestParser()
product_args.add_argument(
"name", type=str, location="args", required=False, help="List Products by name"
)
product_args.add_argument(
"category", type=str, location="args", required=False, help="List Products by category"
)
product_args.add_argument(
"available",
type=inputs.boolean,
location="args",
required=False,
help="List Products by availability",
)
######################################################################
# R E S T A P I E N D P O I N T S
######################################################################


######################################################################
# LIST ALL PRODUCTS
# PATH: /products/{id}
######################################################################
@app.route("/products", methods=["GET"])
def list_products():
"""Returns all of the Products"""
app.logger.info("Request for product list")
category = request.args.get("category")
name = request.args.get("name")
available = request.args.get("available")
products = Product.all()

if category:
products_category = Product.find_by_category(category)
products = [product for product in products if product in products_category]
if name:
products_name = Product.find_by_name(name)
products = [product for product in products if product in products_name]
if available:
products_available = Product.find_by_availability(available)
products = [product for product in products if product in products_available]

results = [product.serialize() for product in products]
app.logger.info("Returning %d products", len(results))
return jsonify(results), status.HTTP_200_OK
@api.route("/products/<product_id>")
@api.param("product_id", "The Product identifier")
class ProductResource(Resource):
"""
ProductResource class
Allows the manipulation of a single Product
GET /product{id} - Returns a Product with the id
PUT /product{id} - Update a Product with the id
DELETE /product{id} - Deletes a Product with the id
"""

# ------------------------------------------------------------------
# RETRIEVE A PRODUCT
# ------------------------------------------------------------------
@api.doc("get_products")
@api.response(404, "Product not found")
@api.marshal_with(product_model)
def get(self, product_id):
"""
Retrieve a single Product
This endpoint will return a Product based on it's id
"""
app.logger.info("Request to Retrieve a product with id [%s]", product_id)
product = Product.find(product_id)
if not product:
abort(status.HTTP_404_NOT_FOUND, f"Product with id '{product_id}' was not found.")
return product.serialize(), status.HTTP_200_OK

######################################################################
# ADD A NEW PRODUCT
# PATH: /products
######################################################################
@app.route("/products", methods=["POST"])
def create_products():
"""
Creates a Product
This endpoint will create a Product based the data in the body that is posted
"""
app.logger.info("Request to create a product")
check_content_type("application/json")
product = Product()
product.deserialize(request.get_json())
product.create()
message = product.serialize()
location_url = url_for("read_products", product_id=product.id, _external=True)
app.logger.info("Product with ID [%s] created.", product.id)
return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url}

# try:
# product.deserialize(request.get_json())
# product.create()
# message = product.serialize()
# location_url = url_for("read_products", product_id=product.id, _external=True)
# app.logger.info("Product with ID [%s] created.", product.id)
# return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url}
# except sqlalchemy.exc.PendingRollbackError as rollback_error:
# # Rollback the session in case of error
# db.session.rollback()
# print("rollback")
# app.logger.error("Error creating product: %s", str(rollback_error))
# return jsonify({"error": "Error creating product"}), status.HTTP_400_BAD_REQUEST
@api.route("/products", strict_slashes=False)
class ProductCollection(Resource):
"""Handles all interactions with collections of Products"""

# ------------------------------------------------------------------
# LIST ALL PRODUCTS
# ------------------------------------------------------------------
@api.doc("list_products")
@api.expect(product_args)
@api.marshal_list_with(product_model)
def get(self):
"""Returns all of the Products"""
app.logger.info("Request to list Products...")
products = []
args = product_args.parse_args()
if args["category"]:
app.logger.info("Filtering by category: %s", args["category"])
products = Product.find_by_category(args["category"])
elif args["name"]:
app.logger.info("Filtering by name: %s", args["name"])
products = Product.find_by_name(args["name"])
elif args["available"] is not None:
app.logger.info("Filtering by availability: %s", args["available"])
products = Product.find_by_availability(args["available"])
else:
app.logger.info("Returning unfiltered list.")
products = Product.all()

results = [product.serialize() for product in products]
app.logger.info("[%s] Products returned", len(results))
return results, status.HTTP_200_OK

# ------------------------------------------------------------------
# ADD A NEW PRODUCT
# ------------------------------------------------------------------
@api.doc("create_products")
@api.response(400, "The posted data was not valid")
@api.expect(create_model)
@api.marshal_with(product_model, code=201)
def post(self):
"""
Creates a Product
This endpoint will create a Product based the data in the body that is posted
"""
app.logger.info("Request to Create a Product")
product = Product()
app.logger.debug("Payload = %s", api.payload)
product.deserialize(api.payload)
product.create()
app.logger.info("Product with new id [%s] created!", product.id)
location_url = api.url_for(ProductResource, product_id=product.id, _external=True)
return product.serialize(), status.HTTP_201_CREATED, {"Location": location_url}


# ######################################################################
# # LIST ALL PRODUCTS
# ######################################################################
# @app.route("/products", methods=["GET"])
# def list_products():
# """Returns all of the Products"""
# app.logger.info("Request for product list")
# category = request.args.get("category")
# name = request.args.get("name")
# available = request.args.get("available")
# products = Product.all()

# if category:
# products_category = Product.find_by_category(category)
# products = [product for product in products if product in products_category]
# if name:
# products_name = Product.find_by_name(name)
# products = [product for product in products if product in products_name]
# if available:
# products_available = Product.find_by_availability(available)
# products = [product for product in products if product in products_available]

# results = [product.serialize() for product in products]
# app.logger.info("Returning %d products", len(results))
# return jsonify(results), status.HTTP_200_OK


# ######################################################################
# # ADD A NEW PRODUCT
# ######################################################################
# @app.route("/products", methods=["POST"])
# def create_products():
# """
# Creates a Product
# This endpoint will create a Product based the data in the body that is posted
# """
# app.logger.info("Request to create a product")
# check_content_type("application/json")
# product = Product()
# product.deserialize(request.get_json())
# product.create()
# message = product.serialize()
# location_url = url_for("read_products", product_id=product.id, _external=True)
# app.logger.info("Product with ID [%s] created.", product.id)
# return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url}

# # try:
# # product.deserialize(request.get_json())
# # product.create()
# # message = product.serialize()
# # location_url = url_for("read_products", product_id=product.id, _external=True)
# # app.logger.info("Product with ID [%s] created.", product.id)
# # return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url}
# # except sqlalchemy.exc.PendingRollbackError as rollback_error:
# # # Rollback the session in case of error
# # db.session.rollback()
# # print("rollback")
# # app.logger.error("Error creating product: %s", str(rollback_error))
# # return jsonify({"error": "Error creating product"}), status.HTTP_400_BAD_REQUEST


######################################################################
Expand Down Expand Up @@ -197,24 +339,24 @@ def delete_products(product_id):
return "", status.HTTP_204_NO_CONTENT


######################################################################
# READ A PRODUCT
######################################################################
@app.route("/products/<int:product_id>", methods=["GET"])
def read_products(product_id):
"""
Read a Product
This endpoint will Read a Product for detail based the id specified in the path
"""
app.logger.info("Request to read product with id: %s", product_id)
product = Product.find(product_id)
if not product:
abort(
status.HTTP_404_NOT_FOUND, f"Product with id '{product_id}' was not found."
)

app.logger.info("Returning product with ID [%s].", product_id)
return jsonify(product.serialize()), status.HTTP_200_OK
# ######################################################################
# # READ A PRODUCT
# ######################################################################
# @app.route("/products/<int:product_id>", methods=["GET"])
# def read_products(product_id):
# """
# Read a Product
# This endpoint will Read a Product for detail based the id specified in the path
# """
# app.logger.info("Request to read product with id: %s", product_id)
# product = Product.find(product_id)
# if not product:
# abort(
# status.HTTP_404_NOT_FOUND, f"Product with id '{product_id}' was not found."
# )

# app.logger.info("Returning product with ID [%s].", product_id)
# return jsonify(product.serialize()), status.HTTP_200_OK


######################################################################
Expand Down
Loading

0 comments on commit 811a972

Please sign in to comment.