There is a Vue.js based front-end for displaying reactive web pages to the user and a Python Flask backend for serving the online store REST API.
The frontend can be started with the following command:
$ npm run frontend:start # starts Vue.js app at http://localhost:8080
Note that the frontend can be found under online_store/frontend
(WIP).
The backend can be started with the following command:
$ export FLASK_DEBUG=1 # for development with live reload
$ export FLASK_ENVIRONMENT=development # this is the default
$ PYTHONPATH='.' python3 manage.py run
This repository was created to solve the Prezola Technical Challenge, which requires a solution capable of adding, removing and listing (added) gifts from a list. It also required a mechanism to purchase a gift from the list and generate a report (of purchased vs non-purchased) gifts.
This code project takes that concept and extends it to a generic (gift) store.
"Write a program to the best of your knowledge which will allow the user to manage a single list of wedding gifts."
The user must be able to:
- Add a gift to the list
- Remove a gift from the list
- List the already added gifts of the list
- Purchase a gift from the list
- Generate a report from the list which will print out gifts & their statuses.
- The report must include two sections:
- Purchased gifts: each purchased gift with their details.
- Not purchased gifts: each available gift with their details.
- The report must include two sections:
There are two concrete implementations for realising a gift list with
the following classes from online_store/backend/gift_list.py
:
BasicGiftList
, a pure python implementation of a gift list (Well Tested).SqlDatabaseGiftList
, an SQL ORM based implementation of a gift list for use within a flask (or Django) REST API app. In this example, the ORM models are found inonline_store/backend/models/
and the REST API is implemented inonline_store/backend/routes/gifts.py
.
$ npm install # needed for Swagger JSON to OAS YAML spec conversion
$ npm run frontend:install # install packages needed for frontend app
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install setuptools
(venv) $ pip install -r requirements-dev.txt -r requirements-test.txt
(venv) $ pip install -r requirements.txt
setup.cfg
has configured pytest
to collect coverage information
and can be run as follows:
(venv) $ PYTHONPATH='.' py.test
The following showcases a simple python implementation of the gift list:
# load store data
>>> import json
>>> store_items = json.load(open('products.json'))
# import implementation
>>> from online_store.backend.gift_list import BasicGiftList
# create a new gift list
>>> gift_list = BasicGiftList('Liam')
>>> gift_list # show list representation in interpreter
Liam -> []
# add gift item
>>> gift_list.add_item(store_items[0])
>>> gift_list
Liam -> [{'id': 1, 'name': 'Tea pot', 'brand': 'Le Creuset', 'price': '47.00GBP', 'in_stock_quantity': 50}]
# remove gift item
>>> gift_list.add_item(store_items[1], quantity=3)
>>> gift_list.remove_item(store_items[0])
>>> gift_list
Liam -> [{'id': 2, 'name': 'Cast Iron Oval Casserole - 25cm; Volcanic', 'brand': 'Le Creuset', 'price': '210.00GBP', 'in_stock_quantity': 27}]
# purchase gift item
>>> gift_list.purchase_item(store_items[1], quantity=1)
# generate report
>>> gift_list.create_report()
Gift List Report for Liam:
==============================
Purchased items:
- {'id': 2, 'name': 'Cast Iron Oval Casserole - 25cm; Volcanic', 'brand': 'Le Creuset', 'price': '210.00GBP', 'in_stock_quantity': 27} (quantity: 1)
------------------------------
Available items:
- {'id': 2, 'name': 'Cast Iron Oval Casserole - 25cm; Volcanic', 'brand': 'Le Creuset', 'price': '210.00GBP', 'in_stock_quantity': 27} (quantity: 2)
Alternatively there is a REST API, which can be run with:
$ source venv/bin/activate
(venv) $ cd online_store
(venv) $ FLASK_DEBUG=1 flask run
This will start a flask development server running on http://localhost:5000 and provides a Swagger-UI at http://localhost:5000/apidocs/.
# register a new user
$ curl -X POST -H "Content-Type: application/json" -d '{"username": "me", "password": "test", "email": "me@test.com"}' 'http://localhost:5000/api/v1/auth/register'
{"code":200,"msg":"success","status":"ok"}
# login with user
$ curl -X POST -H "Content-Type: application/json" -d '{"username": "me", "password": "test"}'
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MDIwMTIxMzQsIm5iZiI6MTYwMjAxMjEzNCwianRpIjoiOTA5NTRlZDAtZWIzMy00MTY2LThhODEtZDE5NDI3MjI5NTE0IiwiZXhwIjoxNjAyMDEzMDM0LCJpZGVudGl0eSI6Im1lIiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.GYVTQK4Xw9JaiJJxa75vlKBS-mho0QjfcM94usPZtSI","code":200,"status":"ok"}
# note access token
$ export TOKEN='<JWT_FROM_LOGIN>'
# access gift list
$ curl -X GET -H "accept: application/json" -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/v1/gifts/list
[]
# add one gift item with store id of 1 to list
$ curl -X POST -H "accept: application/json" -H "Authorization: Bearer $TOKEN" 'http://localhost:5000/api/v1/gifts/list/add?item_id=1&quantity=1'
{
"code": 200,
"msg": "Item added",
"status": "ok"
}
# remove item with id 1 from gift list
$ curl -X DELETE -H "accept: application/json" -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/v1/gifts/list/1
{
"code": 200,
"msg": "Item removed",
"status": "ok"
}
# add then purchase item
$ curl -X POST -H "accept: application/json" -H "Authorization: Bearer $TOKEN" 'http://localhost:5000/api/v1/gifts/list/add?item_id=2&quantity=2'
{
"code": 200,
"msg": "Item added",
"status": "ok"
}
$ curl -X POST -H "accept: application/json" -H "Authorization: Bearer $TOKEN" http://localhost:5000/api/v1/gifts/list/2/purchase?quantity=1
{
"code": 200,
"msg": "gift purchased",
"status": "ok"
}
# produce report
$ curl -X GET -H "accept: application/json" -H "Authorization: Bearer $TOKEN" 'http://localhost:5000/api/v1/gifts/list/report'
{
"available": [
{
"brand": "Le Creuset",
"currency": "GBP",
"id": 2,
"in_stock_quantity": 27,
"name": "Cast Iron Oval Casserole - 25cm; Volcanic",
"price": 210.0,
"quantity": 1
}
],
"purchased": [
{
"brand": "Le Creuset",
"currency": "GBP",
"id": 2,
"in_stock_quantity": 27,
"name": "Cast Iron Oval Casserole - 25cm; Volcanic",
"price": 210.0,
"quantity": 1
}
],
"user": 2
}
There are currently a number of extra features, which help
- User-friendly backend application logging using
loguru
python package. - Simple containerisation using Docker - see
DockerFile
- Authentication using JSON web tokens via flask-jwt-extended middleware.
- OpenAPI Specification (OAS) conformant client documentation generated using flasgger and viewable via SwaggerUI /apidocs endpoint when running the flask server.
- Persistent data storage using SQL Database modelled using sqlalchemy ORM.
The Sphinx documentation builder is currently used to extract python docstrings and the OpenAPI spec of the REST API.
To build the documentation:
$ cd docs/
$ make openapi_spec.yml
$ make html # or another end documentation format e.g. epub
A live deployment to GitHub Pages can be found at https://liam-deacon.github.io/online-store-rest-api/
- Build script / CI using GitHub Actions
- Sphinx documentation support
- Deploy API documentation to GitHub Pages via Actions
- Test SqlDatabaseGiftList
- Automated tests for REST API
- shields.io support for README badges
- Add linting to CI
- Build and deploy docker image(s) to dockerhub via CI/CD
- Deploy flask app to Heroku for demo purposes (follow link)
Given more time, the following improvements could be made:
- Write frontend in React (or maybe Vue.js)
- Create
docker-compose.yml
multi-container Docker compose script for orchestrating frontend, backend and database (e.g. React/NPM-based, Flask, Postgres). - Implement missing features in the code (i.e. wherever
NotImplementError
is raised) - Code tidy and refactor
- Increase overall code coverage (aiming for nirvana at 100%)