Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit Alerts #15

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
299 changes: 258 additions & 41 deletions alerts_backend/python/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"""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,
Expand All @@ -24,6 +23,9 @@
insert,
Date,
select,
DateTime,
delete,
update,
)

AEROAPI_BASE_URL = "https://aeroapi.flightaware.com/aeroapi"
Expand All @@ -49,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():
Expand Down Expand Up @@ -123,6 +126,201 @@ def insert_into_table(data_to_insert: Dict[str, Any], table: Table) -> int:
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:
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": <A detailed description of the response>}
"""
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": <A detailed description of the response>}
"""
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():
"""
Expand All @@ -147,12 +345,20 @@ def get_alert_configs():
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:
data["alert_configurations"].append(dict(row))
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)

Expand Down Expand Up @@ -197,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
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
Expand All @@ -217,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]
Expand Down Expand Up @@ -265,8 +473,17 @@ def create_alert() -> Response:
# Default to None in case a user directly submits an incomplete payload
data["start_date"] = data.pop("start", None)
data["end_date"] = data.pop("end", None)
data["start_date"] = datetime.strptime(data["start_date"], "%Y-%m-%d")
data["end_date"] = datetime.strptime(data["end_date"], "%Y-%m-%d")
# Allow empty strings
if data["start_date"] == "":
data["start_date"] = None
if data["end_date"] == "":
data["end_date"] = None
# Handle if dates are None - accept them but don't parse time
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")

data["fa_alert_id"] = fa_alert_id

if insert_into_table(data, aeroapi_alert_configurations) == -1:
Expand Down