Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ __pycache__

# Pytest folders
.pytest_cache
logs
reports

# Node.js
Expand Down
78 changes: 70 additions & 8 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,92 @@
import os
import logging

from flask import Flask
from logging.handlers import RotatingFileHandler
from typing import Mapping


def create_app():
def create_app(config: Mapping):
"""
Creates and configures the Flask application instance. Intended for use by both production and test code.
Creates and configures the Flask application instance using the provided config mapping.

:param config: The config mapping containing configuration key-value pairs
"""
app = Flask(__name__)

app.config.from_mapping(config)
configure_logging(app)
app.logger.info("Creating Flask application instance")

# TODO: Secure this before deploying into production
app.secret_key = "cUrR33_ChI_#"

# Configure app
app.config.from_mapping(
{
"FLASK_ENV": "development",
}
)
app.logger.debug(f"Using application configuration {config}")

# Register routes via Flask blueprint
from routes import app_blueprint

app.logger.info("Registering server endpoints")
app.register_blueprint(app_blueprint)

return app


app = create_app()
def configure_logging(app: Flask):
"""
Configure logging for the Flask application. Attempts to get the log level from a LOG_LEVEL environment variable,
defaults to INFO if environment variable not set.

:param app: The Flask app instance to configure logging on
"""

# Get log level from env-var, default to INFO if env-var invalid or not set
log_level = logging.getLevelName(os.getenv("LOG_LEVEL"))

if log_level is None or isinstance(log_level, str):
log_level = logging.INFO

app.logger.setLevel(log_level)

# Clear default handler
#
# TODO: Configure logging before creating the Flask app instance to prevent default handler from being added.
# TODO: See https://flask.palletsprojects.com/en/stable/logging/
app.logger.handlers.clear()

# Create and configure formatter
detailed_formatter = logging.Formatter(
"[%(asctime)s.%(msecs)03d] %(levelname)s (%(module)s): %(message)s",
"%Y-%m-%d, %H:%M:%S"
)

# Create and configure console handler for development logging
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(detailed_formatter)
app.logger.addHandler(console_handler)

# Add file handler with rotation for longer-running production deployments, omit during testing
if not bool(app.config["TESTING"]):
# Create logs directory if it doesn't exist
if not os.path.exists("logs"):
os.mkdir("logs")

# Max 10MB per file, keep 10 backup files
file_handler = RotatingFileHandler(
"logs/netex.log", maxBytes=10 * 1024 * 1024, backupCount=10 # 10MB
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(detailed_formatter)
app.logger.addHandler(file_handler)


if __name__ == "__main__":
# TODO: Support environment- and file-based configuration
app = create_app(
{
"FLASK_ENV": "development",
}
)
app.run(debug=True)
18 changes: 16 additions & 2 deletions routes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import humanize
import xarray

from flask import Blueprint, redirect, render_template, request, session, url_for
from flask import (
Blueprint,
current_app,
redirect,
render_template,
request,
session,
url_for,
)
from http import HTTPMethod
from io import BytesIO



app_blueprint = Blueprint("netex", __name__)

FILE_SESSION_KEY = "netcdf_file"
Expand All @@ -17,6 +24,9 @@
def index():
if request.method == HTTPMethod.POST:
netcdf_file = request.files["netcdf_file"]
current_app.logger.debug(
f"Received NetCDF summary request for file '{netcdf_file.filename}'"
)
if netcdf_file:
# Get file size
netcdf_file.seek(0, 2)
Expand All @@ -34,13 +44,17 @@ def index():
url_for("netex.summary", netcdf_filename=netcdf_file.filename)
)

current_app.logger.debug("Received GET request for index page")
return render_template("index.html.jinja")


@app_blueprint.get("/summary/<netcdf_filename>")
def summary(netcdf_filename):
summary = session["summary_text"]
file_size = session["file_size"]

current_app.logger.debug(f"Showing summary for file '{netcdf_filename}'")

return render_template(
"summary.html.jinja",
file_name=netcdf_filename,
Expand Down
7 changes: 4 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
import pytest

from app import create_app
from tests import constants

# TODO: Explain why this is needed for tests
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))


@pytest.fixture(scope="class")
def test_client():
flask_app = create_app()

flask_app.config["TESTING"] = True
flask_app = create_app(
{"FLASK_ENV": "development", "TESTING": constants.SET_TESTING_CONFIG}
)

# Create a test client using the Flask application configured for testing
with flask_app.test_client() as test_client:
Expand Down
2 changes: 2 additions & 0 deletions tests/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Adds the "TESTING" config to the Flask app if True
SET_TESTING_CONFIG = True
9 changes: 5 additions & 4 deletions tests/unit/app_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from app import create_app
from http import HTTPMethod
from tests import constants


class TestApp:
Expand All @@ -12,11 +13,11 @@ def test_app_contains_index_route(self):
WHEN create_app() is called
THEN a Flask app with an index route ("/") supporting GET and POST methods should be returned.
"""
app = create_app()
app = create_app({"TESTING": constants.SET_TESTING_CONFIG})

# Assert configs
assert app is not None
assert app.config["FLASK_ENV"] == "development"
assert app.config["TESTING"] == constants.SET_TESTING_CONFIG
assert app.secret_key is not None

url_map = app.url_map
Expand All @@ -26,7 +27,7 @@ def test_app_contains_index_route(self):
index_rule = routes[0]

assert index_rule.rule == "/"

assert HTTPMethod.GET in index_rule.methods
assert HTTPMethod.POST in index_rule.methods

Expand All @@ -35,7 +36,7 @@ def test_app_contains_summary_route(self):
WHEN create_app() is called
THEN a Flask app with a "/summary/{netcdf_filename} route supporting only the GET method should be returned.
"""
app = create_app()
app = create_app({"TESTING": True})

url_map = app.url_map
# Assert summary route is registered and supports GET method
Expand Down