Initial commit: boarding pass to AirTrail pipeline
Photograph/screenshot a boarding pass, decode its BCBP barcode (PDF417/ Aztec/QR/DataMatrix), fetch the historical flight track from FlightAware the day after the flight, stage it for review, and write it into AirTrail via its REST API on approval. ntfy notifications carry signed Approve/Deny/Investigate actions.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import json
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import db
|
||||
|
||||
JSON_FIELDS = {"track_coordinates", "track_times", "parse_warnings"}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _row_to_dict(row) -> dict:
|
||||
if row is None:
|
||||
return None
|
||||
d = dict(row)
|
||||
for field in JSON_FIELDS:
|
||||
if d.get(field):
|
||||
try:
|
||||
d[field] = json.loads(d[field])
|
||||
except (TypeError, ValueError):
|
||||
d[field] = None
|
||||
return d
|
||||
|
||||
|
||||
# --- settings -----------------------------------------------------------
|
||||
|
||||
def get_setting(key: str, default: str | None = None) -> str | None:
|
||||
with db.connect() as conn:
|
||||
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
|
||||
return row["value"] if row else default
|
||||
|
||||
|
||||
def set_setting(key: str, value: str) -> None:
|
||||
with db.connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO settings (key, value) VALUES (?, ?) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
(key, value),
|
||||
)
|
||||
|
||||
|
||||
def get_or_create_secret_key() -> str:
|
||||
existing = get_setting("secret_key")
|
||||
if existing:
|
||||
return existing
|
||||
new_key = secrets.token_hex(32)
|
||||
set_setting("secret_key", new_key)
|
||||
return new_key
|
||||
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
"notify_backend": "ntfy",
|
||||
"ntfy_server_url": "https://ntfy.sh",
|
||||
"ntfy_topic": "",
|
||||
"ntfy_auth_token": "",
|
||||
"daily_run_time": "03:00",
|
||||
}
|
||||
|
||||
|
||||
def get_all_settings() -> dict:
|
||||
return {key: get_setting(key, default) for key, default in DEFAULT_SETTINGS.items()}
|
||||
|
||||
|
||||
# --- pending_flights ------------------------------------------------------
|
||||
|
||||
INSERT_FIELDS = [
|
||||
"image_path", "raw_bcbp", "passenger_name", "pnr", "from_iata", "to_iata",
|
||||
"from_icao", "to_icao", "operating_carrier_iata", "operating_carrier_icao",
|
||||
"flight_number", "julian_day", "flight_date", "known_departure_time",
|
||||
"compartment", "seat", "process_after", "parse_warnings", "error_message",
|
||||
"status",
|
||||
]
|
||||
|
||||
|
||||
def create_pending_flight(**fields) -> int:
|
||||
now = _now()
|
||||
cols = ["created_at", "updated_at"]
|
||||
vals = [now, now]
|
||||
for f in INSERT_FIELDS:
|
||||
if f in fields:
|
||||
v = fields[f]
|
||||
if f in JSON_FIELDS and v is not None:
|
||||
v = json.dumps(v)
|
||||
cols.append(f)
|
||||
vals.append(v)
|
||||
placeholders = ", ".join("?" for _ in cols)
|
||||
with db.connect() as conn:
|
||||
cur = conn.execute(
|
||||
f"INSERT INTO pending_flights ({', '.join(cols)}) VALUES ({placeholders})",
|
||||
vals,
|
||||
)
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def get_flight(flight_id: int) -> dict | None:
|
||||
with db.connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM pending_flights WHERE id = ?", (flight_id,)
|
||||
).fetchone()
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def list_flights(statuses: list[str] | None = None) -> list[dict]:
|
||||
with db.connect() as conn:
|
||||
if statuses:
|
||||
placeholders = ", ".join("?" for _ in statuses)
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM pending_flights WHERE status IN ({placeholders}) "
|
||||
"ORDER BY created_at DESC",
|
||||
statuses,
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM pending_flights ORDER BY created_at DESC"
|
||||
).fetchall()
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def list_due_flights(now_iso: str) -> list[dict]:
|
||||
with db.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM pending_flights WHERE status = 'queued' AND process_after <= ?",
|
||||
(now_iso,),
|
||||
).fetchall()
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def update_flight(flight_id: int, **fields) -> None:
|
||||
if not fields:
|
||||
return
|
||||
cols = []
|
||||
vals = []
|
||||
for k, v in fields.items():
|
||||
if k in JSON_FIELDS and v is not None and not isinstance(v, str):
|
||||
v = json.dumps(v)
|
||||
cols.append(f"{k} = ?")
|
||||
vals.append(v)
|
||||
cols.append("updated_at = ?")
|
||||
vals.append(_now())
|
||||
vals.append(flight_id)
|
||||
with db.connect() as conn:
|
||||
conn.execute(
|
||||
f"UPDATE pending_flights SET {', '.join(cols)} WHERE id = ?", vals
|
||||
)
|
||||
Reference in New Issue
Block a user