Skip to content

Commit 70fa4f2

Browse files
authored
Merge pull request #5 from flightaware/BCK-7002-3_add_endpoint_and_store
Add POST Request Handling and SQL Storage
2 parents 10a249b + d5e8cd7 commit 70fa4f2

File tree

2 files changed

+116
-22
lines changed

2 files changed

+116
-22
lines changed

alerts_backend/python/app.py

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
"""Query alert information from AeroAPI and present it to a frontend service"""
22
import os
33
from datetime import datetime
4-
from typing import Dict, Any, Union
4+
from typing import Dict, Any, Tuple
55

66
import json
77
import requests
88
from flask import Flask, jsonify, Response, request
99
from flask_cors import CORS
1010

1111
from sqlalchemy import (exc, create_engine, MetaData, Table,
12-
Column, Integer, Boolean, Text, insert, Date)
12+
Column, Integer, Boolean, Text, insert, Date, DateTime)
13+
from sqlalchemy.sql import func
1314

1415
AEROAPI_BASE_URL = "https://aeroapi.flightaware.com/aeroapi"
1516
AEROAPI_KEY = os.environ["AEROAPI_KEY"]
@@ -24,9 +25,14 @@
2425
engine = create_engine(
2526
"sqlite+pysqlite:////var/db/aeroapi_alerts/aeroapi_alerts.db", echo=False, future=True
2627
)
28+
# Set journal_mode to WAL to enable reading and writing concurrently
29+
with engine.connect() as conn_wal:
30+
conn_wal.exec_driver_sql("PRAGMA journal_mode=WAL")
31+
conn_wal.commit()
2732

28-
# Define table and metadata to insert and create
33+
# Define tables and metadata to insert and create
2934
metadata_obj = MetaData()
35+
# Table for alert configurations
3036
aeroapi_alert_configurations = Table(
3137
"aeroapi_alert_configurations",
3238
metadata_obj,
@@ -45,45 +51,109 @@
4551
Column("diverted", Boolean),
4652
Column("filed", Boolean),
4753
)
54+
# Table for POSTed alerts
55+
aeroapi_alerts = Table(
56+
"aeroapi_alerts",
57+
metadata_obj,
58+
Column("id", Integer, primary_key=True, autoincrement=True),
59+
Column("time_alert_received", DateTime(timezone=True), server_default=func.now()), # Store time in UTC that the alert was received
60+
Column("long_description", Text),
61+
Column("short_description", Text),
62+
Column("summary", Text),
63+
Column("event_code", Text),
64+
Column("alert_id", Integer),
65+
Column("fa_flight_id", Text),
66+
Column("ident", Text),
67+
Column("registration", Text),
68+
Column("aircraft_type", Text),
69+
Column("origin", Text),
70+
Column("destination", Text)
71+
)
4872

4973

50-
def create_table():
74+
def create_tables():
5175
"""
52-
Check if the tables exist, and if they don't create them.
76+
Check if the table(s) exist, and if they don't create them.
5377
Returns None, raises exception if error
5478
"""
5579
try:
56-
# Create the table if it doesn't exist
80+
# Create the table(s) if they don't exist
5781
metadata_obj.create_all(engine)
58-
app.logger.info("Table successfully created (if not already created)")
82+
app.logger.info("Table(s) successfully created (if not already created)")
5983
except exc.SQLAlchemyError as e:
60-
# Since creation of table is a critical error, raise exception
61-
app.logger.error(f"SQL error occurred during creation of table (CRITICAL - THROWING ERROR): {e}")
84+
# Since creation of table(s) is a critical error, raise exception
85+
app.logger.error(f"SQL error occurred during creation of table(s) (CRITICAL - THROWING ERROR): {e}")
6286
raise e
6387

6488

65-
def insert_into_db(data_to_insert: Dict[str, Union[str, int, bool]]) -> int:
89+
def insert_into_table(data_to_insert: Dict[str, Any], table: Table) -> int:
6690
"""
67-
Insert object into the database based off of the engine.
68-
Assumes data_to_insert has values for all the keys:
69-
fa_alert_id, ident, origin, destination, aircraft_type, start_date, end_date.
91+
Insert object into the database based off of the table.
92+
Assumes data_to_insert has values for all the keys
93+
that are in the data_to_insert variable, and also that
94+
table is a valid SQLAlchemy Table variable inside the database.
7095
Returns 0 on success, -1 otherwise
7196
"""
7297
try:
7398
with engine.connect() as conn:
74-
stmt = insert(aeroapi_alert_configurations)
99+
stmt = insert(table)
75100
conn.execute(stmt, data_to_insert)
76101
conn.commit()
77-
78-
app.logger.info("Data successfully inserted into table")
79-
102+
app.logger.info(f"Data successfully inserted into table {table.name}")
80103
except exc.SQLAlchemyError as e:
81-
app.logger.error(f"SQL error occurred during insertion into table: {e}")
104+
app.logger.error(f"SQL error occurred during insertion into table {table.name}: {e}")
82105
return -1
83-
84106
return 0
85107

