diff --git a/alerts_backend/python/app.py b/alerts_backend/python/app.py
index 92269b5..24cc843 100644
--- a/alerts_backend/python/app.py
+++ b/alerts_backend/python/app.py
@@ -1,16 +1,32 @@
"""Query alert information from AeroAPI and present it to a frontend service"""
import os
from datetime import datetime
-from typing import Dict, Any, Tuple
+from typing import Dict, Any, Tuple, Set
+import copy
import json
import requests
from flask import Flask, jsonify, Response, request
+from flask.logging import create_logger
from flask_cors import CORS
-from sqlalchemy import (exc, create_engine, MetaData, Table,
- Column, Integer, Boolean, Text, insert, Date, DateTime)
from sqlalchemy.sql import func
+from sqlalchemy import (
+ exc,
+ create_engine,
+ MetaData,
+ Table,
+ Column,
+ Integer,
+ Boolean,
+ Text,
+ insert,
+ Date,
+ select,
+ DateTime,
+ delete,
+ update,
+)
AEROAPI_BASE_URL = "https://aeroapi.flightaware.com/aeroapi"
AEROAPI_KEY = os.environ["AEROAPI_KEY"]
@@ -19,6 +35,7 @@
# pylint: disable=invalid-name
app = Flask(__name__)
+logger = create_logger(app)
CORS(app)
# create the SQL engine using SQLite
@@ -34,41 +51,42 @@
metadata_obj = MetaData()
# Table for alert configurations
aeroapi_alert_configurations = Table(
- "aeroapi_alert_configurations",
- metadata_obj,
- Column("fa_alert_id", Integer, primary_key=True),
- Column("ident", Text),
- Column("origin", Text),
- Column("destination", Text),
- Column("aircraft_type", Text),
- Column("start_date", Date),
- Column("end_date", Date),
- Column("max_weekly", Integer),
- Column("eta", Integer),
- Column("arrival", Boolean),
- Column("cancelled", Boolean),
- Column("departure", Boolean),
- Column("diverted", Boolean),
- Column("filed", Boolean),
- )
+ "aeroapi_alert_configurations",
+ metadata_obj,
+ Column("fa_alert_id", Integer, primary_key=True),
+ Column("ident", Text),
+ Column("origin", Text),
+ Column("destination", Text),
+ Column("aircraft_type", Text),
+ Column("start_date", Date),
+ Column("end_date", Date),
+ Column("max_weekly", Integer),
+ Column("eta", Integer),
+ Column("arrival", Boolean),
+ Column("cancelled", Boolean),
+ Column("departure", Boolean),
+ Column("diverted", Boolean),
+ Column("filed", Boolean),
+)
# Table for POSTed alerts
aeroapi_alerts = Table(
- "aeroapi_alerts",
- metadata_obj,
- Column("id", Integer, primary_key=True, autoincrement=True),
- Column("time_alert_received", DateTime(timezone=True), server_default=func.now()), # Store time in UTC that the alert was received
- Column("long_description", Text),
- Column("short_description", Text),
- Column("summary", Text),
- Column("event_code", Text),
- Column("alert_id", Integer),
- Column("fa_flight_id", Text),
- Column("ident", Text),
- Column("registration", Text),
- Column("aircraft_type", Text),
- Column("origin", Text),
- Column("destination", Text)
- )
+ "aeroapi_alerts",
+ metadata_obj,
+ Column("id", Integer, primary_key=True, autoincrement=True),
+ Column("time_alert_received", DateTime(timezone=True), server_default=func.now()),
+ # Store time in UTC that the alert was received
+ Column("long_description", Text),
+ Column("short_description", Text),
+ Column("summary", Text),
+ Column("event_code", Text),
+ Column("alert_id", Integer),
+ Column("fa_flight_id", Text),
+ Column("ident", Text),
+ Column("registration", Text),
+ Column("aircraft_type", Text),
+ Column("origin", Text),
+ Column("destination", Text),
+)
def create_tables():
@@ -79,10 +97,12 @@ def create_tables():
try:
# Create the table(s) if they don't exist
metadata_obj.create_all(engine)
- app.logger.info("Table(s) successfully created (if not already created)")
+ logger.info("Table(s) successfully created (if not already created)")
except exc.SQLAlchemyError as e:
# Since creation of table(s) is a critical error, raise exception
- app.logger.error(f"SQL error occurred during creation of table(s) (CRITICAL - THROWING ERROR): {e}")
+ logger.error(
+ f"SQL error occurred during creation of table(s) (CRITICAL - THROWING ERROR): {e}"
+ )
raise e
@@ -99,13 +119,250 @@ def insert_into_table(data_to_insert: Dict[str, Any], table: Table) -> int:
stmt = insert(table)
conn.execute(stmt, data_to_insert)
conn.commit()
- app.logger.info(f"Data successfully inserted into table {table.name}")
+ logger.info(f"Data successfully inserted into table {table.name}")
+ except exc.SQLAlchemyError as e:
+ logger.error(f"SQL error occurred during insertion into table {table.name}: {e}")
+ return -1
+ return 0
+
+
+def delete_from_table(fa_alert_id: int):
+ """
+ Delete alert config from SQL Alert Configurations table based on FA Alert ID.
+ Returns 0 on success, -1 otherwise.
+ """
+ try:
+ with engine.connect() as conn:
+ stmt = delete(aeroapi_alert_configurations).where(
+ aeroapi_alert_configurations.c.fa_alert_id == fa_alert_id
+ )
+ conn.execute(stmt)
+ conn.commit()
+ logger.info(f"Data successfully deleted from {aeroapi_alert_configurations.name}")
+ except exc.SQLAlchemyError as e:
+ logger.error(
+ f"SQL error occurred during deletion from table {aeroapi_alert_configurations.name}: {e}"
+ )
+ return -1
+ return 0
+
+
+def modify_from_table(fa_alert_id: int, modified_data: Dict[str, Any]):
+ """
+ Updates alert config from SQL Alert Configurations table based on FA Alert ID.
+ Returns 0 on success, -1 otherwise.
+ """
+ try:
+ with engine.connect() as conn:
+ stmt = update(aeroapi_alert_configurations).where(
+ aeroapi_alert_configurations.c.fa_alert_id == fa_alert_id
+ )
+ conn.execute(stmt, modified_data)
+ conn.commit()
+ logger.info(f"Data successfully updated in table {aeroapi_alert_configurations.name}")
except exc.SQLAlchemyError as e:
- app.logger.error(f"SQL error occurred during insertion into table {table.name}: {e}")
+ logger.error(
+ f"SQL error occurred during updating in table {aeroapi_alert_configurations.name}: {e}"
+ )
return -1
return 0
+def get_alerts_not_from_app(existing_alert_ids: Set[int]):
+ """
+ Function to get all alert configurations that were not configured
+ inside the webapp. Follows exact same format as SQL table, with extra
+ "is_from_app" column set to False. Takes in existing_alerts parameter
+ as a list to compare with configured alerts to ensure no overlap.
+ Returns a dictionary of all the alerts. If no alerts exist, return None.
+ """
+ api_resource = "/alerts"
+ logger.info(f"Making AeroAPI request to GET {api_resource}")
+ result = AEROAPI.get(f"{AEROAPI_BASE_URL}{api_resource}")
+ if not result:
+ return None
+ all_alerts = result.json()["alerts"]
+ if not all_alerts:
+ return None
+ alerts_not_from_app = []
+ for alert in all_alerts:
+ if int(alert["id"]) not in existing_alert_ids:
+ # Don't have to catch key doesn't exist as AeroAPI guarantees
+ # Keys will exist (just might be null)
+ holder = {
+ "fa_alert_id": alert["id"],
+ "ident": alert["ident"],
+ "origin": alert["origin"],
+ "destination": alert["destination"],
+ "aircraft_type": alert["aircraft_type"],
+ "start_date": alert["start"],
+ "end_date": alert["end"],
+ "max_weekly": 1000,
+ "eta": alert["eta"],
+ "arrival": alert["events"]["arrival"],
+ "cancelled": alert["events"]["cancelled"],
+ "departure": alert["events"]["departure"],
+ "diverted": alert["events"]["diverted"],
+ "filed": alert["events"]["filed"],
+ "is_from_app": False,
+ }
+ alerts_not_from_app.append(holder)
+ return alerts_not_from_app
+
+
+@app.route("/modify", methods=["POST"])
+def modify_alert():
+ """
+ Function to modify the alert given (with key "fa_alert_id" in the payload).
+ Modifies the given alert via AeroAPI PUT call and also modifies the respective
+ alert in the SQLite database. Returns JSON Response in form {"Success": True/False,
+ "Description": }
+ """
+ r_success: bool = False
+ r_description: str
+ # Process json
+ content_type = request.headers.get("Content-Type")
+ data: Dict[str, Any]
+
+ if content_type != "application/json":
+ r_description = "Invalid content sent"
+ else:
+ data = request.json
+
+ fa_alert_id = data.pop("fa_alert_id")
+
+ # Make deep copy to send to AeroAPI - needs events in nested dictionary
+ aeroapi_adjusted_data = copy.deepcopy(data)
+ aeroapi_adjusted_data["events"] = {
+ "arrival": aeroapi_adjusted_data.pop("arrival"),
+ "departure": aeroapi_adjusted_data.pop("departure"),
+ "cancelled": aeroapi_adjusted_data.pop("cancelled"),
+ "diverted": aeroapi_adjusted_data.pop("diverted"),
+ "filed": aeroapi_adjusted_data.pop("filed"),
+ }
+ # Rename start and end again
+ aeroapi_adjusted_data["start"] = aeroapi_adjusted_data.pop("start_date")
+ aeroapi_adjusted_data["end"] = aeroapi_adjusted_data.pop("end_date")
+
+ api_resource = f"/alerts/{fa_alert_id}"
+ logger.info(f"Making AeroAPI request to PUT {api_resource}")
+ result = AEROAPI.put(f"{AEROAPI_BASE_URL}{api_resource}", json=aeroapi_adjusted_data)
+ if result.status_code != 204:
+ # return to front end the error, decode and clean the response
+ try:
+ processed_json = result.json()
+ r_description = f"Error code {result.status_code} with the following description for alert configuration {fa_alert_id}: {processed_json['detail']}"
+ except json.decoder.JSONDecodeError:
+ r_description = f"Error code {result.status_code} for the alert configuration {fa_alert_id} could not be parsed into JSON. The following is the HTML response given: {result.text}"
+ else:
+ # Parse into datetime to update in SQLite table
+ if data["start_date"]:
+ data["start_date"] = datetime.strptime(data["start_date"], "%Y-%m-%d")
+ if data["end_date"]:
+ data["end_date"] = datetime.strptime(data["end_date"], "%Y-%m-%d")
+
+ # Check if data was inserted into database properly
+ if modify_from_table(fa_alert_id, data) == -1:
+ r_description = (
+ "Error modifying the alert configuration from the SQL Database - since it was modified "
+ "on AeroAPI but not SQL, this means the alert will still be the original alert on the table - in "
+ "order to properly modify the alert please look in your SQL database."
+ )
+ else:
+ r_success = True
+ r_description = (
+ f"Request sent successfully, alert configuration {fa_alert_id} has been updated"
+ )
+
+ return jsonify({"Success": r_success, "Description": r_description})
+
+
+@app.route("/delete", methods=["POST"])
+def delete_alert():
+ """
+ Function to delete the alert given (with key "fa_alert_id" in the payload).
+ Deletes the given alert via AeroAPI DELETE call and then deletes it from the
+ SQLite database. Returns JSON Response in form {"Success": True/False,
+ "Description": }
+ """
+ r_success: bool = False
+ r_description: str
+ # Process json
+ content_type = request.headers.get("Content-Type")
+ data: Dict[str, Any]
+
+ if content_type != "application/json":
+ r_description = "Invalid content sent"
+ else:
+ data = request.json
+ fa_alert_id = data["fa_alert_id"]
+ api_resource = f"/alerts/{fa_alert_id}"
+ logger.info(f"Making AeroAPI request to DELETE {api_resource}")
+ result = AEROAPI.delete(f"{AEROAPI_BASE_URL}{api_resource}", json=data)
+ if result.status_code != 204:
+ # return to front end the error, decode and clean the response
+ try:
+ processed_json = result.json()
+ r_description = f"Error code {result.status_code} with the following description for alert configuration {fa_alert_id}: {processed_json['detail']}"
+ except json.decoder.JSONDecodeError:
+ r_description = f"Error code {result.status_code} for the alert configuration {fa_alert_id} could not be parsed into JSON. The following is the HTML response given: {result.text}"
+ else:
+ # Check if data was inserted into database properly
+ if delete_from_table(fa_alert_id) == -1:
+ r_description = "Error deleting the alert configuration from the SQL Database - since it was deleted \
+ on AeroAPI but not SQL, this means the alert will still be shown on the table - in order to properly \
+ delete the alert please look in your SQL database."
+ else:
+ r_success = True
+ r_description = (
+ f"Request sent successfully, alert configuration {fa_alert_id} has been deleted"
+ )
+
+ return jsonify({"Success": r_success, "Description": r_description})
+
+
+@app.route("/posted_alerts")
+def get_posted_alerts():
+ """
+ Function to return all the alerts that are currently configured
+ via the SQL table.
+ """
+ data: Dict[str, Any] = {"posted_alerts": []}
+ with engine.connect() as conn:
+ stmt = select(aeroapi_alerts)
+ result = conn.execute(stmt)
+ conn.commit()
+ for row in result:
+ data["posted_alerts"].append(dict(row))
+
+ return jsonify(data)
+
+
+@app.route("/alert_configs")
+def get_alert_configs():
+ """
+ Function to return all the alerts that are currently configured
+ via the SQL table.
+ """
+ data: Dict[str, Any] = {"alert_configurations": []}
+ existing_alert_ids = set()
+ with engine.connect() as conn:
+ stmt = select(aeroapi_alert_configurations)
+ result = conn.execute(stmt)
+ conn.commit()
+ for row in result:
+ row_holder = dict(row)
+ row_holder["is_from_app"] = True
+ data["alert_configurations"].append(row_holder)
+ existing_alert_ids.add(row_holder["fa_alert_id"])
+
+ # Append alerts not created from app
+ alerts_not_from_app = get_alerts_not_from_app(existing_alert_ids)
+ data["alert_configurations"].extend(alerts_not_from_app)
+
+ return jsonify(data)
+
+
@app.route("/post", methods=["POST"])
def handle_alert() -> Tuple[Response, int]:
"""
@@ -146,7 +403,9 @@ def handle_alert() -> Tuple[Response, int]:
r_status = 200
except KeyError as e:
# If value doesn't exist, do not insert into table and produce error
- app.logger.error(f"Alert POST request did not have one or more keys with data. Will process but will return 400: {e}")
+ logger.error(
+ f"Alert POST request did not have one or more keys with data. Will process but will return 400: {e}"
+ )
r_title = "Missing info in request"
r_detail = "At least one value to insert in the database is missing in the post request"
r_status = 400
@@ -166,7 +425,7 @@ def create_alert() -> Response:
# initialize response headers
r_alert_id: int = -1
r_success: bool = False
- r_description: str = ''
+ r_description: str
# Process json
content_type = request.headers.get("Content-Type")
data: Dict[str, Any]
@@ -190,7 +449,7 @@ def create_alert() -> Response:
if "max_weekly" not in data:
data["max_weekly"] = 1000
- app.logger.info(f"Making AeroAPI request to POST {api_resource}")
+ logger.info(f"Making AeroAPI request to POST {api_resource}")
result = AEROAPI.post(f"{AEROAPI_BASE_URL}{api_resource}", json=data)
if result.status_code != 201:
# return to front end the error, decode and clean the response