From 3902b3ea656631dfe0440226c9c18ae7764fa068 Mon Sep 17 00:00:00 2001 From: Todd Birchard Date: Sun, 3 Sep 2023 10:37:26 -0400 Subject: [PATCH 1/3] Remove dependency on external API. --- .env.example | 4 +- LICENSE | 2 +- README.md | 2 +- config.py | 12 +-- data/products.json | 92 +++++++++++++++++++ flask_blueprint_tutorial/__init__.py | 8 +- flask_blueprint_tutorial/api.py | 33 ++++--- flask_blueprint_tutorial/assets.py | 6 +- flask_blueprint_tutorial/home/home.py | 16 ++-- .../home/templates/index.jinja2 | 4 +- flask_blueprint_tutorial/products/products.py | 5 +- flask_blueprint_tutorial/profile/profile.py | 6 +- .../templates/blueprintinfo.jinja2 | 2 +- .../templates/navigation.jinja2 | 14 +-- main.py | 11 ++- poetry.lock | 59 +++++++++++- pyproject.toml | 6 +- 17 files changed, 214 insertions(+), 68 deletions(-) create mode 100644 data/products.json diff --git a/.env.example b/.env.example index d1e83ee..3b3a02d 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -FLASK_APP=wsgi.py -FLASK_DEBUG=False +FLASK_APP=main.py +FLASK_ENV=production SECRET_KEY=randomstringofcharacters LESS_BIN=/usr/local/bin/lessc ASSETS_DEBUG=False diff --git a/LICENSE b/LICENSE index b6ae3eb..425c816 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Hackers and Slackers +Copyright (c) 2023 Hackers and Slackers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d412399..8eebc7e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Get set up locally in two steps: Replace the values in **.env.example** with your values and rename this file to **.env**: -* `FLASK_APP`: Entry point of your application; should be `wsgi.py`. +* `FLASK_APP`: Entry point of your application; should be `main.py`. * `FLASK_ENV`: The environment in which to run your application; either `development` or `production`. * `SECRET_KEY`: Randomly generated string of characters used to encrypt your app's data. * `LESS_BIN` *(optional for static assets)*: Path to your local LESS installation via `which lessc`. diff --git a/config.py b/config.py index bb0280c..4739e94 100644 --- a/config.py +++ b/config.py @@ -3,16 +3,16 @@ from dotenv import load_dotenv -basedir = path.abspath(path.dirname(__file__)) -load_dotenv(path.join(basedir, ".env")) +BASE_DIR = path.abspath(path.dirname(__file__)) +load_dotenv(path.join(BASE_DIR, ".env")) class Config: """Configuration from environment variables.""" SECRET_KEY = environ.get("SECRET_KEY") - FLASK_ENV = environ.get("FLASK_DEBUG") - FLASK_APP = "wsgi.py" + FLASK_ENV = environ.get("FLASK_ENV") + FLASK_APP = "main.py" # Flask-Assets LESS_BIN = environ.get("LESS_BIN") @@ -27,5 +27,5 @@ class Config: # Datadog DD_SERVICE = environ.get("DD_SERVICE") - # API - BEST_BUY_API_KEY = environ.get("BEST_BUY_API_KEY") + # Hardcoded data + PRODUCT_DATA_FILEPATH = f"{BASE_DIR}/data/products.json" diff --git a/data/products.json b/data/products.json new file mode 100644 index 0000000..f14f40f --- /dev/null +++ b/data/products.json @@ -0,0 +1,92 @@ +[ + { + "customerReviewAverage": 5.00, + "customerReviewCount": 293, + "name": "Red Robin - $25 Gift Card", + "sku": 4259000, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/4259/4259000_sd.jpg", + "manufacturer": "Red Robin", + "longDescription": "Come in and enjoy an outrageously delicious burger with Bottomless Steak Fries. Pair it with a cold beer or signature Freckled Lemonade - it's a duo that's sure to make you smile.", + "salePrice": 25.00 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 53, + "name": "Bowers & Wilkins - 700 Series 3-way Floorstanding Speaker w/5\" midrange, dual 5\" bass (each) - White", + "sku": 6023602, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6023/6023602_sd.jpg", + "manufacturer": "Bowers & Wilkins", + "longDescription": "Generate commanding sound with this Bowers & Wilkins loudspeaker. Its Carbon Dome tweeter plays accurate, clear highs, and the two Aerofoil bass drivers deliver stiffness and rigidity while producing dynamic bass. This vented Bowers & Wilkins loudspeaker has a 5-inch midrange driver to round out its full sound, and its slim construction makes it suitable for small or large spaces.", + "salePrice": 1487.99 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 53, + "name": "Bowers & Wilkins - 700 Series 3-way Floorstanding Speaker w/5\" midrange, dual 5\" bass (each) - Gloss Black", + "sku": 6027601, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6027/6027601_sd.jpg", + "manufacturer": "Bowers & Wilkins", + "longDescription": "Enjoy accurate, realistic sound with this black Bowers & Wilkins floor speaker. Its two bass drivers deliver thumping low frequencies, and the Carbon Dome tweeter and midrange driver offer commanding, studio-quality audio. This Bowers & Wilkins floor speaker integrates into your living space and entertainment system for seamless sound production with a frequency range of 48Hz - 28kHz.", + "salePrice": 1487.99 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 55, + "name": "Canon - RF50mm F1.2 L USM Standard Prime Lens for EOS R-Series Cameras - Black", + "sku": 6298180, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6298/6298180_sd.jpg", + "manufacturer": "Canon", + "longDescription": "Capture high-quality images and sharp details with this 50mm Canon lens. The 1.31-foot minimum focusing distance and 0.19x magnification let you photograph from a range of distances, and its UD lens reduces distortion. This Canon lens has an added coating to minimize lens flare and ghosting in various types of light.", + "salePrice": 2199.99 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 83, + "name": "Nikkor Z 24-70mm f/2.8 S Optical Zoom Lens for Nikon Z - Black", + "sku": 6334316, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6334/6334316_sd.jpg", + "manufacturer": "Nikon", + "longDescription": "Capture high-quality photographs whether shooting at close, medium or long range with this Nikon NIKKOR Z 24-70mm lens. The dust-resistant and drip-resistant design helps keep this lens in good condition, and the auto-focusing feature is quick and quiet. This Nikon NIKKOR Z 24-70mm lens features a Z system that produces images with enhanced sharpness and illumination.", + "salePrice": 2099.99 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 64, + "name": "Apple Watch Ultra (GPS + Cellular) 49mm Titanium Case with White Ocean Band - Titanium (Verizon)", + "sku": 6340050, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6340/6340050_sd.jpg", + "manufacturer": "Apple", + "longDescription": "The most rugged and capable Apple Watch ever, designed for exploration, adventure, and endurance. With a 49mm aerospace-grade titanium case, extra-long battery life,¹ specialized apps that work with the advanced sensors, and a new customizable Action button. See Dimension section below for band sizing information.", + "salePrice": 799.99 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 64, + "name": "Apple Watch Ultra (GPS + Cellular) 49mm Titanium Case with Yellow Ocean Band - Titanium (Verizon)", + "sku": 6340051, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6340/6340051_sd.jpg", + "manufacturer": "Apple", + "longDescription": "The most rugged and capable Apple Watch ever, designed for exploration, adventure, and endurance. With a 49mm aerospace-grade titanium case, extra-long battery life,¹ specialized apps that work with the advanced sensors, and a new customizable Action button. See Dimension section below for band sizing information.", + "salePrice": 799.99 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 64, + "name": "Apple Watch Ultra (GPS + Cellular) 49mm Titanium Case with Midnight Ocean Band - Titanium (Verizon)", + "sku": 6340057, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6340/6340057_sd.jpg", + "manufacturer": "Apple", + "longDescription": "The most rugged and capable Apple Watch ever, designed for exploration, adventure, and endurance. With a 49mm aerospace-grade titanium case, extra-long battery life,¹ specialized apps that work with the advanced sensors, and a new customizable Action button. See Dimension section below for band sizing information.", + "salePrice": 799.99 + }, + { + "customerReviewAverage": 5.00, + "customerReviewCount": 59, + "name": "NETGEAR - 8-Port 10/100/1000 Gigabit Ethernet PoE/PoE+ Unmanaged Switch", + "sku": 6356333, + "image": "https://pisces.bbystatic.com/prescaled/500/500/image2/BestBuy_US/images/products/6356/6356333_sd.jpg", + "manufacturer": "NETGEAR", + "longDescription": "Introducing the NETGEAR GS108LP 8-port Gigabit Ethernet unmanaged switch with 60W PoE budget. The flexible PoE+ integrated technology allows you to increase or decrease the PoE budget at any time to provide to your devices the power they need with interchangeable external power supply and an intuitive power selector. The compact and fanless design makes this switch an ideal solution to connect or power any device in any business environment.", + "salePrice": 98.99 + } +] \ No newline at end of file diff --git a/flask_blueprint_tutorial/__init__.py b/flask_blueprint_tutorial/__init__.py index 16ffb5a..60a384a 100644 --- a/flask_blueprint_tutorial/__init__.py +++ b/flask_blueprint_tutorial/__init__.py @@ -2,10 +2,8 @@ from flask import Flask from flask_assets import Environment -from config import Config - -def init_app(): +def flask_app(): """Create Flask application.""" app = Flask(__name__, instance_relative_config=False) app.config.from_object("config.Config") @@ -20,8 +18,8 @@ def init_app(): from .profile import profile # Register Blueprints - app.register_blueprint(profile.profile_bp) - app.register_blueprint(home.home_bp) + app.register_blueprint(profile.profile_blueprint) + app.register_blueprint(home.home_blueprint) app.register_blueprint(products.product_bp) # Compile static assets diff --git a/flask_blueprint_tutorial/api.py b/flask_blueprint_tutorial/api.py index f229fb6..4f803c1 100644 --- a/flask_blueprint_tutorial/api.py +++ b/flask_blueprint_tutorial/api.py @@ -1,19 +1,18 @@ -"""Source app with worthless data.""" -import requests +"""Read placeholder data for demo purposes.""" +import json +from flask import Flask -def fetch_products(app): - """Grab product listings from BestBuy.""" - endpoint = "https://api.bestbuy.com/v1/products(customerReviewAverage>=4&customerReviewCount>100&longDescription=*)" - params = { - "show": "customerReviewAverage,customerReviewCount,name,sku,image,description,manufacturer,longDescription,salePrice,sku", - "apiKey": app.config["BEST_BUY_API_KEY"], - "format": "json", - "pageSize": 6, - "totalPages": 1, - "sort": "customerReviewAverage.dsc", - } - headers = {"Accept": "application/json", "Content-Type": "application/json"} - req = requests.get(endpoint, params=params, headers=headers) - products = req.json()["products"] - return products + +def fetch_products(app: Flask) -> dict: + """ + Grab hardcoded product listings. + + :param Flask app: Flask application object. + + :returns: dict + """ + product_data_filepath = app.config["PRODUCT_DATA_FILEPATH"] + with open(product_data_filepath, encoding="utf-8") as file: + products_data = json.load(file) + return products_data diff --git a/flask_blueprint_tutorial/assets.py b/flask_blueprint_tutorial/assets.py index 10e19f5..9f4e3f5 100644 --- a/flask_blueprint_tutorial/assets.py +++ b/flask_blueprint_tutorial/assets.py @@ -14,19 +14,19 @@ def compile_static_assets(assets): extra={"rel": "stylesheet/less"}, ) home_style_bundle = Bundle( - "home_bp/less/home.less", + "home_blueprint/less/home.less", filters="less,cssmin", output="dist/css/home.css", extra={"rel": "stylesheet/less"}, ) profile_style_bundle = Bundle( - "profile_bp/less/profile.less", + "profile_blueprint/less/profile.less", filters="less,cssmin", output="dist/css/profile.css", extra={"rel": "stylesheet/less"}, ) product_style_bundle = Bundle( - "products_bp/less/products.less", + "products_blueprint/less/products.less", filters="less,cssmin", output="dist/css/products.css", extra={"rel": "stylesheet/less"}, diff --git a/flask_blueprint_tutorial/home/home.py b/flask_blueprint_tutorial/home/home.py index 67b3114..aa2ee8b 100644 --- a/flask_blueprint_tutorial/home/home.py +++ b/flask_blueprint_tutorial/home/home.py @@ -6,14 +6,12 @@ from flask_blueprint_tutorial.api import fetch_products # Blueprint Configuration -home_bp = Blueprint( - "home_bp", __name__, template_folder="templates", static_folder="static" -) +home_blueprint = Blueprint("home_blueprint", __name__, template_folder="templates", static_folder="static") -@home_bp.route("/", methods=["GET"]) +@home_blueprint.route("/", methods=["GET"]) def home(): - """Homepage.""" + """Render application Homepage.""" products = fetch_products(app) return render_template( "index.jinja2", @@ -24,9 +22,9 @@ def home(): ) -@home_bp.route("/about", methods=["GET"]) +@home_blueprint.route("/about", methods=["GET"]) def about(): - """About page.""" + """Render static `about` page.""" return render_template( "index.jinja2", title="About", @@ -35,9 +33,9 @@ def about(): ) -@home_bp.route("/contact", methods=["GET"]) +@home_blueprint.route("/contact", methods=["GET"]) def contact(): - """Contact page.""" + """Render page.""" return render_template( "index.jinja2", title="Contact", diff --git a/flask_blueprint_tutorial/home/templates/index.jinja2 b/flask_blueprint_tutorial/home/templates/index.jinja2 index fd51f46..4989cb2 100644 --- a/flask_blueprint_tutorial/home/templates/index.jinja2 +++ b/flask_blueprint_tutorial/home/templates/index.jinja2 @@ -13,8 +13,8 @@