86108

109+
@app.route("/post", methods=["POST"])
110+
def handle_alert() -> Tuple[Response, int]:
111+
"""
112+
Function to receive AeroAPI POST requests. Filters the request
113+
and puts the necessary data into the SQL database.
114+
Returns a JSON Response and also the status code in a tuple.
115+
"""
116+
# Form response
117+
r_title: str
118+
r_detail: str
119+
r_status: int
120+
data: Dict[str, Any] = request.json
121+
# Process data by getting things needed
122+
processed_data: Dict[str, Any]
123+
try:
124+
processed_data = {
125+
"long_description": data["long_description"],
126+
"short_description": data["short_description"],
127+
"summary": data["summary"],
128+
"event_code": data["event_code"],
129+
"alert_id": data["alert_id"],
130+
"fa_flight_id": data["flight"]["fa_flight_id"],
131+
"ident": data["flight"]["ident"],
132+
"registration": data["flight"]["registration"],
133+
"aircraft_type": data["flight"]["aircraft_type"],
134+
"origin": data["flight"]["origin"],
135+
"destination": data["flight"]["destination"],
136+
}
137+
138+
# Check if data was inserted into database properly
139+
if insert_into_table(processed_data, aeroapi_alerts) == -1:
140+
r_title = "Error inserting into SQL Database"
141+
r_detail = "Inserting into the database had an error"
142+
r_status = 500
143+
else:
144+
r_title = "Successful request"
145+
r_detail = "Request processed and stored successfully"
146+
r_status = 200
147+
except KeyError as e:
148+
# If value doesn't exist, do not insert into table and produce error
149+
app.logger.error(f"Alert POST request did not have one or more keys with data. Will process but will return 400: {e}")
150+
r_title = "Missing info in request"
151+
r_detail = "At least one value to insert in the database is missing in the post request"
152+
r_status = 400
153+
154+
return jsonify({"title": r_title, "detail": r_detail, "status": r_status}), r_status
155+
156+
87157
@app.route("/create", methods=["POST"])
88158
def create_alert() -> Response:
89159
"""
@@ -99,7 +169,7 @@ def create_alert() -> Response:
99169
r_description: str = ''
100170
# Process json
101171
content_type = request.headers.get("Content-Type")
102-
data: Dict[Any]
172+
data: Dict[str, Any]
103173

104174
if content_type != "application/json":
105175
r_description = "Invalid content sent"
@@ -148,7 +218,7 @@ def create_alert() -> Response:
148218
data["end_date"] = datetime.strptime(data["end_date"], "%Y-%m-%d")
149219
data["fa_alert_id"] = fa_alert_id
150220

151-
if insert_into_db(data) == -1:
221+
if insert_into_table(data, aeroapi_alert_configurations) == -1:
152222
r_description = f"Database insertion error, check your database configuration. Alert has still been configured with alert id {r_alert_id}"
153223
else:
154224
r_success = True
@@ -159,5 +229,5 @@ def create_alert() -> Response:
159229

160230
if __name__ == "__main__":
161231
# Create the table if it wasn't created before startup
162-
create_table()
232+
create_tables()
163233
app.run(host="0.0.0.0", port=5000, debug=True)

docker-compose-alerts.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ services:
2121
max-size: "10mb"
2222
max-file: "5"
2323

24+
# Create separate Docker service to handle only POST requests to server, default port is 8081
25+
# Note that the port mapped is 5000 instead of 80, and the endpoint URL MUST use /post, not /api/post
26+
python-alerts-endpoint-backend:
27+
image: "ghcr.io/flightaware/aeroapps/python-alerts-backend:${AEROAPPS_VERSION:-latest}"
28+
volumes:
29+
- "aeroapi_alerts:/var/db/aeroapi_alerts"
30+
profiles: [ "python" ]
31+
build:
32+
context: .
33+
dockerfile: alerts_backend/python/Dockerfile
34+
ports:
35+
- "${POST_PORT:-8081}:5000"
36+
networks:
37+
internal:
38+
aliases:
39+
- alerts-backend
40+
environment:
41+
- AEROAPI_KEY=${AEROAPI_KEY:?AEROAPI_KEY variable must be set}
42+
logging:
43+
driver: "json-file"
44+
options:
45+
max-size: "10mb"
46+
max-file: "5"
47+
2448
alerts-frontend:
2549
image: "ghcr.io/flightaware/alerts_frontend/alerts-frontend:${ALERTS_VERSION:-latest}"
2650
profiles: ["python"]

0 commit comments

Comments
 (0)