A Flask web application for tracking which aircraft are displayed at which aviation museums worldwide, with proximity search to find the nearest museum with a given aircraft.
- Python 3.9+
- MySQL 8.0+ (or MariaDB 10.6+)
mysql -u root -p < schema.sqlpip install -r requirements.txtexport MYSQL_HOST=127.0.0.1
export MYSQL_PORT=3306
export MYSQL_USER=root
export MYSQL_PASSWORD=yourpassword
export MYSQL_DB=airplane_museum_tracker
export SECRET_KEY=your-secret-key-herepython seed_data.pyThis creates a default admin account and prints an API key. Save the API key for programmatic access.
python app.pyVisit http://localhost:5000 in your browser.
- Aircraft Search -- search by tail number, model, variant (e.g. C-130J vs C-130H), name, or manufacturer
- Museum Directory -- browse and filter museums by region or country
- International Support -- museums from any country; coordinates are optional
- Proximity Search -- enter an aircraft and zip/postal code or city to find the nearest museum with that aircraft
- Admin Panel -- login-protected panel with full CRUD: list/create/edit/delete for aircraft, museums, and exhibit links
- REST API -- versioned JSON API (
/api/v1/) with Bearer token authentication - API Key Management -- generate, list, and revoke API keys from the web UI
- API Documentation -- built-in interactive docs at
/api/v1/docs
Session-based authentication via Flask-Login. Register at /register or use the seeded admin account. The admin panel and API key management pages require login.
Bearer token authentication. Include your API key in the Authorization header:
Authorization: Bearer amt_your_api_key_here
Three permission levels:
| Level | Can do |
|---|---|
read |
Search, view details, proximity lookups (public endpoints also work without a key) |
readwrite |
All of read, plus create and update records |
admin |
All of readwrite, plus delete records |
Generate API keys from the web UI at /admin/api-keys, or the first key is printed when you run seed_data.py.
All endpoints are prefixed with /api/v1/.
| Method | Endpoint | Description |
|---|---|---|
| GET | /aircraft/search?q= |
Search aircraft |
| GET | /aircraft/{id} |
Aircraft detail with museums |
| GET | /museums/search?q=®ion=&country=&state= |
Search museums |
| GET | /museums/{id} |
Museum detail with aircraft |
| GET | /museums/regions |
List regions with counts |
| GET | /museums/countries |
List countries with counts |
| GET | /nearest?aircraft=&location= |
Find nearest museum |
| GET | /stats |
Dashboard counts |
| GET | /docs |
API documentation page |
| Method | Endpoint | Description |
|---|---|---|
| POST | /aircraft |
Create aircraft |
| PUT | /aircraft/{id} |
Update aircraft |
| POST | /museums |
Create museum |
| PUT | /museums/{id} |
Update museum |
| POST | /exhibits |
Link aircraft to museum |
| PUT | /exhibits/{id} |
Update exhibit |
| Method | Endpoint | Description |
|---|---|---|
| DELETE | /aircraft/{id} |
Delete aircraft |
| DELETE | /museums/{id} |
Delete museum |
| DELETE | /exhibits/{id} |
Delete exhibit link |
| Method | Endpoint | Description |
|---|---|---|
| GET | /keys |
List your API keys |
| POST | /keys |
Generate new API key |
| DELETE | /keys/{id} |
Revoke an API key |
Museums now support international locations:
| Field | Required | Notes |
|---|---|---|
name |
Yes | Museum name |
city |
Yes | City |
state_province |
No | State, province, county, etc. |
country |
Yes | Country name (defaults to "United States") |
postal_code |
No | Zip/postal code (format varies by country) |
region |
Yes | North America, Europe, Asia-Pacific, Middle East, South America, Africa, Oceania |
address |
No | Full street address |
website |
No | URL |
latitude |
No | Decimal degrees (for proximity search) |
longitude |
No | Decimal degrees (for proximity search) |
Museums without coordinates are still searchable and browsable, but won't appear in distance-sorted proximity results. They are listed separately when relevant.
# Search (no auth needed)
curl http://localhost:5000/api/v1/aircraft/search?q=C-130
# Create museum (readwrite key) — only name, city, country, region required
curl -X POST http://localhost:5000/api/v1/museums \
-H "Authorization: Bearer amt_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"RAF Museum","city":"London","country":"United Kingdom","region":"Europe"}'
# Update museum with coordinates
curl -X PUT http://localhost:5000/api/v1/museums/1 \
-H "Authorization: Bearer amt_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"latitude":51.5953,"longitude":-0.2376}'
# Delete (admin key only)
curl -X DELETE http://localhost:5000/api/v1/aircraft/42 \
-H "Authorization: Bearer amt_YOUR_KEY"Aircraft and museums can be imported in bulk from CSV or JSON.
Web UI: /admin/import — file upload or paste, with a Validate (dry-run)
button before the real Import. The page is in the admin nav under
Bulk Import.
API: POST /api/v1/aircraft/bulk_import, POST /api/v1/museums/bulk_import.
Either send a multipart file upload, or a JSON body of the form
{ "format": "csv" | "json" | "auto",
"data": "<the CSV or JSON text>",
"dry_run": false }The response is a per-row report:
{ "created": 4, "skipped": 0, "errors": [], "dry_run": false }Rules
- Permission:
adminoraircraft_admin. - Cap: 5,000 rows per request. Split larger imports.
- Atomic: any validation error rolls back the whole batch — partial imports are too painful to debug after the fact.
- Existing duplicates (same
(model, tail_number)for aircraft, same(name, city, country)for museums) are reported as skipped and cause the batch to roll back. Re-import after removing them.
Aircraft column / field names (CSV header order = JSON keys)
manufacturer, model, variant, tail_number, model_name,
aircraft_name, aircraft_type, wing_type, military_civilian,
role_type, year_built, description, aliases. Required:
manufacturer, model. aliases in CSV is semicolon-separated
(Herc;Hercules); in JSON it's an array.
Museum field names
name, city, state_province, country, postal_code, region,
address, website, latitude, longitude. Required: name, city,
region. latitude and longitude must both be present or both empty.
Sample files: scripts/sample_aircraft.csv, scripts/sample_museums.csv.
A small set of CLI tools in scripts/ that talk to the public REST API.
They depend only on the requests
library — install once: pip install requests.
Configuration (env vars; flags override)
AIRPLANE_BASE_URL # default http://127.0.0.1:5000
AIRPLANE_API_KEY # required only for write operations (admin / aircraft_admin)
Tools
| Script | What it does |
|---|---|
airplane_api.py |
Reusable AirplaneClient class — used by every script below; also fine to import from your own one-offs. |
export_aircraft.py |
Dump every aircraft to CSV or JSON in the bulk-import format. |
export_museums.py |
Same for museums. |
import_data.py |
POST a CSV/JSON file to the bulk-import endpoint with --dry-run support. |
find_nearest.py |
CLI wrapper for /api/v1/nearest. |
health_check.py |
Smoke-tests /api/v1/stats, exits non-zero on failure. Cron-friendly. |
Common workflows
# Backup the catalog (no API key needed; reads are public)
python3 scripts/export_aircraft.py --format json --out aircraft_backup.json
python3 scripts/export_museums.py --format json --out museum_backup.json
# Round-trip: export → edit in a spreadsheet → re-import
python3 scripts/export_aircraft.py --format csv --out aircraft.csv
# (open aircraft.csv in Excel, edit)
AIRPLANE_API_KEY=amt_... \
python3 scripts/import_data.py --entity aircraft --file aircraft.csv --dry-run
AIRPLANE_API_KEY=amt_... \
python3 scripts/import_data.py --entity aircraft --file aircraft.csv
# Quick lookups (no auth)
python3 scripts/find_nearest.py "C-130" "Dayton, OH"
# Cron-friendly status probe (exits 0 on success, non-zero on failure)
* * * * * python3 /opt/AirplaneFinder/scripts/health_check.py --quiet \
|| curl -s https://status-collector.example.com/airplane-downpip install -r requirements.txt -r requirements-dev.txt
pytestThe suite uses an in-memory SQLite database — no MySQL needed locally. 86 tests covering: auth flow (login, logout, lockout, session timeout, session-fixation defense), role-based access, security hardening (headers, open-redirect, default-secret guard), the aircraft API regression-prone paths (link_id, uniqueness, sort whitelist), pure helpers, and the logger fallback.
Run a single file or test:
pytest tests/test_auth.py
pytest tests/test_auth.py::TestLogout::test_logout_actually_logs_out_with_remember_cookie -vBefore each deploy, run:
bash scripts/security_check.shThis runs pip-audit against requirements.txt to flag any dependencies
with known CVEs in the PyPI Advisory Database,
and prints a summary of outdated packages in your .venv for visibility.
The script installs pip-audit into a one-shot temporary venv if it isn't
already on your $PATH, so it doesn't pollute the app's runtime
environment. Exit code is non-zero on findings, so it can fail a CI pipeline
or a deploy script — wire it in as the first step of whatever you use.
airplane-museum-tracker/
├── app.py # Flask app: routes, auth, API
├── models.py # SQLAlchemy models (User, ApiKey, Aircraft, Museum, etc.)
├── config.py # Configuration
├── schema.sql # MySQL schema (includes users + api_keys tables)
├── seed_data.py # Sample data + default admin user/key
├── requirements.txt
├── static/
│ ├── css/style.css
│ └── js/app.js
└── templates/
├── base.html # Layout with auth-aware nav
├── index.html # Dashboard + proximity search
├── aircraft.html # Aircraft directory
├── museums.html # Museum directory
├── login.html # Login page
├── register.html # Registration page
├── admin.html # Admin panel (CRUD)
├── api_keys.html # API key management
└── api_docs.html # API documentation