{{ title }}

{{ subtitle }}

diff --git a/flask_blueprint_tutorial/products/products.py b/flask_blueprint_tutorial/products/products.py index 755e426..8f84181 100644 --- a/flask_blueprint_tutorial/products/products.py +++ b/flask_blueprint_tutorial/products/products.py @@ -6,14 +6,13 @@ from flask_blueprint_tutorial.api import fetch_products # Blueprint Configuration -product_bp = Blueprint( - "products_bp", __name__, template_folder="templates", static_folder="static" -) +product_bp = Blueprint("products_blueprint", __name__, template_folder="templates", static_folder="static") @product_bp.route("/products//", methods=["GET"]) def product_page(product_id): """Product description page.""" + products_json = app.config["FLASK_ENV"] product = fetch_products(app)[product_id] return render_template( "products.jinja2", diff --git a/flask_blueprint_tutorial/profile/profile.py b/flask_blueprint_tutorial/profile/profile.py index 0d24c4c..6149e9b 100644 --- a/flask_blueprint_tutorial/profile/profile.py +++ b/flask_blueprint_tutorial/profile/profile.py @@ -5,12 +5,10 @@ fake = Faker() # Blueprint Configuration -profile_bp = Blueprint( - "profile_bp", __name__, template_folder="templates", static_folder="static" -) +profile_blueprint = Blueprint("profile_blueprint", __name__, template_folder="templates", static_folder="static") -@profile_bp.route("/profile", methods=["GET"]) +@profile_blueprint.route("/profile", methods=["GET"]) def user_profile(): """Logged-in user profile page.""" user = fake.simple_profile() diff --git a/flask_blueprint_tutorial/templates/blueprintinfo.jinja2 b/flask_blueprint_tutorial/templates/blueprintinfo.jinja2 index 347ae41..731ad3a 100644 --- a/flask_blueprint_tutorial/templates/blueprintinfo.jinja2 +++ b/flask_blueprint_tutorial/templates/blueprintinfo.jinja2 @@ -6,4 +6,4 @@
  • View: {{ request.endpoint }}
  • Route: {{ request.path }}
  • -
    \ No newline at end of file + \ No newline at end of file diff --git a/flask_blueprint_tutorial/templates/navigation.jinja2 b/flask_blueprint_tutorial/templates/navigation.jinja2 index 3a1ab7e..2f5878e 100644 --- a/flask_blueprint_tutorial/templates/navigation.jinja2 +++ b/flask_blueprint_tutorial/templates/navigation.jinja2 @@ -2,19 +2,19 @@