Skip to content

Commit

Permalink
Merge pull request #57 from CSCI-GA-2820-SP24-003/flask-restx-swagger
Browse files Browse the repository at this point in the history
Refactor using RESTX and Swagger
  • Loading branch information
EricYoung37 authored Apr 12, 2024
2 parents 161fa81 + 6c5951b commit 802f168
Show file tree
Hide file tree
Showing 15 changed files with 565 additions and 424 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/bdd.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: BDD Tests
name: BDD CI Build
on:
push:
branches:
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/tdd.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
name: TDD CI build
name: TDD CI Build

on:
push:
branches:
- master
# - any-future-branch
# you may want the checks for any future branches you push to GitHub
pull_request:
branches:
- master
Expand Down
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Python](https://img.shields.io/badge/Language-Python-blue.svg)](https://python.org/)
[![Build Status](https://github.com/CSCI-GA-2820-SP24-003/inventory/actions/workflows/tdd.yml/badge.svg)](https://github.com/CSCI-GA-2820-SP24-003/inventory/actions)
[![Build Status](https://github.com/CSCI-GA-2820-SP24-003/inventory/actions/workflows/bdd.yml/badge.svg)](https://github.com/CSCI-GA-2820-SP24-003/inventory/actions)
[![codecov](https://codecov.io/gh/CSCI-GA-2820-SP24-003/inventory/graph/badge.svg?token=IM6VUBAEYC)](https://codecov.io/gh/CSCI-GA-2820-SP24-003/inventory)

## Description
Expand All @@ -12,20 +13,23 @@ The inventory resource keeps track of how many of each product we have in our wa
## Database Schema
| Column | Data type | Condition |
| --- | --- | --- |
| `id` | `<integer>` | `id > 0` |
| `quantity` | `<integer>` | `quantity > 0` |
| `inventory_name` | `<string>` | `name = string` |
| `category` | `<string>` | `category = string` |
| `id` | `int` | `id > 0` |
| `quantity` | `int` | `quantity > 0` |
| `inventory_name` | `string` | N/A |
| `category` | `string` | N/A |
| `condition` | `Enum` | `condition in set(NEW, OPENED, USED)` |
| `restock_level` | `<integer>` | `restock_level > 0` |

## API endpoints

| Method | URI | Description | Content-Type |
| Method | URI | Description | Input |
| --- | --- | ------ | --- |
| `GET` | `/inventory/` | List all items in the inventory | N/A |
| `GET` | `/inventory/<int:id>` | Given the correct `id` this retrieves the inventory | N/A |
| `DELETE` | `/inventory/<int:id>` | Given the correct `id` this deletes the entry | N/A |
| `PUT` | `/inventory/<int:id>` | Given the correct `id` this updates the entry | N/A |
| `POST` | `/inventory` | Given the inventory parameters, create a new inventory entry | application/json |
| `GET` | `/inventory/` | List all items in the inventory | Item Attribute(s) |
| `GET` | `/inventory/<int:id>` | Given the correct `id` this retrieves the inventory | Item ID |
| `DELETE` | `/inventory/<int:id>` | Given the correct `id` this deletes the entry | Item ID |
| `PUT` | `/inventory/<int:id>` | Given the correct `id` this updates the entry | Item Attributes |
| `POST` | `/inventory` | Given the inventory parameters, create a new inventory entry | Item Attributes |
| `PUT` | `/inventory/<int:id>/restock` | Click on the restock button will increase the `quantity` of an item if it is below `restock_level` | Item Attributes |

## License

Expand Down
7 changes: 3 additions & 4 deletions features/inventory.feature
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ Feature: The inventory service back-end
So that I can keep track of all my inventory

Background:
Given the following inventories
Given the following items
| name | category | quantity | condition | restock_level |
| iphone | electronics | 20 | NEW | 100 |
| apple | fruit | 30 | NEW | 110 |
| ipad | electronics | 40 | USED | 120 |
| peach | fruit | 50 | OPEN | 130 |
| peach | fruit | 50 | OPENED | 130 |

Scenario: The server is running
When I visit the "Home Page"
Expand Down Expand Up @@ -102,8 +102,7 @@ Scenario: Restock
And the "Name" field should be empty
And the "Category" field should be empty
When I paste the "Id" field
And I set the "Restock_quantity" to "30"
And I press the "Restock" button
Then I should see the message "Success"
And I should see "ipod" in the "Name" field
And I should see "90" in the "Quantity" field
And I should see "240" in the "Quantity" field
11 changes: 6 additions & 5 deletions features/steps/inventory_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@
HTTP_201_CREATED = 201
HTTP_204_NO_CONTENT = 204

@given('the following inventories')

@given('the following items')
def step_impl(context):
""" Delete all Inventories and load new ones """
""" Delete all items and load new ones """

# List all of the inventories and delete them one by one
rest_endpoint = f"{context.base_url}/inventory"
# List all of the items and delete them one by one
rest_endpoint = f"{context.base_url}/api/inventory" # need the prefix api after refactor
context.resp = requests.get(rest_endpoint)
assert(context.resp.status_code == HTTP_200_OK)
for inventory in context.resp.json():
context.resp = requests.delete(f"{rest_endpoint}/{inventory['id']}")
assert(context.resp.status_code == HTTP_204_NO_CONTENT)

# load the database with new inventories
# load the database with new items
for row in context.table:
payload = {
"inventory_name": row['name'],
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
Flask = "^3.0.2"
flask-restx = "^1.3.0"
Flask-SQLAlchemy = "^3.1.1"
psycopg = {extras = ["binary"], version = "^3.1.17"}
retry = "^0.9.2"
Expand Down
22 changes: 22 additions & 0 deletions service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,52 @@
"""
import sys
from flask import Flask
from flask_restx import Api
from service import config
from service.common import log_handlers

# Will be initialize when app is created
api = None # pylint: disable=invalid-name

############################################################
# Initialize the Flask instance
############################################################


def create_app():
"""Initialize the core application."""
# Create Flask application
app = Flask(__name__)
app.config.from_object(config)

app.url_map.strict_slashes = False

# Initialize Plugins
# pylint: disable=import-outside-toplevel
from service.models import db

db.init_app(app)

# Configure Swagger before initializing it
global api
api = Api(
app,
version="1.0.0",
title="Inventory REST API Service",
description="This is an inventory server.",
default="Inventory",
default_label="inventory operations",
doc="/apidocs", # default also could use doc='/apidocs/'
prefix="/api",
)

with app.app_context():
# Dependencies require we import the routes AFTER the Flask app is created
# pylint: disable=wrong-import-position, wrong-import-order, unused-import
from service import routes, models # noqa: F401 E402
from service.common import error_handlers, cli_commands # noqa: F401, E402

# try creating all tables of db
try:
db.create_all()
except Exception as error: # pylint: disable=broad-except
Expand Down
130 changes: 68 additions & 62 deletions service/common/error_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,85 +16,91 @@
"""
Module: error_handlers
"""
from flask import jsonify
# from flask import jsonify
from service import api
from flask import current_app as app # Import Flask application
from service.models import DataValidationError
from service.models import DataValidationError, DatabaseConnectionError
from . import status


######################################################################
# Error Handlers
######################################################################
@app.errorhandler(DataValidationError)
@api.errorhandler(DataValidationError)
def request_validation_error(error):
"""Handles Value Errors from bad data"""
return bad_request(error)
message = str(error)
app.logger.error(message)
return {
"status_code": status.HTTP_400_BAD_REQUEST,
"error": "Bad Request",
"message": message,
}, status.HTTP_400_BAD_REQUEST


@app.errorhandler(status.HTTP_400_BAD_REQUEST)
def bad_request(error):
"""Handles bad requests with 400_BAD_REQUEST"""
@api.errorhandler(DatabaseConnectionError)
def database_connection_error(error):
"""Handles Database Errors from connection attempts"""
message = str(error)
app.logger.warning(message)
return (
jsonify(
status=status.HTTP_400_BAD_REQUEST, error="Bad Request", message=message
),
status.HTTP_400_BAD_REQUEST,
)
app.logger.critical(message)
return {
"status_code": status.HTTP_503_SERVICE_UNAVAILABLE,
"error": "Service Unavailable",
"message": message,
}, status.HTTP_503_SERVICE_UNAVAILABLE


@app.errorhandler(status.HTTP_404_NOT_FOUND)
def not_found(error):
"""Handles resources not found with 404_NOT_FOUND"""
message = str(error)
app.logger.warning(message)
return (
jsonify(status=status.HTTP_404_NOT_FOUND, error="Not Found", message=message),
status.HTTP_404_NOT_FOUND,
)
# @app.errorhandler(status.HTTP_404_NOT_FOUND)
# def not_found(error):
# """Handles resources not found with 404_NOT_FOUND"""
# message = str(error)
# app.logger.warning(message)
# return (
# jsonify(status=status.HTTP_404_NOT_FOUND, error="Not Found", message=message),
# status.HTTP_404_NOT_FOUND,
# )


@app.errorhandler(status.HTTP_405_METHOD_NOT_ALLOWED)
def method_not_supported(error):
"""Handles unsupported HTTP methods with 405_METHOD_NOT_SUPPORTED"""
message = str(error)
app.logger.warning(message)
return (
jsonify(
status=status.HTTP_405_METHOD_NOT_ALLOWED,
error="Method not Allowed",
message=message,
),
status.HTTP_405_METHOD_NOT_ALLOWED,
)
# @app.errorhandler(status.HTTP_405_METHOD_NOT_ALLOWED)
# def method_not_supported(error):
# """Handles unsupported HTTP methods with 405_METHOD_NOT_SUPPORTED"""
# message = str(error)
# app.logger.warning(message)
# return (
# jsonify(
# status=status.HTTP_405_METHOD_NOT_ALLOWED,
# error="Method not Allowed",
# message=message,
# ),
# status.HTTP_405_METHOD_NOT_ALLOWED,
# )


@app.errorhandler(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
def mediatype_not_supported(error):
"""Handles unsupported media requests with 415_UNSUPPORTED_MEDIA_TYPE"""
message = str(error)
app.logger.warning(message)
return (
jsonify(
status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
error="Unsupported media type",
message=message,
),
status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
)
# @app.errorhandler(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
# def mediatype_not_supported(error):
# """Handles unsupported media requests with 415_UNSUPPORTED_MEDIA_TYPE"""
# message = str(error)
# app.logger.warning(message)
# return (
# jsonify(
# status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
# error="Unsupported media type",
# message=message,
# ),
# status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
# )


@app.errorhandler(status.HTTP_500_INTERNAL_SERVER_ERROR)
def internal_server_error(error):
"""Handles unexpected server error with 500_SERVER_ERROR"""
message = str(error)
app.logger.error(message)
return (
jsonify(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
error="Internal Server Error",
message=message,
),
status.HTTP_500_INTERNAL_SERVER_ERROR,
)
# @app.errorhandler(status.HTTP_500_INTERNAL_SERVER_ERROR)
# def internal_server_error(error):
# """Handles unexpected server error with 500_SERVER_ERROR"""
# message = str(error)
# app.logger.error(message)
# return (
# jsonify(
# status=status.HTTP_500_INTERNAL_SERVER_ERROR,
# error="Internal Server Error",
# message=message,
# ),
# status.HTTP_500_INTERNAL_SERVER_ERROR,
# )
Loading

0 comments on commit 802f168

Please sign in to comment.