diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 92b3fea..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - "env": { - "browser": true, - "es6": true - }, - "extends": "eslint:recommended", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 14, - "sourceType": "module" - }, - "rules": { - } -}; diff --git a/.gitignore b/.gitignore index 87339fa..cf41dea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,4 @@ __pycache__/ __pycache__ .mypy_cache config -.hypothesis -personal-data -static/bootstrap5 -static/leaflet* -static/es-module-shims +.hypothesis/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index fdd8f72..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,67 +0,0 @@ -# Development Guidelines - -## Project Overview -This is a personal agenda web application built with Flask that tracks various events and important dates: -- Events: birthdays, holidays, travel itineraries, conferences, waste collection schedules -- Space launches, meteor showers, astronomical events -- Financial information (FX rates, stock market) -- UK-specific features (holidays, waste collection, railway schedules) -- Authentication via UniAuth -- Frontend uses Bootstrap 5, Leaflet for maps, FullCalendar for calendar views - -## Python Environment -- Always use `python3` directly, never `python` -- All Python code should include type annotations -- Use `typing.Any` instead of `Any` in type hints (import from typing module) -- Run `mypy --strict` (fix any type errors in the file) and `black` on modified code after creating or modifying Python files -- Avoid running `black .` -- Main entry point: `python3 web_view.py` (Flask app on port 5000) -- Tests: Use `pytest` (tests in `/tests/` directory) - -## Project Structure -- `agenda/` - Main Python package with modules for different event types -- `web_view.py` - Flask web application entry point -- `templates/` - Jinja2 HTML templates -- `static/` - CSS, JS, and frontend assets -- `config/` - Configuration files -- `personal-data/` - User's personal data (not in git) - -## Git Workflow -- Avoid committing unrelated untracked files (e.g., `node_modules/`, build artifacts) -- Only commit relevant project files -- Personal data directory (`personal-data/`) is excluded from git - -## Conference attendance fields -Conferences in `conferences.yaml` support optional `attend_start` and `attend_end` fields for when you arrive late or leave early. Both accept a plain date or a datetime with time and timezone (YAML datetime syntax). When present, the trip page shows the attendance dates instead of the official conference dates. The official `start`/`end` fields are always kept for context. - -```yaml -- name: FOSDEM - start: 2023-02-04 - end: 2023-02-05 - attend_end: 2023-02-04 # left after day 1 - attend_start: 2023-02-04 14:00:00+00:00 # or with time -``` - -## Notes -- Trip stats new-country badges come from `agenda.stats.calculate_yearly_stats` via `year_stats.new_countries` (first-visit year, excluding `PREVIOUSLY_VISITED`). -- Trip stats are calculated in `agenda/stats.py`: - - `travel_legs()` extracts airlines, airports, and stations from individual trip travel legs - - `calculate_yearly_stats()` aggregates stats per year including flight/train counts, airlines, airports, stations - - `calculate_overall_stats()` aggregates yearly stats into overall totals for the summary section - -## Travel type patterns -Transport types: `flight`, `train`, `ferry`, `coach`, `bus`. - -**Road transport (bus and coach)** share a common loader `load_road_transport()` in `trip.py`. `load_coaches` and `load_buses` are thin wrappers that pass type name, YAML filenames, and CO2 factor (coach: 0.027 kg/km, bus: 0.1 kg/km). Both use `from_station`/`to_station` fields and support scalar or dict-keyed GeoJSON route filenames in the stop/station data. - -**Ferry** is loaded separately: uses `from_terminal`/`to_terminal` fields and GeoJSON routes come from the terminal's `routes` dict (always a dict, not scalar). - -**Route rendering** (`get_trip_routes`): bus and coach are handled in a single combined block (they use the same pattern — `from_station`/`to_station`, `{type}_routes/` folder). Ferry always has a geojson file and renders as type `"train"` for the map renderer. - -**Trip elements** (`Trip.elements()` in `types.py`): bus and coach are handled in a single combined block using `item["type"] in ("coach", "bus")`. Ferry is separate because it uses `from_terminal`/`to_terminal` and always requires `arrive` (not optional). - -**Location collection** (`get_locations`): bus → `bus_stop`, coach → `coach_station`, ferry → `ferry_terminal` (all separate map pin types). - -**CO2 factors** (kg CO2e per passenger per km): train 0.037, coach 0.027, ferry 0.02254, bus 0.1. - -**Schengen tracking**: ferry journeys are tracked for Schengen compliance; bus and coach are not. diff --git a/agenda/__init__.py b/agenda/__init__.py index 0428f0c..baf8813 100644 --- a/agenda/__init__.py +++ b/agenda/__init__.py @@ -2,7 +2,6 @@ from datetime import date, datetime, time -import pycountry import pytz uk_tz = pytz.timezone("Europe/London") @@ -11,34 +10,3 @@ uk_tz = pytz.timezone("Europe/London") def uk_time(d: date, t: time) -> datetime: """Combine time and date for UK timezone.""" return uk_tz.localize(datetime.combine(d, t)) - - -def format_list_with_ampersand(items: list[str]) -> str: - """Join a list of strings with commas and an ampersand.""" - if len(items) > 1: - return ", ".join(items[:-1]) + " & " + items[-1] - elif items: - return items[0] - return "" - - -def get_country(alpha_2: str | None) -> pycountry.db.Country | None: - """Lookup country by alpha-2 country code.""" - if not alpha_2: - return None - if alpha_2.count(",") > 3: # ESA - return pycountry.db.Country(flag="🇪🇺", name="ESA") - if not alpha_2: - return None - if alpha_2 == "xk": - return pycountry.db.Country( - flag="\U0001f1fd\U0001f1f0", name="Kosovo", alpha_2="xk" - ) - - country: pycountry.db.Country | None = None - if len(alpha_2) == 2: - country = pycountry.countries.get(alpha_2=alpha_2.upper()) - elif len(alpha_2) == 3: - country = pycountry.countries.get(alpha_3=alpha_2.upper()) - - return country diff --git a/agenda/accommodation.py b/agenda/accommodation.py index 5061a83..3ce4b9e 100644 --- a/agenda/accommodation.py +++ b/agenda/accommodation.py @@ -1,23 +1,20 @@ -"""Accommodation.""" - -from collections import defaultdict -from datetime import datetime, timedelta +"""Accomodation""" import yaml -from .event import Event -from .types import StrDict, Trip +from .types import Event def get_events(filepath: str) -> list[Event]: - """Get accommodation from YAML.""" + """Get accomodation from YAML.""" with open(filepath) as f: return [ Event( date=item["from"], end_date=item["to"], name="accommodation", - title=( + title="🧳" + + ( f'{item["location"]} Airbnb' if item.get("operator") == "airbnb" else item["name"] @@ -26,52 +23,3 @@ def get_events(filepath: str) -> list[Event]: ) for item in yaml.safe_load(f) ] - - -def prepare_accommodation_list( - items: list[StrDict], trips: list[Trip], now: datetime -) -> StrDict: - """Link stays to trips and prepare date groups and annual night counts.""" - # Create a dictionary to hold stats for each year - year_stats: defaultdict[int, dict[str, int]] = defaultdict( - lambda: {"total_nights": 0, "nights_abroad": 0} - ) - - # Calculate stats for each year - for stay in items: - current_date = stay["from"].date() - end_date = stay["to"].date() - while current_date < end_date: - year = current_date.year - year_stats[year]["total_nights"] += 1 - if stay.get("country") != "gb": - year_stats[year]["nights_abroad"] += 1 - current_date += timedelta(days=1) - - # Sort the stats by year in descending order - sorted_year_stats = sorted( - year_stats.items(), key=lambda item: item[0], reverse=True - ) - - trip_lookup = {} - - for trip in trips: - for trip_stay in trip.accommodation: - key = (trip_stay["from"], trip_stay["name"]) - trip_lookup[key] = trip - - for item in items: - key = (item["from"], item["name"]) - if this_trip := trip_lookup.get(key): - item["linked_trip"] = this_trip - - past = [conf for conf in items if conf["to"] < now] - current = [conf for conf in items if conf["from"] <= now and conf["to"] >= now] - future = [conf for conf in items if conf["from"] > now] - - return { - "past": past, - "current": current, - "future": future, - "year_stats": sorted_year_stats, - } diff --git a/agenda/add_new_conference.py b/agenda/add_new_conference.py deleted file mode 100644 index e352e50..0000000 --- a/agenda/add_new_conference.py +++ /dev/null @@ -1,801 +0,0 @@ -"""Helpers for adding conferences to the YAML data file.""" - -import configparser -import json -import os -import re -import sys -import typing -from datetime import date, datetime, time, timezone -from urllib.parse import parse_qs, urlparse - -import html2text -import lxml.html -import openai -import pycountry -import requests -import yaml - -from agenda.conference import ConferenceSeries, conference_date_fields, load_series - -USER_AGENT = "add-new-conference/0.1" -COORDINATE_PATTERNS = ( - re.compile(r"@(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)"), - re.compile(r"[?&]q=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)"), - re.compile(r"[?&](?:ll|center)=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)"), - re.compile(r"!3d(-?\d+(?:\.\d+)?)!4d(-?\d+(?:\.\d+)?)"), - re.compile(r"[?&]destination=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)"), -) - - -def read_api_key() -> str: - """Read API key from ~/.config/openai/config.""" - config_path = os.path.expanduser("~/.config/openai/config") - parser = configparser.ConfigParser() - parser.read(config_path) - return parser["openai"]["api_key"] - - -def conference_yaml_format_description() -> str: - """Return the conference YAML format description for LLM prompts.""" - return """ -Use this YAML format for one conference entry. - -Required fields: -- `name`: event name. -- `topic`: topic/category. -- `location`: city or location label. Use `TBC` if the page confirms a future - event but not a city. -- Date information in nested `dates`. - -Event name should have any four digit year removed, so "BSDCan 2027" becomes "BSDCan". - -Preferred date shape: -- `dates.status`: one of `exact`, `tentative`, or `approximate`. -- For `exact`: use when the page confirms specific dates/times. Include - `dates.start` and `dates.end` as YAML dates or timezone-aware datetimes. -- For `tentative`: use when specific dates are guessed or explicitly - unconfirmed. Include `dates.start`, `dates.end`, and preferably `dates.label` - and `dates.basis`. -- For `approximate`: use when only a broad date phrase is known. Include - `dates.label`, `dates.earliest`, and `dates.latest`. Examples: `March 2027` - should become earliest `2027-03-01`, latest `2027-03-31`; `mid-April 2027` - should become a sensible bounded range such as `2027-04-11` to `2027-04-20`. - -Important date rule: -- If the source page contains exact dates, output `dates.status: exact` even if - the existing agenda entry or conference announcement previously had only - approximate dates. -- Always include an end date for `exact` and `tentative`. For a single-day - event, `dates.end` can be the same as `dates.start`. -- Do not output legacy top-level `start`, `end`, or `date_status`. - -Common optional fields: -- `series`: a key from the known conference series list, when this event belongs - to a listed series. -- `country`: valid ISO 3166-1 alpha-2 country code in lowercase, for example - `ca`, `gb`, `us`. Do not output country names. -- `venue`, `address`, `latitude`, `longitude`, `url`, `cfp_url`, `cfp_end`, - `hashtag`, `description`. -- For an address written in a non-Latin script, output a conventional - Latin-script rendering only when it can be derived confidently. Do not invent - or loosely translate an address. If no reliable rendering is available, omit - `address`; coordinates are preferable to an unreadable or inaccurate address. -- `free`, `price`, `currency`, `hackathon`, `online`, `attendees`. -- When `free: true`, omit `price` and `currency` because they are redundant. -- Do not include `going`, `registered`, `accommodation_booked`, - `transport_booked`, or `trip` unless the source explicitly says they apply to - my attendance. -""" - - -def yaml_example_text() -> str: - """Return examples of the conference YAML format.""" - return """ -- name: Geomob London - series: geomob-london - topic: Maps - location: London - country: gb - dates: - status: exact - start: 2026-01-28 18:00:00+00:00 - end: 2026-01-28 22:00:00+00:00 - url: https://thegeomob.com/post/jan-28th-2026-geomoblon-details - venue: Geovation Hub - address: Sutton Yard, 65 Goswell Rd, London EC1V 7EN - latitude: 51.5242464 - longitude: -0.0997024 - free: true - hashtag: '#geomobLON' - -- name: DebConf 25 - series: debconf - topic: Debian - location: Plouzane - country: fr - dates: - status: exact - start: 2025-07-07 - end: 2025-07-20 - url: https://wiki.debian.org/DebConf/25 - cfp_url: https://debconf25.debconf.org/talks/new/ - venue: Ecole nationale superieure Mines-Telecom Atlantique Bretagne Pays de la Loire - campus de Brest - latitude: 48.35934 - longitude: -4.569889 - -- name: Wikimedia Hackathon - series: wikimedia-hackathon - topic: Wikimedia - location: Albania - country: al - dates: - status: approximate - label: mid-April 2027 - earliest: 2027-04-11 - latest: 2027-04-20 - url: https://www.mediawiki.org/wiki/Wikimedia_Hackathon_2027 - hackathon: true - -- name: PyCascades - series: pycascades - topic: Python - location: Seattle, Washington - country: us - dates: - status: approximate - label: March 2027 - earliest: 2027-03-01 - latest: 2027-03-31 -""" - - -def series_prompt_text(series: dict[str, ConferenceSeries]) -> str: - """Return compact known series text for the LLM prompt.""" - if not series: - return "No known conference series loaded." - - lines = ["Known conference series IDs:"] - for series_id, item in sorted(series.items()): - details = [item["name"]] - if topic := item.get("topic"): - details.append(f"topic: {topic}") - if location := item.get("usual_location"): - details.append(f"usual location: {location}") - if country := item.get("country"): - details.append(f"country: {country}") - lines.append(f"- {series_id}: " + "; ".join(details)) - return "\n".join(lines) - - -def build_prompt( - url: str, - source_text: str, - detected_coordinates: tuple[float, float] | None, - series: dict[str, ConferenceSeries] | None = None, -) -> str: - """Build prompt with embedded YAML format details and examples.""" - coordinate_note = "" - if detected_coordinates is not None: - coordinate_note = ( - "\nDetected venue coordinates from a map link on the page:\n" - f"latitude: {detected_coordinates[0]}\n" - f"longitude: {detected_coordinates[1]}\n" - ) - - prompt = f""" -I keep a record of interesting conferences in a YAML file. - -Format rules: - -{conference_yaml_format_description()} - -{series_prompt_text(series or {})} - -Here are some examples of the format I use: - -{yaml_example_text()} - -Now here is a new conference of interest: - -Conference URL: {url} - -Return the YAML representation for this conference following the same style and -keys as the examples. Only include keys if the information is available. Do not -invent details. - -Important: if this is a Geomob event, use a `dates.end` datetime of 22:00 local -time on the event date unless the page explicitly provides a different end time. -{coordinate_note} - -Wrap your answer in a JSON object with a single key "yaml". -=== -{source_text} -""" - return prompt - - -def get_from_open_ai(prompt: str, model: str = "gpt-5.4") -> dict[str, str]: - """Pass prompt to OpenAI and get reply.""" - client = openai.OpenAI(api_key=read_api_key()) - - response = client.chat.completions.create( - messages=[{"role": "user", "content": prompt}], - model=model, - response_format={"type": "json_object"}, - ) - - reply = response.choices[0].message.content - assert isinstance(reply, str) - return typing.cast(dict[str, str], json.loads(reply)) - - -def fetch_webpage(url: str) -> lxml.html.HtmlElement: - """Fetch webpage HTML and parse it.""" - response = requests.get(url, headers={"User-Agent": USER_AGENT}) - response.raise_for_status() - return lxml.html.fromstring(response.content) - - -def webpage_to_text(root: lxml.html.HtmlElement) -> str: - """Convert parsed HTML into readable text content.""" - metadata = extract_event_metadata(root) - root_copy = lxml.html.fromstring(lxml.html.tostring(root)) - - for script_or_style in root_copy.xpath("//script|//style"): - script_or_style.drop_tree() - - text_maker = html2text.HTML2Text() - text_maker.ignore_links = True - text_maker.ignore_images = True - page_text = text_maker.handle(lxml.html.tostring(root_copy, encoding="unicode")) - if not metadata: - return page_text - - metadata_text = "\n".join(f"- {key}: {value}" for key, value in metadata.items()) - return f"Structured event metadata:\n{metadata_text}\n\n{page_text}" - - -def extract_event_metadata(root: lxml.html.HtmlElement) -> dict[str, str]: - """Extract Schema.org Event microdata, including visually hidden values.""" - event_nodes = root.xpath( - '//*[@itemscope and contains(@itemtype, "schema.org/Event")]' - ) - if not event_nodes: - return {} - - metadata: dict[str, str] = {} - for element in event_nodes[0].xpath(".//*[@itemprop]"): - key = str(element.get("itemprop", "")).strip() - value = str(element.get("content", "")).strip() - if key and value and key not in metadata: - metadata[key] = value - return metadata - - -def apply_event_metadata_dates( - conf: dict[str, typing.Any], root: lxml.html.HtmlElement -) -> None: - """Fill missing generated dates from Schema.org Event microdata.""" - dates = conf.get("dates") - if isinstance(dates, dict) and dates.get("start") is not None: - return - if conf.get("start") is not None: - return - - metadata = extract_event_metadata(root) - start = parse_yaml_date_value(metadata.get("startDate")) - if start is None: - return - end = parse_yaml_date_value(metadata.get("endDate")) or start - conf["dates"] = {"status": "exact", "start": start, "end": end} - - -def parse_osm_url(url: str) -> tuple[float, float] | None: - """Extract latitude/longitude from an OpenStreetMap URL.""" - parsed = urlparse(url) - query = parse_qs(parsed.query) - - mlat = query.get("mlat") - mlon = query.get("mlon") - if mlat and mlon: - return float(mlat[0]), float(mlon[0]) - - if parsed.fragment.startswith("map="): - parts = parsed.fragment.split("/") - if len(parts) >= 3: - return float(parts[-2]), float(parts[-1]) - - return None - - -def extract_google_maps_latlon(url: str) -> tuple[float, float] | None: - """Extract latitude/longitude from a Google Maps URL.""" - for pattern in COORDINATE_PATTERNS: - match = pattern.search(url) - if match: - return float(match.group(1)), float(match.group(2)) - - return None - - -def latlon_from_google_maps_url( - url: str, timeout: int = 10 -) -> tuple[float, float] | None: - """Resolve a Google Maps URL and extract latitude/longitude.""" - response = requests.get( - url, - allow_redirects=True, - timeout=timeout, - headers={"User-Agent": "lookup.py/1.0"}, - ) - response.raise_for_status() - - coordinates = extract_google_maps_latlon(response.url) - if coordinates is not None: - return coordinates - - return extract_google_maps_latlon(response.text) - - -def parse_coordinates_from_url(url: str) -> tuple[float, float] | None: - """Extract latitude/longitude from a supported map URL.""" - lower_url = url.lower() - - if "openstreetmap.org" in lower_url: - return parse_osm_url(url) - - if "google." in lower_url or "maps.app.goo.gl" in lower_url: - coordinates = extract_google_maps_latlon(url) - if coordinates is not None: - return coordinates - - try: - return latlon_from_google_maps_url(url) - except requests.RequestException: - return None - - return None - - -def detect_page_coordinates(root: lxml.html.HtmlElement) -> tuple[float, float] | None: - """Detect venue coordinates from Google Maps or OSM links.""" - for link in root.xpath("//a[@href]"): - href = str(link.get("href", "")).strip() - if not href: - continue - - coordinates = parse_coordinates_from_url(href) - if coordinates is not None: - return coordinates - - return None - - -def parse_date(date_str: str) -> datetime: - """Parse ISO date or datetime into a naive datetime (UTC if tz-aware).""" - try: - dt = datetime.fromisoformat(date_str) - except ValueError: - dt = datetime.fromisoformat(date_str.split("T")[0]) - - if dt.tzinfo is not None: - dt = dt.astimezone(timezone.utc).replace(tzinfo=None) - - return dt - - -def data_dir_from_conferences_path(yaml_path: str) -> str: - """Return personal-data directory from a conferences.yaml path.""" - return os.path.dirname(os.path.abspath(yaml_path)) - - -def url_has_year_component(url: str) -> bool: - """Return True if the URL contains a year or edition path component.""" - parsed = urlparse(url) - components = [part for part in parsed.path.split("/") if part] - if parsed.netloc: - components.extend(part for part in parsed.netloc.split(".") if part) - - for component in components: - if re.fullmatch(r"20\d{2}", component): - return True - if re.search(r"(?:^|[-_/])20\d{2}(?:$|[-_/])", component): - return True - if re.fullmatch(r"\d{1,2}x", component, flags=re.IGNORECASE): - return True - return False - - -def insert_sorted( - conferences: list[dict[str, typing.Any]], new_conf: dict[str, typing.Any] -) -> list[dict[str, typing.Any]]: - """Insert a conference sorted by start date and skip duplicate URLs.""" - new_url = new_conf.get("url") - new_start = conference_sort_datetime(new_conf) - new_year = new_start.year - update_idx = find_inexact_existing_conference(conferences, new_conf) - if update_idx is not None: - existing = conferences.pop(update_idx) - merged = dict(existing) - merged.update(new_conf) - print(f"Updating inexact conference entry: {existing.get('name')}") - return insert_sorted(conferences, merged) - - if new_url: - for conf in conferences: - if conf.get("url") == new_url: - existing_start = conference_sort_datetime(conf) - existing_year = existing_start.year - - if url_has_year_component(new_url): - print(f"⚠️ Conference with URL {new_url} already exists, skipping.") - return conferences - if existing_year == new_year: - print( - f"⚠️ Conference already exists in YAML " - f"(url={new_url}, year={existing_year}), skipping." - ) - return conferences - - for idx, conf in enumerate(conferences): - existing_start = conference_sort_datetime(conf) - if new_start < existing_start: - conferences.insert(idx, new_conf) - return conferences - conferences.append(new_conf) - return conferences - - -def date_ranges_overlap( - first: dict[str, typing.Any], second: dict[str, typing.Any] -) -> bool: - """Return True if two conference date ranges overlap.""" - first_fields = conference_date_fields(first) - second_fields = conference_date_fields(second) - return typing.cast(date, first_fields["start_date"]) <= typing.cast( - date, second_fields["end_date"] - ) and typing.cast(date, second_fields["start_date"]) <= typing.cast( - date, first_fields["end_date"] - ) - - -def same_conference_identity( - existing: dict[str, typing.Any], new_conf: dict[str, typing.Any] -) -> bool: - """Return True if two entries appear to represent the same conference.""" - existing_url = existing.get("url") - new_url = new_conf.get("url") - if existing_url and new_url and existing_url == new_url: - return True - - existing_series = existing.get("series") - new_series = new_conf.get("series") - if existing_series and new_series and existing_series == new_series: - return date_ranges_overlap(existing, new_conf) - - return str(existing.get("name", "")).casefold() == str( - new_conf.get("name", "") - ).casefold() and date_ranges_overlap(existing, new_conf) - - -def find_inexact_existing_conference( - conferences: list[dict[str, typing.Any]], new_conf: dict[str, typing.Any] -) -> int | None: - """Return index of an inexact existing entry that exact new data can update.""" - new_fields = conference_date_fields(new_conf) - if new_fields["date_status"] != "exact": - return None - - for idx, existing in enumerate(conferences): - existing_fields = conference_date_fields(existing) - if existing_fields["date_status"] == "exact": - continue - if same_conference_identity(existing, new_conf): - return idx - - return None - - -def conference_sort_datetime(conf: dict[str, typing.Any]) -> datetime: - """Return conference sort date as a datetime.""" - sort_date = conference_date_fields(conf)["sort_date"] - if isinstance(sort_date, datetime): - return sort_date - return datetime.combine(sort_date, time()) - - -def validate_country(conf: dict[str, typing.Any]) -> None: - """Ensure country is a valid ISO 3166-1 alpha-2 code, normalise if possible.""" - country = conf.get("country") - if not country: - return - - country = country.strip() - if len(country) == 2: - if pycountry.countries.get(alpha_2=country.upper()): - conf["country"] = country.lower() - return - raise ValueError(f"❌ Invalid ISO 3166-1 code '{country}'") - - match = pycountry.countries.get(name=country) - if not match: - try: - match = pycountry.countries.search_fuzzy(country)[0] - except LookupError as exc: - raise ValueError( - f"❌ Country '{country}' not recognised as ISO 3166-1" - ) from exc - - conf["country"] = match.alpha_2.lower() - - -def validate_series( - conf: dict[str, typing.Any], series: dict[str, ConferenceSeries] -) -> None: - """Ensure a generated series ID exists in conference_series.yaml.""" - series_id = conf.get("series") - if series_id is None: - return - if not isinstance(series_id, str) or series_id not in series: - raise ValueError( - f"Generated conference uses unknown series {series_id!r}. " - "Add it to conference_series.yaml first or remove the series field." - ) - - -def normalize_free_event_fields(conf: dict[str, typing.Any]) -> None: - """Remove redundant pricing fields from free conferences.""" - if conf.get("free") is True: - conf.pop("price", None) - conf.pop("currency", None) - - -def parse_yaml_datetime(value: typing.Any) -> datetime | None: - """Convert YAML date/datetime values to a datetime.""" - if isinstance(value, datetime): - return value - - if isinstance(value, date): - return datetime.combine(value, time()) - - if isinstance(value, str): - try: - return datetime.fromisoformat(value) - except ValueError: - return datetime.combine(date.fromisoformat(value.split("T")[0]), time()) - - return None - - -def parse_yaml_date_value(value: typing.Any) -> date | datetime | None: - """Convert YAML date/datetime strings to date-like values.""" - if isinstance(value, datetime): - return value - - if isinstance(value, date): - return value - - if not isinstance(value, str): - return None - - try: - if " " in value or "T" in value: - return datetime.fromisoformat(value) - return date.fromisoformat(value) - except ValueError: - return None - - -def normalize_date_values(conf: dict[str, typing.Any]) -> None: - """Normalize quoted ISO date/datetime values produced by the LLM.""" - dates = conf.get("dates") - if isinstance(dates, dict): - for field in ("start", "end", "earliest", "latest"): - if field in dates: - parsed = parse_yaml_date_value(dates[field]) - if parsed is not None: - dates[field] = parsed - - for field in ("start", "end"): - if field in conf: - parsed = parse_yaml_date_value(conf[field]) - if parsed is not None: - conf[field] = parsed - - -def same_type_as_start( - start_value: typing.Any, - new_dt: datetime, - keep_timezone: bool = True, - prefer_datetime: bool = False, -) -> typing.Any: - """Return end value shaped like the start value when possible.""" - if isinstance(start_value, datetime): - if keep_timezone: - return new_dt - return new_dt.replace(tzinfo=None) - - if isinstance(start_value, date): - if prefer_datetime: - return new_dt - return new_dt.date() - - if isinstance(start_value, str): - if prefer_datetime or " " in start_value or "T" in start_value: - return new_dt.isoformat(sep=" ") - return new_dt.date().isoformat() - - return new_dt - - -def normalize_dates_field(conf: dict[str, typing.Any]) -> None: - """Move legacy top-level date fields into the nested dates mapping.""" - normalize_date_values(conf) - raw_dates = conf.get("dates") - dates = raw_dates if isinstance(raw_dates, dict) else None - - if dates is None and ("start" in conf or "end" in conf): - start = conf.pop("start", None) - end = conf.pop("end", start) - status = str(conf.pop("date_status", "exact")) - conf["dates"] = {"status": status, "start": start, "end": end} - return - - if dates is not None: - if "start" in conf and "start" not in dates: - dates["start"] = conf["start"] - if "end" in conf and "end" not in dates: - dates["end"] = conf["end"] - if "date_status" in conf and "status" not in dates: - dates["status"] = conf["date_status"] - - conf.pop("start", None) - conf.pop("end", None) - conf.pop("date_status", None) - normalize_date_values(conf) - - -def validate_generated_conference(conf: dict[str, typing.Any]) -> None: - """Validate generated conference YAML before inserting it.""" - try: - conference_date_fields(conf) - except ValueError as exc: - generated_yaml = yaml.dump(conf, sort_keys=False, allow_unicode=True).strip() - raise ValueError( - "Generated conference YAML is missing valid date information. " - "Expected nested `dates:` with exact/tentative start/end or " - f"approximate earliest/latest.\n\nGenerated YAML:\n{generated_yaml}" - ) from exc - - -def maybe_extract_explicit_end_time(source_text: str) -> int | None: - """Extract an explicit 12-hour clock end time for Geomob-style pages.""" - lowered = source_text.lower() - - if "10pm" in lowered or "10 pm" in lowered or "22:00" in lowered: - return 22 - - if "11pm" in lowered or "11 pm" in lowered or "23:00" in lowered: - return 23 - - return None - - -def normalise_end_field(new_conf: dict[str, typing.Any], source_text: str) -> None: - """Ensure an end value exists, with a Geomob-specific fallback.""" - dates = new_conf.get("dates") - nested_dates = dates if isinstance(dates, dict) else None - start_value = ( - nested_dates.get("start") if nested_dates is not None else new_conf.get("start") - ) - if start_value is None: - return - - start_dt = parse_yaml_datetime(start_value) - if start_dt is None: - return - - name = str(new_conf.get("name", "")) - url = str(new_conf.get("url", "")) - is_geomob = "geomob" in name.lower() or "thegeomob.com" in url.lower() - - if is_geomob: - end_hour = maybe_extract_explicit_end_time(source_text) - if end_hour is None: - end_hour = 22 - - geomob_end = start_dt.replace(hour=end_hour, minute=0, second=0, microsecond=0) - end_value = same_type_as_start(start_value, geomob_end, prefer_datetime=True) - if nested_dates is not None: - nested_dates["end"] = end_value - else: - new_conf["end"] = end_value - return - - if nested_dates is not None: - if "end" not in nested_dates: - nested_dates["end"] = same_type_as_start(start_value, start_dt) - return - - if "end" not in new_conf: - new_conf["end"] = same_type_as_start(start_value, start_dt) - - -def load_conferences(yaml_path: str) -> list[dict[str, typing.Any]]: - """Load conference YAML.""" - with open(yaml_path) as file: - loaded = yaml.safe_load(file) - assert isinstance(loaded, list) - return typing.cast(list[dict[str, typing.Any]], loaded) - - -def load_conference_series_for_path(yaml_path: str) -> dict[str, ConferenceSeries]: - """Load conference series next to the target conferences YAML file.""" - return load_series(data_dir_from_conferences_path(yaml_path)) - - -def dump_conferences(yaml_path: str, conferences: list[dict[str, typing.Any]]) -> None: - """Write conference YAML.""" - with open(yaml_path, "w") as file: - text = yaml.dump(conferences, sort_keys=False, allow_unicode=True) - text = text.replace("\n- name:", "\n\n- name:") - file.write(text.lstrip()) - - -def add_new_conference(url: str, yaml_path: str) -> bool: - """Fetch, generate and insert a conference into the YAML file.""" - conferences = load_conferences(yaml_path) - - if url_has_year_component(url): - for conf in conferences: - if conf.get("url") == url: - fields = conference_date_fields(conf) - if fields["date_status"] != "exact": - continue - print( - "⚠️ Conference already exists in YAML " - + f"(url={url}), skipping before API call." - ) - return False - - soup = fetch_webpage(url) - source_text = webpage_to_text(soup) - detected_coordinates = detect_page_coordinates(soup) - series = load_conference_series_for_path(yaml_path) - prompt = build_prompt(url, source_text, detected_coordinates, series) - new_yaml_text = get_from_open_ai(prompt)["yaml"] - - new_conf = yaml.safe_load(new_yaml_text) - if isinstance(new_conf, list): - new_conf = new_conf[0] - assert isinstance(new_conf, dict) - - validate_country(new_conf) - validate_series(new_conf, series) - normalize_free_event_fields(new_conf) - apply_event_metadata_dates(new_conf, soup) - normalize_dates_field(new_conf) - normalise_end_field(new_conf, source_text) - normalize_dates_field(new_conf) - validate_generated_conference(new_conf) - - if detected_coordinates is not None: - new_conf["latitude"] = detected_coordinates[0] - new_conf["longitude"] = detected_coordinates[1] - - updated = insert_sorted(conferences, new_conf) - dump_conferences(yaml_path, updated) - return True - - -def main(argv: list[str] | None = None) -> int: - """CLI entrypoint.""" - args = argv if argv is not None else sys.argv[1:] - if not args: - raise SystemExit("Usage: add-new-conference URL") - - yaml_path = os.path.expanduser("~/src/personal-data/conferences.yaml") - add_new_conference(args[0], yaml_path) - return 0 diff --git a/agenda/airbnb.py b/agenda/airbnb.py deleted file mode 100644 index dfbf3c2..0000000 --- a/agenda/airbnb.py +++ /dev/null @@ -1,190 +0,0 @@ -"""Library for parsing Airbnb booking HTML files.""" - -import json -import re -import typing -from datetime import datetime -from typing import Any -from zoneinfo import ZoneInfo - -import lxml.html -import pycountry - -StrDict = dict[str, typing.Any] - - -def build_datetime(date_str: str, time_str: str, tz_name: str) -> datetime: - """ - Combine an ISO date string, HH:MM time string, and a timezone name - into a timezone-aware datetime in the specified timezone. - """ - dt_str = f"{date_str}T{time_str}" - naive_dt = datetime.fromisoformat(dt_str) - return naive_dt.replace(tzinfo=ZoneInfo(tz_name)) - - -def list_to_dict(items: list[typing.Any]) -> dict[str, typing.Any]: - """Convert a flat list to a dict, assuming alternating keys and values.""" - return {items[i]: items[i + 1] for i in range(0, len(items), 2)} - - -def extract_country_code(address: str) -> str | None: - """Return ISO 3166-1 alpha-2 country code from a free-text address.""" - address_lower = address.lower() - for country in pycountry.countries: - if country.name.lower() in address_lower: - return str(country.alpha_2.lower()) - if ( - hasattr(country, "official_name") - and country.official_name.lower() in address_lower - ): - return str(country.alpha_2.lower()) - return None - - -def get_json_blob(tree: Any) -> str: - data_id = "data-injector-instances" - js_string = tree.xpath(f'//*[@id="{data_id}"]/text()')[0] - return str(js_string) - - -def get_ui_state(tree: Any) -> StrDict: - data_id = "data-injector-instances" - js_string = tree.xpath(f'//*[@id="{data_id}"]/text()')[0] - big_blob = json.loads(str(js_string)) - ui_state = walk_tree(big_blob, "uiState") - return list_to_dict(ui_state[0]) - - -def get_reservation_data(ui_state: StrDict) -> StrDict: - return { - row["id"]: row for row in ui_state["reservation"]["scheduled_event"]["rows"] - } - - -def get_room_url(tree: Any) -> str | None: - for e in tree.xpath('//a[@data-testid="reservation-destination-link"]'): - href = e.get("href") - assert isinstance(href, str) - if not href.startswith("/room"): - continue - return "https://www.airbnb.co.uk" + href - return None - - -def get_price_from_reservation(reservation: StrDict) -> str: - price = reservation["payment_summary"]["subtitle"] - assert isinstance(price, str) - tc = "Total cost: " - if price.startswith(tc): - price = price[len(tc) :] - assert price[0] == "£" - return price[1:] - - -def extract_booking_from_html(html_file: str) -> StrDict: - """Extract booking information from Airbnb HTML file.""" - - with open(html_file, "r", encoding="utf-8") as f: - text_content = f.read() - - confirmation_match = re.search( - r"/trips/v1/reservation-details/ro/RESERVATION2_CHECKIN/([A-Z0-9]+)", - text_content, - ) - if confirmation_match is None: - raise ValueError("Could not find confirmation code in HTML") - confirmation_code = confirmation_match.group(1) - - tree = lxml.html.parse(html_file) - root = tree.getroot() - try: - ui_state = get_ui_state(tree) - except Exception: - print(html_file) - raise - - reservation = get_reservation_data(ui_state) - m_guests = re.match(r"^(\d+) guests?$", reservation["guests"]["subtitle"]) - if m_guests is None: - raise ValueError("Could not parse number of guests") - number_of_adults = int(m_guests.group(1)) - - price = get_price_from_reservation(reservation) - metadata = ui_state["reservation"]["metadata"] - country_code = metadata["country"].lower() - - title = reservation["dynamic_marquee_title_image_v3"]["title"] - location = title.rpartition(" in ")[2] - - checkin_checkout = reservation["checkin_checkout_arrival_guide"] - check_in_time = checkin_checkout["leading_subtitle"] - check_out_time = checkin_checkout["trailing_subtitle"] - - check_in = build_datetime( - metadata["check_in_date"], check_in_time, metadata["timezone"] - ) - - check_out = build_datetime( - metadata["check_out_date"], check_out_time, metadata["timezone"] - ) - - address = reservation["map"]["address"] if "map" in reservation else None - - if "header_action.pdp" in reservation: - name = reservation["header_action.pdp"]["subtitle"] - else: - name = root.findtext(".//h1") - - booking = { - "type": "apartment", - "operator": "airbnb", - "name": name, - "location": location, - "booking_reference": confirmation_code, - "booking_url": f"https://www.airbnb.co.uk/trips/v1/reservation-details/ro/RESERVATION2_CHECKIN/{confirmation_code}", - "country": country_code, - "latitude": metadata["lat"], - "longitude": metadata["lng"], - "timezone": metadata["timezone"], - "from": check_in, - "to": check_out, - "price": price, - "currency": "GBP", - "number_of_adults": number_of_adults, - } - if address: - booking["address"] = address - - room_url = get_room_url(tree) - if room_url is not None: - booking["url"] = room_url - - return booking - - -def walk_tree(data: Any, want_key: str) -> Any: - """Recursively search for a dict containing 'reservation' and return its value.""" - if isinstance(data, dict): - if want_key in data: - return data[want_key] - for key, value in data.items(): - result = walk_tree(value, want_key) - if result is not None: - return result - elif isinstance(data, list): - for item in data: - result = walk_tree(item, want_key) - if result is not None: - return result - return None - - -def parse_multiple_files(filenames: list[str]) -> list[StrDict]: - """Parse multiple Airbnb HTML files and return a list of booking dictionaries.""" - bookings = [] - for html_file in sorted(filenames): - booking = extract_booking_from_html(html_file) - assert booking - bookings.append(booking) - return bookings diff --git a/agenda/airport_index.json b/agenda/airport_index.json deleted file mode 100644 index 9ea81d2..0000000 --- a/agenda/airport_index.json +++ /dev/null @@ -1,9053 +0,0 @@ -{ - "AAA": ["Anaa Airport", "Anaa", "PF", true], - "AAB": ["Arrabury Airport", "Tanbar", "AU", false], - "AAC": ["El Arish International Airport", "El Arish", "EG", true], - "AAD": ["Adado Airport", "Adado", "SO", false], - "AAE": ["Annaba Rabah Bitat Airport", "Annaba", "DZ", true], - "AAF": ["Apalachicola Regional Airport", "Apalachicola", "US", false], - "AAH": ["Aachen-Merzbrück Airport", "Aachen", "DE", false], - "AAI": ["Arraias Airport", "Arraias", "BR", false], - "AAJ": ["Cayana Airstrip", "Awaradam", "SR", false], - "AAK": ["Aranuka Airport", "Buariki", "KI", true], - "AAL": ["Aalborg Airport", "Aalborg", "DK", true], - "AAM": ["Malamala Airport", "Malamala", "ZA", false], - "AAN": ["Al Ain International Airport", "Al Ain", "AE", true], - "AAO": ["Anaco Airport", "Anaco", "VE", false], - "AAP": ["Aji Pangeran Tumenggung Pranoto International Airport", "Samarinda", "ID", true], - "AAQ": ["Anapa Vityazevo Airport", "Krasnyi Kurgan", "RU", false], - "AAR": ["Aarhus Airport", "Aarhus", "DK", true], - "AAS": ["Apalapsili Airport", "Apalapsili", "ID", false], - "AAT": ["Altay Xuedu Airport", "Altay", "CN", true], - "AAU": ["Asau Airport", "Asau", "WS", false], - "AAV": ["Allah Valley Airport", "Surallah", "PH", false], - "AAX": ["Romeu Zema Airport", "Araxá", "BR", true], - "AAY": ["Al Ghaydah International Airport", "Al Ghaydah", "YE", true], - "AAZ": ["Quezaltenango Airport", "Quezaltenango", "GT", true], - "ABA": ["Abakan International Airport", "Abakan", "RU", true], - "ABB": ["Asaba International Airport", "Asaba", "NG", true], - "ABC": ["Albacete Airport / Los Llanos Air Base", "Albacete", "ES", false], - "ABD": ["Abadan Ayatollah Jami International Airport", "Abadan", "IR", true], - "ABE": ["Lehigh Valley International Airport", "Allentown/Bethlehem", "US", true], - "ABF": ["Abaiang Airport", "Abaiang", "KI", false], - "ABG": ["Abingdon Downs Airport", "Abingdon Downs", "AU", false], - "ABH": ["Alpha Airport", "Alpha", "AU", false], - "ABI": ["Abilene Regional Airport", "Abilene", "US", true], - "ABJ": ["Félix-Houphouët-Boigny International Airport", "Abidjan", "CI", true], - "ABK": ["Kebri Dahar Airport", "Kebri Dahar", "ET", true], - "ABL": ["Ambler Airport", "Ambler", "US", true], - "ABM": ["Northern Peninsula Airport", "Bamaga", "AU", true], - "ABN": ["Albina Airport", "Albina", "SR", false], - "ABO": ["Aboisso Airport", "Aboisso", "CI", false], - "ABP": ["Atkamba Airport", "Atkamba Mission", "PG", false], - "ABQ": ["Albuquerque International Sunport", "Albuquerque", "US", true], - "ABR": ["Aberdeen Regional Airport", "Aberdeen", "US", true], - "ABS": ["Abu Simbel Airport", "Abu Simbel", "EG", true], - "ABT": ["King Saud Bin Abdulaziz (Al Baha) Airport", "Al-Baha", "SA", true], - "ABU": ["AA Bere Tallo (Haliwen) Airport", "Atambua", "ID", true], - "ABV": ["Nnamdi Azikiwe International Airport", "Abuja", "NG", true], - "ABX": ["Albury Airport", "East Albury", "AU", true], - "ABY": ["Southwest Georgia Regional Airport", "Albany", "US", true], - "ABZ": ["Aberdeen International Airport", "Aberdeen", "GB", true], - "ACA": ["General Juan N. Álvarez International Airport", "Acapulco", "MX", true], - "ACB": ["Antrim County Airport", "Bellaire", "US", false], - "ACC": ["Kotoka International Airport", "Accra", "GH", true], - "ACD": ["Alcides Fernández Airport", "Acandí", "CO", false], - "ACE": ["César Manrique-Lanzarote Airport", "San Bartolomé", "ES", true], - "ACF": ["Aral Tarim Airport", "Aral", "CN", true], - "ACH": ["Sankt Gallen Altenrhein Airport", "St. Gallen", "CH", true], - "ACI": ["Alderney Airport", "Saint Anne", "GG", true], - "ACJ": ["Anuradhapura Airport", "Anuradhapura", "LK", false], - "ACK": ["Nantucket Memorial Airport", "Nantucket", "US", true], - "ACM": ["Arica Airport", "Arica", "CO", false], - "ACN": ["Ciudad Acuña International Airport", "Ciudad Acuña", "MX", false], - "ACO": ["Cóbano Airport", "Cóbano", "CR", false], - "ACP": ["Sahand Airport", "Maragheh", "IR", false], - "ACR": ["Araracuara Airport", "Araracuara", "CO", false], - "ACS": ["Achinsk Airport", "Achinsk", "RU", false], - "ACT": ["Waco Regional Airport", "Waco", "US", true], - "ACU": ["Achutupu Airport", "Mamitupu", "PA", false], - "ACV": ["California Redwood Coast-Humboldt County Airport", "Arcata/Eureka", "US", true], - "ACX": ["Xingyi Wanfenglin Airport", "Xingyi", "CN", true], - "ACY": ["Atlantic City International Airport", "Atlantic City", "US", true], - "ACZ": ["Zabol Airport", "Zabol", "IR", false], - "ADA": ["Adana Şakirpaşa Airport", "Seyhan", "TR", false], - "ADB": ["Adnan Menderes International Airport", "Gaziemir", "TR", true], - "ADC": ["Andakombe Airport", "Andekombe", "PG", false], - "ADD": ["Addis Ababa Bole International Airport", "Addis Ababa", "ET", true], - "ADE": ["Aden International Airport", "Aden", "YE", true], - "ADF": ["Adıyaman Airport", "Adıyaman", "TR", true], - "ADG": ["Lenawee County Airport", "Adrian", "US", false], - "ADH": ["Aldan Airport", "Aldan", "RU", false], - "ADI": ["Arandis Airport", "Arandis", "NA", false], - "ADJ": ["Marka International (Amman Civil) Airport", "Amman", "JO", true], - "ADK": ["Adak Airport", "Adak", "US", true], - "ADL": ["Adelaide International Airport", "Adelaide", "AU", true], - "ADM": ["Ardmore Municipal Airport", "Ardmore", "US", false], - "ADO": ["Andamooka Airport", "Andamooka", "AU", false], - "ADP": ["Ampara Airport", "Ampara", "LK", false], - "ADQ": ["Kodiak Airport", "Kodiak", "US", true], - "ADR": ["Robert F Swinnie Airport", "Andrews", "US", false], - "ADS": ["Addison Airport", "Dallas", "US", false], - "ADT": ["Ada Regional Airport", "Ada", "US", false], - "ADU": ["Ardabil Airport", "Ardabil", "IR", true], - "ADV": ["El Daein Airport", "El Daein", "SD", false], - "ADW": ["Joint Base Andrews", "Camp Springs", "US", false], - "ADX": ["Leuchars Station Airfield", "Leuchars, Fife", "GB", false], - "ADY": ["Alldays Airport", "Alldays", "ZA", false], - "ADZ": ["Gustavo Rojas Pinilla International Airport", "San Andrés", "CO", true], - "AEA": ["Abemama Airport", "Abemama", "KI", false], - "AEB": ["Baise (Bose) Bama Airport", "Baise (Tianyang)", "CN", true], - "AEE": ["Adareil Airport", "Adar", "SS", false], - "AEG": ["Aek Godang Airport", "Padang Sidempuan", "ID", false], - "AEH": ["Abeche Airport", "Abeche", "TD", false], - "AEK": ["Aseki Airport", "Aseki", "PG", false], - "AEL": ["Albert Lea Municipal Airport", "Albert Lea", "US", false], - "AEM": ["Amgu Airport", "Amgu", "RU", false], - "AEO": ["Aioun el Atrouss Airport", "Aioun El Atrouss", "MR", false], - "AEP": ["Aeroparque Jorge Newbery", "Buenos Aires", "AR", true], - "AEQ": ["Ar Horqin Airport", "Chifeng", "CN", false], - "AER": ["Sochi International Airport", "Sochi", "RU", true], - "AES": ["Ålesund Airport", "Ålesund", "NO", true], - "AET": ["Allakaket Airport", "Allakaket", "US", true], - "AEU": ["Abu Musa Island Airport", "Abu Musa", "IR", true], - "AEX": ["Alexandria International Airport", "Alexandria", "US", true], - "AEY": ["Akureyri International Airport", "Akureyri", "IS", true], - "AFA": ["Suboficial Ay Santiago Germano Airport", "San Rafael", "AR", true], - "AFD": ["Port Alfred Airport", "Port Alfred", "ZA", false], - "AFF": ["USAF Academy Airfield", "Colorado Springs", "US", false], - "AFI": ["Amalfi Airport", "Amalfi", "CO", false], - "AFK": ["Kondavattavana Tank Seaplane Base", "Ampara", "LK", false], - "AFL": ["Piloto Osvaldo Marques Dias Airport", "Alta Floresta", "BR", true], - "AFN": ["Jaffrey Airfield Silver Ranch Airport", "Jaffrey", "US", false], - "AFO": ["Afton Municipal Airport", "Afton", "US", false], - "AFR": ["Afore Airstrip", "Afore", "PG", false], - "AFS": ["Sugraly Airport", "Zarafshan", "UZ", false], - "AFT": ["Afutara Aerodrome", "Bila", "SB", false], - "AFW": ["Perot Field/Fort Worth Alliance Airport", "Fort Worth", "US", false], - "AFY": ["Afyon Air Base", "Afyonkarahisar", "TR", false], - "AFZ": ["Sabzevar National Airport", "Sabzevar", "IR", true], - "AGA": ["Al Massira Airport", "Agadir (Temsia)", "MA", true], - "AGB": ["Augsburg Airport", "Augsburg", "DE", false], - "AGC": ["Allegheny County Airport", "Pittsburgh", "US", false], - "AGD": ["Anggi Airport", "Anggi-Papua Island", "ID", false], - "AGE": ["Wangerooge Airport", "Wangerooge", "DE", true], - "AGF": ["Agen La Garenne airport", "Agen", "FR", false], - "AGG": ["Angoram Airport", "Angoram", "PG", false], - "AGH": ["Ängelholm-Helsingborg Airport", "Ängelholm", "SE", true], - "AGI": ["Wageningen Airstrip", "Wageningen", "SR", true], - "AGJ": ["Aguni Airport", "Aguni", "JP", true], - "AGK": ["Kagua Airport", "Kagua", "PG", false], - "AGL": ["Wanigela Airport", "Wanigela", "PG", false], - "AGM": ["Tasiilaq Heliport", "Tasiilaq", "GL", true], - "AGN": ["Angoon Seaplane Base", "Angoon", "US", true], - "AGO": ["Magnolia Municipal Airport / Ralph C Weiser Field", "Magnolia", "US", false], - "AGP": ["Málaga-Costa del Sol Airport", "Málaga", "ES", true], - "AGR": ["Agra Airport / Agra Air Force Station", "Agra", "IN", true], - "AGS": ["Augusta Regional At Bush Field", "Augusta", "US", true], - "AGT": ["Guaraní International Airport", "Ciudad del Este", "PY", true], - "AGU": ["Aguascalientes International Airport", "Aguascalientes", "MX", true], - "AGV": ["Oswaldo Guevara Mujica Airport", "Acarigua", "VE", false], - "AGW": ["Agnew Airport", "Agnew", "AU", false], - "AGX": ["Agatti Airport", "Agatti", "IN", true], - "AGZ": ["Aggeneys Airport", "Aggeneys", "ZA", false], - "AHA": ["Maa Mahamaya Airport", "Ambikapur", "IN", true], - "AHB": ["Abha International Airport", "Abha", "SA", true], - "AHC": ["Amedee Army Air Field", "Herlong", "US", false], - "AHD": ["Ardmore Downtown Executive Airport", "Ardmore", "US", false], - "AHE": ["Ahe Airport", "Ahe Atoll", "PF", true], - "AHF": ["Arapahoe Municipal Airport", "Arapahoe", "US", false], - "AHG": ["Agalega Island Airstrip", "Vingt Cinq", "MU", false], - "AHH": ["Amery Municipal Airport", "Amery", "US", false], - "AHI": ["Amahai Airport", "Amahai", "ID", false], - "AHJ": ["Hongyuan Airport", "Ngawa (Hongyuan)", "CN", false], - "AHL": ["Aishalton Airport", "Aishalton", "GY", false], - "AHM": ["Ashland Municipal Airport Sumner Parker Field", "Ashland", "US", false], - "AHN": ["Athens Ben Epps Airport", "Athens", "US", false], - "AHO": ["Alghero-Fertilia Airport", "Alghero", "IT", true], - "AHS": ["Ahuas Airport", "Ahuas", "HN", false], - "AHU": ["Cherif Al Idrissi Airport", "Al Hoceima", "MA", true], - "AHY": ["Ambatolhy Airport", "Ambatolahy", "MG", false], - "AHZ": ["L'Alpe d'Huez - Henri Giraud Altiport", "Huez, Isère", "FR", false], - "AIA": ["Alliance Municipal Airport", "Alliance", "US", true], - "AIC": ["Ailinglaplap Airok Airport", "Bigatyelang Island", "MH", false], - "AID": ["Anderson Municipal Darlington Field", "Anderson", "US", false], - "AIE": ["Aiome Airport", "Aiome", "PG", false], - "AIF": ["Marcelo Pires Halzhausen Airport", "Assis", "BR", false], - "AIG": ["Yalinga Airport", "Yalinga", "CF", false], - "AIH": ["Aiambak Airport", "Aiambak", "PG", false], - "AII": ["Ali-Sabieh Airport", "Ali-Sabieh", "DJ", false], - "AIK": ["Aiken Regional Airport", "Aiken", "US", false], - "AIL": ["Ailigandí Airport", "Isla Lorenzo Bello", "PA", false], - "AIM": ["Ailuk Airport", "Ailuk Island", "MH", false], - "AIN": ["Wainwright Airport", "Wainwright", "US", true], - "AIO": ["Atlantic Municipal Airport", "Atlantic", "US", false], - "AIP": ["Adampur Airport", "Adampur", "IN", true], - "AIR": ["Aripuanã Airport", "Aripuanã", "BR", false], - "AIS": ["Arorae Island Airport", "Arorae Island", "KI", false], - "AIT": ["Aitutaki Airport", "Aitutaki", "CK", true], - "AIU": ["Enua Airport", "Atiu Island", "CK", true], - "AIV": ["George Downer Airport", "Aliceville", "US", false], - "AIW": ["Ai-Ais Airport", "Ai-Ais", "NA", false], - "AIY": ["Araçuaí Airport", "Araçuaí", "BR", false], - "AIZ": ["Lee C Fine Memorial Airport", "Kaiser Lake Ozark", "US", false], - "AJA": ["Ajaccio Napoléon Bonaparte airport", "Ajaccio", "FR", true], - "AJF": ["Al-Jawf International Airport", "Al-Jawf", "SA", true], - "AJI": ["Ağrı Airport", "Ağrı", "TR", true], - "AJJ": ["Akjoujt Airport", "Akjoujt", "MR", false], - "AJK": ["Arak Airport", "Araak", "IR", false], - "AJL": ["Lengpui Airport", "Aizawl (Lengpui)", "IN", true], - "AJN": ["Ouani Airport", "Ouani", "KM", true], - "AJR": ["Arvidsjaur Airport", "Arvidsjaur", "SE", true], - "AJS": ["Punta Abreojos Airport", "Mulegé", "MX", false], - "AJU": ["Aracaju - Santa Maria Airport", "Aracaju", "BR", true], - "AJY": ["Mano Dayak International Airport", "Agadez", "NE", false], - "AKA": ["Ankang Fuqiang Airport", "Ankang (Hanbin)", "CN", true], - "AKB": ["Atka Airport", "Atka", "US", true], - "AKC": ["Akron Fulton International Airport", "Akron", "US", false], - "AKD": ["Akola Airport", "Akola", "IN", false], - "AKE": ["Akieni Airport", "Akieni", "GA", false], - "AKF": ["Kufra Airport", "Kufra", "LY", true], - "AKG": ["Anguganak Airport", "Anguganak", "PG", false], - "AKH": ["Prince Sultan Air Base", "Al Kharj", "SA", false], - "AKI": ["Akiak Airport", "Akiak", "US", true], - "AKJ": ["Asahikawa Airport", "Higashikagura", "JP", true], - "AKK": ["Akhiok Airport", "Akhiok", "US", true], - "AKL": ["Auckland International Airport", "Auckland", "NZ", true], - "AKM": ["Zakuoma Airport", "ZaKouma", "TD", false], - "AKN": ["King Salmon Airport", "King Salmon", "US", true], - "AKO": ["Colorado Plains Regional Airport", "Akron", "US", false], - "AKP": ["Anaktuvuk Pass Airport", "Anaktuvuk Pass", "US", true], - "AKQ": ["Astra Ksetra Airport", "Menggala", "ID", false], - "AKR": ["Akure Airport", "Akure", "NG", true], - "AKS": ["Gwaunaru'u Airport", "Auki", "SB", true], - "AKT": ["RAF Akrotiri", "Akrotiri", "CY", false], - "AKU": ["Aksu Hongqipo Airport", "Aksu (Onsu)", "CN", true], - "AKV": ["Akulivik Airport", "Akulivik", "CA", true], - "AKW": ["Aghajari Airport", "Omidiyeh", "IR", false], - "AKX": ["Aktobe International Airport", "Aktobe", "KZ", true], - "AKY": ["Sittwe Airport", "Sittwe", "MM", true], - "ALA": ["Almaty International Airport", "Almaty", "KZ", true], - "ALB": ["Albany International Airport", "Albany", "US", true], - "ALC": ["Alicante-Elche Miguel Hernández Airport", "Alicante", "ES", true], - "ALD": ["Alerta Airport", "Fortaleza", "PE", false], - "ALE": ["Alpine Casparis Municipal Airport", "Alpine", "US", false], - "ALF": ["Alta Airport", "Alta", "NO", true], - "ALG": ["Houari Boumediene Airport", "Algiers", "DZ", true], - "ALH": ["Albany Airport", "Albany", "AU", true], - "ALI": ["Alice International Airport", "Alice", "US", false], - "ALJ": ["Alexander Bay Airport", "Alexander Bay", "ZA", false], - "ALM": ["Alamogordo White Sands Regional Airport", "Alamogordo", "US", false], - "ALN": ["St Louis Regional Airport", "Alton/St Louis", "US", false], - "ALO": ["Waterloo Regional Airport", "Waterloo", "US", true], - "ALP": ["Aleppo International Airport", "Aleppo", "SY", true], - "ALQ": ["Alegrete Novo Airport", "Alegrete", "BR", false], - "ALR": ["Alexandra Aerodrome", "Alexandra", "NZ", false], - "ALS": ["San Luis Valley Regional Airport/Bergman Field", "Alamosa", "US", true], - "ALT": ["Alenquer Municipal Airport", "Alenquer", "BR", false], - "ALU": ["Alula Airport", "Alula", "SO", false], - "ALV": ["Andorra la Vella Heliport", "Escaldes-Engordany", "AD", false], - "ALW": ["Walla Walla Regional Airport", "Walla Walla", "US", true], - "ALX": ["Thomas C Russell Field", "Alexander City", "US", false], - "ALZ": ["Alitak Seaplane Base", "Lazy Bay", "US", true], - "AMA": ["Rick Husband Amarillo International Airport", "Amarillo", "US", true], - "AMB": ["Ambilobe Airport", "", "MG", false], - "AMC": ["Am Timan Airport", "Am Timan", "TD", false], - "AMD": ["Sardar Vallabh Patel International Airport", "Ahmedabad", "IN", true], - "AME": ["Alto Molocue Airport", "Alto Molocue", "MZ", false], - "AMF": ["Ama Airport", "", "PG", false], - "AMH": ["Arba Minch Airport", "Arba Minch", "ET", true], - "AMJ": ["Cirilo Queiróz Airport", "Almenara", "BR", false], - "AMK": ["Animas Air Park", "Durango", "US", false], - "AML": ["Puerto Armuelles Airport", "Puerto Armuelles", "PA", false], - "AMM": ["Queen Alia International Airport", "Amman", "JO", true], - "AMN": ["Gratiot Community Airport", "Alma", "US", false], - "AMO": ["Mao Airport", "Mao", "TD", false], - "AMP": ["Ampanihy Airport", "Ampanihy", "MG", false], - "AMQ": ["Pattimura International Airport", "Ambon", "ID", true], - "AMS": ["Amsterdam Airport Schiphol", "Amsterdam", "NL", true], - "AMT": ["Amata Airport", "", "AU", false], - "AMU": ["Amanab Airport", "Amanab", "PG", false], - "AMV": ["Amderma Airport", "Amderma", "RU", true], - "AMW": ["Ames Municipal Airport", "Ames", "US", false], - "AMX": ["Ammaroo Airport", "", "AU", false], - "AMY": ["Ambatomainty Airport", "", "MG", false], - "AMZ": ["Ardmore Airport", "Manurewa", "NZ", false], - "ANA": ["Angama Mara Airport", "Lolgorien", "KE", false], - "ANB": ["Anniston Regional Airport", "Anniston", "US", false], - "ANC": ["Ted Stevens Anchorage International Airport", "Anchorage", "US", true], - "AND": ["Anderson Regional Airport", "Anderson", "US", false], - "ANE": ["Angers Marcé airport", "Angers", "FR", false], - "ANF": ["Andrés Sabella Gálvez International Airport", "Antofagasta", "CL", true], - "ANG": ["Angoulême Brie-Champniers airport", "Angoulême", "FR", false], - "ANI": ["Aniak Airport", "Aniak", "US", true], - "ANJ": ["Zanaga Airport", "Zanaga", "CG", false], - "ANK": ["Etimesgut Air Base", "Ankara", "TR", false], - "ANL": ["Andulo Airport", "Andulo", "AO", false], - "ANM": ["Antsirabe Airport", "Antsirabe", "MG", false], - "ANN": ["Annette Island Airport", "Metlakatla", "US", false], - "ANO": ["Angoche Airport", "Angoche", "MZ", false], - "ANP": ["Lee Airport", "Annapolis", "US", false], - "ANQ": ["Tri State Steuben County Airport", "Angola", "US", false], - "ANR": ["Antwerp International Airport (Deurne)", "Antwerp", "BE", true], - "ANS": ["Andahuaylas Airport", "Andahuaylas", "PE", true], - "ANT": ["Sankt Anton am Arlberg Heliport", "Sankt Anton am Arlberg", "AT", false], - "ANU": ["V. C. Bird International Airport", "Osbourn", "AG", true], - "ANV": ["Anvik Airport", "Anvik", "US", true], - "ANW": ["Ainsworth Regional Airport", "Ainsworth", "US", false], - "ANX": ["Andøya Airport, Andenes", "Andenes", "NO", true], - "ANY": ["Anthony Municipal Airport", "Anthony", "US", false], - "ANZ": ["Angus Downs Airport", "Angus Downs Station", "AU", false], - "AOB": ["Annanberg Airport", "", "PG", false], - "AOC": ["Leipzig–Altenburg Airport", "Nobitz", "DE", false], - "AOD": ["Abou-Deïa Airport", "Abou-Deïa", "TD", false], - "AOE": ["Hasan Polatkan Airport", "Eskişehir", "TR", true], - "AOG": ["Anshan Teng'ao Airport / Anshan Air Base", "Anshan", "CN", true], - "AOH": ["Lima Allen County Airport", "Lima", "US", false], - "AOI": ["Marche Airport", "Falconara Marittima (AN)", "IT", true], - "AOJ": ["Aomori Airport", "Aomori", "JP", true], - "AOK": ["Karpathos Airport", "Karpathos Island", "GR", true], - "AOL": ["Paso De Los Libres Airport", "Paso de los Libres", "AR", false], - "AOM": ["Adam Airport", "Adam", "OM", false], - "AOO": ["Altoona Blair County Airport", "Altoona", "US", true], - "AOP": ["Alferez FAP Alfredo Vladimir Sara Bauer Airport", "Andoas", "PE", false], - "AOQ": ["Aappilattoq Heliport", "Aappilattoq", "GL", true], - "AOR": ["Sultan Abdul Halim Airport", "Alor Satar", "MY", true], - "AOS": ["Amook Bay Seaplane Base", "Amook Bay", "US", true], - "AOT": ["Aosta Corrado Gex Airport", "Saint-Christophe (AO)", "IT", false], - "AOU": ["Attopeu Airport", "Attopeu", "LA", false], - "APA": ["Centennial Airport", "Denver", "US", false], - "APB": ["Apolo Airport", "Apolo", "BO", false], - "APC": ["Napa County Airport", "Napa", "US", false], - "APD": ["Arung Palakka Airport", "Bone", "ID", false], - "APF": ["Naples Municipal Airport", "Naples", "US", false], - "APG": ["Phillips Army Air Field", "Aberdeen", "US", false], - "APH": ["Mary Walker LZ Airport", "Bowling Green", "US", false], - "API": ["Gomez Nino Apiay Air Base", "Apiay", "CO", false], - "APJ": ["Ali Pulan Airport", "Burang Town", "CN", false], - "APK": ["Apataki Airport", "Apataki", "PF", true], - "APL": ["Nampula Airport", "Nampula", "MZ", true], - "APN": ["Alpena County Regional Airport", "Alpena", "US", true], - "APO": ["Antonio Roldán Betancur Airport", "Carepa", "CO", true], - "APP": ["Asapa Airport", "", "PG", false], - "APQ": ["Arapiraca Airport", "Arapiraca", "BR", false], - "APR": ["April River Airport", "April River", "PG", false], - "APS": ["Anápolis Airport", "Anápolis", "BR", false], - "APT": ["Marion County Airport - Brown Field", "Jasper", "US", false], - "APU": ["Captain João Busse Airport", "Apucarana", "BR", false], - "APV": ["Apple Valley Airport", "Apple Valley", "US", false], - "APW": ["Faleolo International Airport", "Apia", "WS", true], - "APX": ["Arapongas Airport", "Arapongas", "BR", false], - "APY": ["Alto Parnaíba Airport", "Alto Parnaíba", "BR", false], - "APZ": ["Zapala Airport", "Zapala", "AR", false], - "AQA": ["Araraquara Airport", "Araraquara", "BR", true], - "AQB": ["Quiché Airport", "Santa Cruz del Quiché", "GT", false], - "AQG": ["Anqing Tianzhushan Airport / Anqing North Air Base", "Anqing", "CN", true], - "AQI": ["Qaisumah–Hafar Al-Batin International Airport", "Qaisumah", "SA", true], - "AQJ": ["King Hussein International Airport", "Aqaba", "JO", true], - "AQM": ["Ariquemes Airport", "Ariquemes", "BR", false], - "AQP": ["Rodríguez Ballón International Airport", "Arequipa", "PE", true], - "AQY": ["Girdwood-Alyeska Airport", "Girdwood", "US", false], - "ARA": ["Acadiana Regional Airport", "New Iberia", "US", false], - "ARB": ["Ann Arbor Municipal Airport", "Ann Arbor", "US", false], - "ARC": ["Arctic Village Airport", "Arctic Village", "US", true], - "ARD": ["Alor Island - Mali Airport", "Kabola", "ID", true], - "ARE": ["Antonio Nery Juarbe Pol Airport", "Arecibo", "PR", false], - "ARF": ["Acaricuara Airport", "Acaricuara", "CO", false], - "ARG": ["Walnut Ridge Regional Airport", "Walnut Ridge", "US", false], - "ARH": ["Talagi Airport", "Archangelsk", "RU", true], - "ARI": ["Chacalluta International Airport", "Arica", "CL", true], - "ARK": ["Arusha Airport", "Arusha", "TZ", true], - "ARL": ["Arly Airport", "Arly", "BF", false], - "ARM": ["Armidale Airport", "Armidale", "AU", true], - "ARN": ["Stockholm-Arlanda Airport", "Stockholm", "SE", true], - "ARO": ["Arboletes Airport", "Arboletes", "CO", false], - "ARP": ["Aragip Airport", "", "PG", false], - "ARQ": ["El Troncal Airport", "Arauquita", "CO", false], - "ARR": ["D. Casimiro Szlapelis Airport", "Alto Rio Senguerr", "AR", false], - "ARS": ["Aragarças Airport", "Aragarças", "BR", false], - "ART": ["Watertown International Airport", "Watertown", "US", true], - "ARU": ["Araçatuba Airport", "Araçatuba", "BR", true], - "ARV": ["Lakeland Airport Noble F Lee Memorial Field", "Woodruff", "US", false], - "ARW": ["Arad International Airport", "Arad", "RO", true], - "ARX": ["Aracati Dragão do Mar Regional Airport", "Aracati", "BR", false], - "ARY": ["Ararat Airport", "Ararat", "AU", false], - "ARZ": ["N'zeto Airport", "N'zeto", "AO", false], - "ASA": ["Assab International Airport", "Assab", "ER", false], - "ASB": ["Ashgabat International Airport", "Ashgabat", "TM", true], - "ASC": ["Ascención de Guarayos Airport", "Ascensión de Guarayos", "BO", false], - "ASD": ["Andros Town Airport", "Andros Town", "BS", true], - "ASE": ["Aspen-Pitkin County Airport (Sardy Field)", "Aspen", "US", true], - "ASF": ["Astrakhan Narimanovo Boris M. Kustodiev International Airport", "Astrakhan", "RU", true], - "ASG": ["Ashburton Airport", "Ashburton", "NZ", false], - "ASH": ["Nashua Airport / Boire Field", "Nashua", "US", false], - "ASI": ["RAF Ascension Island", "Cat Hill", "SH", true], - "ASJ": ["Amami Airport", "Amami", "JP", true], - "ASK": ["Yamoussoukro International Airport", "Yamoussoukro", "CI", false], - "ASL": ["Harrison County Airport", "Marshall", "US", false], - "ASM": ["Asmara International Airport", "Asmara", "ER", true], - "ASN": ["Talladega Municipal Airport", "Talladega", "US", false], - "ASO": ["Asosa Airport", "Asosa", "ET", true], - "ASP": ["Alice Springs Airport", "Alice Springs", "AU", true], - "ASQ": ["Austin Airport", "Austin", "US", false], - "ASR": ["Kayseri Erkilet International Airport", "Kayseri", "TR", true], - "ASS": ["Arathusa Safari Lodge Airport", "Arathusa", "ZA", false], - "AST": ["Astoria Regional Airport", "Astoria", "US", false], - "ASU": ["Silvio Pettirossi International Airport", "Asunción", "PY", true], - "ASV": ["Amboseli Airport", "Ol Tukai", "KE", true], - "ASW": ["Aswan International Airport", "Aswan", "EG", true], - "ASX": ["John F Kennedy Memorial Airport", "Ashland", "US", false], - "ASY": ["Ashley Municipal Airport", "Ashley", "US", false], - "ASZ": ["Asirim Airport", "", "PG", false], - "ATA": ["Comandante FAP German Arias Graziani Airport", "Anta", "PE", false], - "ATB": ["Atbara Airport", "Atbara", "SD", false], - "ATC": ["Arthur's Town Airport", "Arthur's Town", "BS", true], - "ATD": ["Uru Harbour Airport", "Atoifi", "SB", false], - "ATE": ["Antlers Municipal Airport", "Antlers", "US", false], - "ATF": ["Chachoán Regional Airport", "Ambato", "EC", false], - "ATG": ["Minhas Air Base", "Kamra", "PK", false], - "ATH": ["Athens Eleftherios Venizelos International Airport", "Spata-Artemida", "GR", true], - "ATI": ["Artigas International Airport", "Artigas", "UY", false], - "ATJ": ["Antsirabe Airport", "Antsirabe", "MG", false], - "ATK": ["Atqasuk Edward Burnell Sr Memorial Airport", "Atqasuk", "US", true], - "ATL": ["Hartsfield Jackson Atlanta International Airport", "Atlanta", "US", true], - "ATM": ["Altamira Interstate Airport", "Altamira", "BR", true], - "ATN": ["Namatanai Airport", "Namatanai", "PG", false], - "ATO": ["Ohio University Airport", "Albany", "US", false], - "ATP": ["Aitape Airport", "Aitape", "PG", false], - "ATQ": ["Sri Guru Ram Das Ji International Airport", "Amritsar", "IN", true], - "ATR": ["Atar International Airport", "Atar", "MR", false], - "ATS": ["Artesia Municipal Airport", "Artesia", "US", false], - "ATT": ["Atmautluak Airport", "Atmautluak", "US", true], - "ATU": ["Casco Cove Coast Guard Station", "Attu", "US", false], - "ATV": ["Ati Airport", "Ati", "TD", false], - "ATW": ["Appleton International Airport", "Appleton", "US", true], - "ATX": ["Atbasar Airport", "Atbasar", "KZ", false], - "ATY": ["Watertown Regional Airport", "Watertown", "US", true], - "ATZ": ["Asyut International Airport", "Asyut", "EG", true], - "AUA": ["Queen Beatrix International Airport", "Oranjestad", "AW", true], - "AUC": ["Santiago Perez Airport", "Arauca", "CO", true], - "AUD": ["Augustus Downs Airport", "", "AU", false], - "AUE": ["Abu Rudeis Airport", "Ras Abu Rudeis", "EG", false], - "AUF": ["Auxerre Branches airport", "Auxerre", "FR", false], - "AUG": ["Augusta State Airport", "Augusta", "US", true], - "AUH": ["Zayed International Airport", "Abu Dhabi", "AE", true], - "AUI": ["Aua Island Airport", "Aua Island", "PG", false], - "AUJ": ["Ambunti Airport", "Ambunti", "PG", false], - "AUK": ["Alakanuk Airport", "Alakanuk", "US", true], - "AUL": ["Aur Island Airport", "Aur Atoll", "MH", true], - "AUM": ["Austin Municipal Airport", "Austin", "US", false], - "AUN": ["Auburn Municipal Airport", "Auburn", "US", false], - "AUO": ["Auburn University Regional Airport", "Auburn", "US", false], - "AUP": ["Agaun Airport", "Agaun", "PG", false], - "AUQ": ["Hiva Oa-Atuona Airport", "Hiva Oa Island", "PF", true], - "AUR": ["Aurillac airport", "Aurillac", "FR", true], - "AUS": ["Austin Bergstrom International Airport", "Austin", "US", true], - "AUT": ["Atauro Airport", "Vila", "TL", false], - "AUU": ["Aurukun Airport", "Aurukun", "AU", true], - "AUV": ["Aumo Airport", "Aumo", "PG", false], - "AUW": ["Wausau Downtown Airport", "Wausau", "US", false], - "AUX": ["Araguaína Airport", "Araguaína", "BR", true], - "AUY": ["Aneityum Airport", "Anatom Island", "VU", false], - "AUZ": ["Aurora Municipal Airport", "Chicago/Aurora", "US", false], - "AVA": ["Anshun Huangguoshu Airport", "Anshun (Xixiu)", "CN", true], - "AVB": ["Aviano Air Base", "Aviano (PN)", "IT", false], - "AVG": ["Auvergne Airport", "Auvergne Station", "AU", false], - "AVI": ["Máximo Gómez Airport", "Ciro Redondo", "CU", false], - "AVK": ["Arvaikheer Airport", "Arvaikheer", "MN", true], - "AVL": ["Asheville Regional Airport", "Asheville", "US", true], - "AVN": ["Avignon Caumont airport", "Avignon", "FR", true], - "AVO": ["Avon Park Executive Airport", "Avon Park", "US", false], - "AVP": ["Wilkes-Barre/Scranton International Airport", "Wilkes-Barre/Scranton", "US", true], - "AVR": ["Amravati Airport", "Amravati", "IN", true], - "AVU": ["Avu Avu Airport", "Avu Avu", "SB", false], - "AVV": ["Melbourne Avalon International Airport", "Geelong/Melbourne", "AU", true], - "AVW": ["Marana Regional Airport", "Marana", "US", false], - "AVX": ["Catalina Airport", "Avalon", "US", false], - "AWA": ["Hawassa International Airport", "Hawassa", "ET", true], - "AWB": ["Awaba Airport", "Awaba", "PG", false], - "AWD": ["Aniwa Airport", "Aniwa", "VU", false], - "AWE": ["Alowe Airport", "Wonga Wongué Presidential Reserve", "GA", false], - "AWK": ["Wake Island Airfield", "Wake Island", "UM", true], - "AWM": ["West Memphis Municipal Airport", "West Memphis", "US", false], - "AWN": ["Alton Downs Airport", "", "AU", false], - "AWP": ["Austral Downs Airport", "", "AU", false], - "AWR": ["Awar Airport (unusable)", "Awar", "PG", false], - "AWZ": ["Qasem Soleimani International Airport", "Ahvaz", "IR", true], - "AXA": ["Clayton J. Lloyd International Airport", "The Valley", "AI", true], - "AXB": ["Maxson Airfield", "Alexandria Bay", "US", false], - "AXC": ["Aramac Airport", "", "AU", false], - "AXD": ["Alexandroupoli Democritus Airport", "Alexandroupolis", "GR", true], - "AXE": ["Xanxerê Airport", "Xanxerê", "BR", false], - "AXF": ["Alxa Left Banner Bayanhot Airport", "Bayanhot", "CN", true], - "AXG": ["Algona Municipal Airport", "Algona", "US", false], - "AXJ": ["Amakusa Airport", "Amakusa", "JP", true], - "AXK": ["Ataq Airport", "Ataq", "YE", false], - "AXL": ["Alexandria Homestead Airport", "", "AU", false], - "AXM": ["El Eden Airport", "Armenia", "CO", true], - "AXN": ["Chandler Field", "Alexandria", "US", false], - "AXO": ["Pantar Kabir Airport", "Kabir", "ID", false], - "AXP": ["Spring Point Airport", "Spring Point", "BS", true], - "AXR": ["Arutua Airport", "", "PF", true], - "AXS": ["Altus Quartz Mountain Regional Airport", "Altus", "US", false], - "AXT": ["Akita Airport", "Akita", "JP", true], - "AXU": ["Axum Airport", "Axum", "ET", true], - "AXV": ["Neil Armstrong Airport", "Wapakoneta", "US", false], - "AXX": ["Angel Fire Airport", "Angel Fire", "US", false], - "AYA": ["Ayapel Airport", "Ayapel", "CO", false], - "AYC": ["Ayacucho Airport", "Ayacucho", "CO", false], - "AYD": ["Alroy Downs Airport", "Alroy Downs", "AU", false], - "AYG": ["Yaguara Airport", "San Vicente Del Caguán", "CO", false], - "AYI": ["Yari Airport", "Yari", "CO", false], - "AYJ": ["Maharshi Valmiki International Airport", "Faizabad", "IN", true], - "AYK": ["Arkalyk North Airport", "Arkalyk", "KZ", false], - "AYL": ["Anthony Lagoon Airport", "", "AU", false], - "AYM": ["Yas Island Seaplane Base", "Abu Dhabi", "AE", false], - "AYN": ["Anyang Yindu Airport", "Anyang", "CN", false], - "AYO": ["Aeropuerto Nacional Juan de Ayolas", "Ayolas", "PY", false], - "AYP": ["Air Force Colonel Alfredo Mendivil Duarte Airport", "Ayacucho", "PE", true], - "AYQ": ["Ayers Rock Connellan Airport", "Yulara", "AU", true], - "AYR": ["Ayr Airport", "Ayr", "AU", false], - "AYS": ["Waycross Ware County Airport", "Waycross", "US", false], - "AYT": ["Antalya International Airport", "Antalya", "TR", true], - "AYU": ["Aiyura Airport", "Aiyura Valley", "PG", false], - "AYW": ["Ayawasi Airport", "Ayawasi", "ID", false], - "AYX": ["Teniente General Gerardo Pérez Pinedo Airport", "Atalaya", "PE", false], - "AYY": ["Arugam Bay SPB", "Pottuvil", "LK", false], - "AZA": ["Mesa Gateway Airport", "Mesa", "US", true], - "AZB": ["Amazon Bay Airport", "", "PG", false], - "AZD": ["Shahid Sadooghi Airport", "Yazd", "IR", true], - "AZG": ["Apatzingán - Pablo L. Sidar Airport", "Apatzingán", "MX", false], - "AZH": ["Azamgarh Airport", "Azamgarh", "IN", false], - "AZI": ["Al Bateen Executive Airport", "Abu Dhabi", "AE", false], - "AZJ": ["Zhenjiang Dalu Airport", "Zhenjiang", "CN", false], - "AZL": ["Fazenda Tucunaré Airport", "Sapezal", "BR", false], - "AZN": ["Andijan International Airport", "Andijan", "UZ", true], - "AZO": ["Kalamazoo/Battle Creek International Airport", "Kalamazoo", "US", true], - "AZP": ["Jorge Jiménez Cantú National Airport", "Atizapán de Zaragoza", "MX", false], - "AZR": ["Touat-Cheikh Sidi Mohamed Belkebir Airport", "Adrar", "DZ", true], - "AZS": ["Samaná El Catey International Airport", "Samana", "DO", true], - "AZT": ["Zapatoca Airport", "Zapatoca", "CO", false], - "AZZ": ["Ambriz Airport", "Ambriz", "AO", false], - "BAA": ["Bialla Airport", "Bialla, Matalilu, Ewase", "PG", false], - "BAB": ["Beale Air Force Base", "Beale Air Force Base", "US", false], - "BAC": ["Barranca De Upia Airport", "Barranca De Upia", "CO", false], - "BAD": ["Barksdale Air Force Base", "Bossier City", "US", false], - "BAE": ["Barcelonnette - Saint-Pons Airfield", "Saint-Pons, Alpes-de-Haute-Provence", "FR", false], - "BAF": ["Westfield-Barnes Regional Airport", "Westfield", "US", false], - "BAG": ["Loakan Airport", "Baguio", "PH", false], - "BAH": ["Bahrain International Airport", "Manama", "BH", true], - "BAI": ["Buenos Aires Airport", "Punta Arenas", "CR", false], - "BAJ": ["Bali Airport", "Unea Island", "PG", false], - "BAL": ["Batman Airport", "Batman", "TR", true], - "BAM": ["Battle Mountain Airport", "Battle Mountain", "US", false], - "BAN": ["Basongo Airport", "Basongo", "CD", false], - "BAQ": ["Ernesto Cortissoz International Airport", "Barranquilla", "CO", true], - "BAR": ["Qionghai Bo'ao Airport", "Qionghai (Basuo)", "CN", true], - "BAS": ["Ballalae Airport", "Ballalae", "SB", true], - "BAT": ["Chafei Amsei Airport", "Barretos", "BR", false], - "BAV": ["Baotou Donghe International Airport", "Baotou", "CN", true], - "BAX": ["Barnaul Gherman Titov International Airport", "Barnaul", "RU", true], - "BAY": ["Maramureș International Airport", "Tăuții-Măgherăuș", "RO", true], - "BAZ": ["Barcelos Airport", "Barcelos", "BR", true], - "BBA": ["Balmaceda Airport", "Balmaceda", "CL", true], - "BBB": ["Benson Municipal Airport", "Benson", "US", false], - "BBC": ["Bay City Regional Airport", "Bay City", "US", false], - "BBD": ["Curtis Field", "Brady", "US", false], - "BBG": ["Butaritari Airport", "Butaritari", "KI", true], - "BBH": ["Stralsund-Barth Airport", "Barth", "DE", false], - "BBI": ["Biju Patnaik International Airport", "Bhubaneswar", "IN", true], - "BBJ": ["Bitburg Airport", "Bitburg", "DE", false], - "BBK": ["Kasane International Airport", "Kasane", "BW", true], - "BBL": ["Ballera Airport", "", "AU", false], - "BBM": ["Battambang Airport", "Battambang", "KH", true], - "BBN": ["Bario Airport", "Bario", "MY", true], - "BBO": ["Berbera Airport", "Berbera", "SO", true], - "BBP": ["Bembridge Airport", "Bembridge, Isle of Wight", "GB", false], - "BBQ": ["Burton-Nibbs International Airport", "Codrington", "AG", true], - "BBR": ["Basse-Terre Baillif Airport", "Basse-Terre", "GP", true], - "BBS": ["Blackbushe Airport", "Camberley, Surrey", "GB", false], - "BBT": ["Berbérati Airport", "Berbérati", "CF", false], - "BBU": ["Bucharest Băneasa Aurel Vlaicu International Airport", "Bucharest", "RO", true], - "BBV": ["Nero-Mer Airport", "Grand-Béréby", "CI", false], - "BBW": ["Broken Bow Municipal Airport", "Broken Bow", "US", false], - "BBX": ["Wings Field", "Philadelphia", "US", false], - "BBY": ["Bambari Airport", "Bambari", "CF", false], - "BBZ": ["Zambezi Airport", "Zambezi", "ZM", false], - "BCA": ["Gustavo Rizo Airport", "Baracoa", "CU", true], - "BCB": ["Virginia Tech Montgomery Executive Airport", "Blacksburg", "US", false], - "BCC": ["Bear Creek 3 Airport", "Bear Creek", "US", false], - "BCD": ["Bacolod-Silay International Airport", "Bacolod City", "PH", true], - "BCE": ["Bryce Canyon Airport", "Bryce Canyon", "US", false], - "BCF": ["Bouca Airport", "Bouca", "CF", false], - "BCG": ["Bemichi Airport", "Kumaka", "GY", false], - "BCH": ["Baucau Airport", "Baucau", "TL", true], - "BCI": ["Barcaldine Airport", "Barcaldine", "AU", true], - "BCJ": ["Beichuan Yongchang Airport", "Mianyang", "CN", false], - "BCK": ["Bolwarra Airport", "", "AU", false], - "BCL": ["Barra del Colorado Airport", "Pococi", "CR", false], - "BCM": ["Bacău George Enescu International Airport", "Bacău", "RO", true], - "BCN": ["Josep Tarradellas Barcelona-El Prat Airport", "Barcelona", "ES", true], - "BCO": ["Jinka Airport", "Jinka", "ET", true], - "BCP": ["Bambu Airport", "Bambu", "PG", false], - "BCQ": ["Brak Airport", "Brak", "LY", false], - "BCR": ["Novo Campo Airport", "Boca do Acre", "BR", false], - "BCS": ["Southern Seaplane Airport", "Belle Chasse", "US", false], - "BCT": ["Boca Raton Airport", "Boca Raton", "US", false], - "BCU": ["Sir Abubakar Tafawa Balewa Bauchi State International Airport", "Bauchi", "NG", true], - "BCV": ["Hector Silva Airstrip", "Belmopan", "BZ", false], - "BCW": ["Benguera Island Airport", "Benguera Island", "MZ", false], - "BCX": ["Beloretsk Airport", "Beloretsk", "RU", false], - "BCZ": ["Milyakburra Airport", "Bickerton Island", "AU", false], - "BDA": ["L.F. Wade International Airport", "Hamilton", "BM", true], - "BDB": ["Bundaberg Airport", "Bundaberg", "AU", true], - "BDC": ["Barra do Corda Airport", "Barra do Corda", "BR", false], - "BDD": ["Badu Island Airport", "Badu Island", "AU", true], - "BDE": ["Baudette International Airport", "Baudette", "US", false], - "BDF": ["Rinkenberger Restricted Landing Area", "Bradford", "US", false], - "BDG": ["Blanding Municipal Airport", "Blanding", "US", false], - "BDH": ["Bandar Lengeh International Airport", "Bandar Lengeh", "IR", true], - "BDI": ["Bird Island Airport", "Bird Island", "SC", false], - "BDJ": ["Syamsudin Noor International Airport", "Banjarbaru", "ID", true], - "BDK": ["Soko Airport", "Bondoukou", "CI", false], - "BDL": ["Bradley International Airport", "Hartford", "US", true], - "BDM": ["Bandırma Airport", "Bandırma", "TR", false], - "BDN": ["Talhar Airport", "Badin", "PK", false], - "BDO": ["Husein Sastranegara International Airport", "Bandung", "ID", true], - "BDP": ["Bhadrapur Airport", "Bhadrapur", "NP", true], - "BDQ": ["Vadodara International Airport", "Vadodara", "IN", true], - "BDR": ["Igor I Sikorsky Memorial Airport", "Bridgeport", "US", false], - "BDS": ["Brindisi Airport", "Brindisi", "IT", true], - "BDT": ["Gbadolite Airport", "Gbadolite", "CD", true], - "BDU": ["Bardufoss Airport", "Målselv", "NO", true], - "BDV": ["Moba Airport", "Moba", "CD", false], - "BDW": ["Bedford Downs Airport", "", "AU", false], - "BDX": ["Broadus Airport", "Broadus", "US", false], - "BDY": ["Bandon State Airport", "Bandon", "US", false], - "BDZ": ["Baindoung Airport", "", "PG", false], - "BEB": ["Benbecula Airport", "Balivanich", "GB", true], - "BEC": ["Beech Factory Airport", "Wichita", "US", false], - "BED": ["Laurence G Hanscom Field", "Bedford", "US", true], - "BEE": ["Beagle Bay Airport", "Dampier Peninsula", "AU", false], - "BEF": ["Bluefields Airport", "Bluefields", "NI", true], - "BEG": ["Belgrade Nikola Tesla Airport", "Belgrade", "RS", true], - "BEH": ["Southwest Michigan Regional Airport", "Benton Harbor", "US", false], - "BEI": ["Beica Airport", "Beica", "ET", false], - "BEJ": ["Kalimarau Airport", "Tanjung Redeb - Borneo Island", "ID", true], - "BEK": ["Bareilly Air Force Station", "Bareilly", "IN", true], - "BEL": ["Val de Cans/Júlio Cezar Ribeiro International Airport", "Belém", "BR", true], - "BEM": ["Beni Mellal Airport", "Oulad Yaich", "MA", true], - "BEN": ["Benina International Airport", "Benina", "LY", true], - "BEO": ["Lake Macquarie Airport", "City of Lake Macquarie", "AU", false], - "BEP": ["Bellary Airport", "Bellary", "IN", false], - "BEQ": ["RAF Honington", "Bury Saint Edmunds, Suffolk", "GB", false], - "BER": ["Berlin Brandenburg Airport", "Berlin", "DE", true], - "BES": ["Brest Bretagne airport", "Brest", "FR", true], - "BET": ["Bethel Airport", "Bethel", "US", true], - "BEU": ["Bedourie Airport", "Bedourie", "AU", true], - "BEV": ["Beersheba (Teyman) Airport", "Beersheba", "IL", false], - "BEW": ["Beira International Airport", "Beira", "MZ", true], - "BEX": ["RAF Benson", "Wallingford, Oxfordshire", "GB", false], - "BEY": ["Beirut Rafic Hariri International Airport", "Beirut", "LB", true], - "BEZ": ["Beru Airport", "Beru", "KI", false], - "BFA": ["Bahía Negra Airport", "Bahía Negra", "PY", false], - "BFC": ["Bloomfield River Airport", "", "AU", false], - "BFD": ["Bradford Regional Airport", "Bradford", "US", true], - "BFE": ["Bielefeld Airport", "Bielefeld", "DE", false], - "BFF": ["Western Neb. Rgnl/William B. Heilig Airport", "Scottsbluff", "US", true], - "BFG": ["Bullfrog Basin Airport", "Bullfrog", "US", false], - "BFH": ["Bacacheri Airport", "Curitiba", "BR", false], - "BFI": ["King County International Airport - Boeing Field", "Seattle", "US", true], - "BFJ": ["Bijie Feixiong Airport", "Bijie", "CN", true], - "BFK": ["Buckley Space Force Base", "Aurora", "US", false], - "BFL": ["Meadows Field", "Bakersfield", "US", true], - "BFM": ["Mobile Downtown Airport", "Mobile", "US", false], - "BFN": ["Bram Fischer International Airport", "Bloemfontein", "ZA", true], - "BFO": ["Buffalo Range Airport", "Chiredzi", "ZW", false], - "BFP": ["Beaver County Airport", "Beaver Falls", "US", false], - "BFQ": ["Bahia Piña Airport", "Puerto Piña", "PA", true], - "BFR": ["Virgil I Grissom Municipal Airport", "Bedford", "US", false], - "BFS": ["Belfast International Airport", "Belfast", "GB", true], - "BFT": ["Beaufort Executive Airport", "Beaufort", "US", false], - "BFU": ["Bengbu Renheji Airport", "Bengbu", "CN", false], - "BFV": ["Buri Ram Airport", "Buriram", "TH", true], - "BFW": ["Sidi Bel Abbes Airport", "Sidi Bel Abbès", "DZ", false], - "BFX": ["Bafoussam Airport", "Bafoussam", "CM", false], - "BFY": ["Bengbu Tenghu Airport", "Bengbu", "CN", true], - "BGA": ["Palonegro Airport", "Bucaramanga", "CO", true], - "BGB": ["Booue Airport", "Booue", "GA", false], - "BGC": ["Bragança Airport", "Bragança", "PT", true], - "BGD": ["Hutchinson County Airport", "Borger", "US", false], - "BGE": ["Decatur County Industrial Air Park", "Bainbridge", "US", false], - "BGF": ["Bangui M'Poko International Airport", "Bangui", "CF", true], - "BGG": ["Bingöl Airport", "Bingöl", "TR", true], - "BGH": ["Abbaye Airport", "Boghe", "MR", false], - "BGI": ["Grantley Adams International Airport", "Bridgetown", "BB", true], - "BGJ": ["Borgarfjörður eystri Airport", "Borgarfjörður eystri", "IS", false], - "BGK": ["Big Creek Airport", "Big Creek", "BZ", true], - "BGL": ["Baglung Airport", "Baglung", "NP", false], - "BGM": ["Greater Binghamton/Edwin A Link field", "Binghamton", "US", true], - "BGN": ["Belaya Gora Airport", "Belaya Gora", "RU", false], - "BGO": ["Bergen Airport, Flesland", "Bergen", "NO", true], - "BGQ": ["Big Lake Airport", "Big Lake", "US", false], - "BGR": ["Bangor International Airport", "Bangor", "US", true], - "BGS": ["Boguchany Airport", "Boguchany", "RU", false], - "BGT": ["Bagdad Airport", "Bagdad", "US", false], - "BGU": ["Bangassou Airport", "Bangassou", "CF", false], - "BGV": ["Aeroclube de Bento Gonçalves Airport", "Bento Gonçalves", "BR", false], - "BGW": ["Baghdad International Airport / New Al Muthana Air Base", "Baghdad", "IQ", true], - "BGX": ["Comandante Gustavo Kraemer Airport", "Bagé", "BR", false], - "BGY": ["Il Caravaggio International Airport", "Orio al Serio (BG)", "IT", true], - "BHA": ["Los Perales Airport", "Bahía de Caraquez", "EC", false], - "BHB": ["Hancock County-Bar Harbor Airport", "Bar Harbor", "US", true], - "BHC": ["Bhurban Heliport", "Bhurban", "PK", false], - "BHD": ["George Best Belfast City Airport", "Belfast", "GB", true], - "BHE": ["Woodbourne Airport", "Blenheim", "NZ", true], - "BHF": ["Cupica Airport", "Bahía Solano", "CO", false], - "BHG": ["Brus Laguna Airport", "Brus Laguna", "HN", false], - "BHH": ["Bisha Airport", "Bisha", "SA", true], - "BHI": ["Comandante Espora Airport", "Bahía Blanca", "AR", true], - "BHJ": ["Bhuj Airport", "Bhuj", "IN", true], - "BHK": ["Bukhara International Airport", "Bukhara", "UZ", true], - "BHL": ["Bahía de los Ángeles Airport", "Ensenada", "MX", false], - "BHM": ["Birmingham-Shuttlesworth International Airport", "Birmingham", "US", true], - "BHN": ["Beihan Airport", "Beihan", "YE", false], - "BHO": ["Raja Bhoj International Airport", "Bhopal", "IN", true], - "BHP": ["Bhojpur Airport", "Bhojpur", "NP", false], - "BHQ": ["Broken Hill Airport", "Broken Hill", "AU", true], - "BHR": ["Bharatpur Airport", "Bharatpur", "NP", true], - "BHS": ["Bathurst Airport", "Bathurst", "AU", true], - "BHT": ["Brighton Downs Airport", "Brighton Downs", "AU", false], - "BHU": ["Bhavnagar Airport", "Bhavnagar", "IN", true], - "BHV": ["Bahawalpur Airport", "Bahawalpur", "PK", true], - "BHW": ["Bhagatanwala Airport", "Bhagatanwala", "PK", false], - "BHX": ["Birmingham Airport", "Birmingham, West Midlands", "GB", true], - "BHY": ["Beihai Fucheng Airport", "Beihai", "CN", true], - "BIA": ["Bastia-Poretta International airport", "Bastia", "FR", true], - "BIB": ["Baidoa Airport", "Baidoa", "SO", false], - "BIC": ["Aérodrome de Belle Île", "Bangor", "FR", false], - "BID": ["Block Island State Airport", "Block Island", "US", true], - "BIE": ["Beatrice Municipal Airport", "Beatrice", "US", false], - "BIF": ["Biggs Army Air Field (Fort Bliss)", "Fort Bliss/El Paso", "US", false], - "BIG": ["Allen Army Airfield", "Delta Junction Ft Greely", "US", false], - "BIH": ["Eastern Sierra Regional Airport", "Bishop", "US", true], - "BII": ["Bikini Atoll Airport", "Bikini Atoll", "MH", false], - "BIJ": ["Biliau Airport", "", "PG", false], - "BIK": ["Frans Kaisiepo Airport", "Biak", "ID", true], - "BIL": ["Billings Logan International Airport", "Billings", "US", true], - "BIM": ["South Bimini Airport", "South Bimini", "BS", true], - "BIN": ["Bamyan Airport", "Bamyan", "AF", false], - "BIO": ["Bilbao Airport", "Bilbao", "ES", true], - "BIP": ["Bulimba Airport", "", "AU", false], - "BIQ": ["Biarritz Pays Basque airport", "Biarritz", "FR", true], - "BIR": ["Biratnagar Airport", "Biratnagar", "NP", true], - "BIS": ["Bismarck Municipal Airport", "Bismarck", "US", true], - "BIT": ["Baitadi Airport", "Baitadi", "NP", false], - "BIU": ["Bildudalur Airport", "Bildudalur", "IS", false], - "BIV": ["Bria Airport", "Bria", "CF", false], - "BIW": ["Billiluna Airport", "", "AU", false], - "BIX": ["Keesler Air Force Base", "Biloxi", "US", false], - "BIY": ["Bisho Airport", "Bisho", "ZA", false], - "BIZ": ["Bimin Airport", "Bimin", "PG", false], - "BJA": ["Soummam–Abane Ramdane Airport", "Béjaïa", "DZ", true], - "BJB": ["Bojnord Airport", "Bojnord", "IR", true], - "BJC": ["Rocky Mountain Metropolitan Airport", "Denver", "US", true], - "BJE": ["Baleela Airport", "Baleela Base Camp", "SD", false], - "BJF": ["Båtsfjord Airport", "Båtsfjord", "NO", true], - "BJH": ["Bajhang Airport", "Bajhang", "NP", false], - "BJI": ["Bemidji Regional Airport", "Bemidji", "US", false], - "BJJ": ["Wayne County Airport", "Wooster", "US", false], - "BJK": ["Benjina Airport", "Maikoor Island", "ID", false], - "BJL": ["Banjul International Airport", "Banjul (Yundum)", "GM", true], - "BJM": ["Bujumbura Melchior Ndadaye International Airport", "Bujumbura", "BI", true], - "BJO": ["Bermejo Airport", "Bermejo", "BO", false], - "BJP": ["Estadual Arthur Siqueira Airport", "Bragança Paulista", "BR", false], - "BJQ": ["Bahja Airport", "Bahja", "OM", false], - "BJR": ["Bahir Dar Airport", "Bahir Dar", "ET", true], - "BJT": ["Bentota River Waterdrome", "Bentota", "LK", true], - "BJU": ["Bajura Airport", "Bajura", "NP", false], - "BJV": ["Milas Bodrum International Airport", "Bodrum", "TR", true], - "BJW": ["Bajawa Soa (Turelelo) Airport", "Soa, Ngada", "ID", false], - "BJX": ["Guanajuato International Airport", "Silao", "MX", true], - "BJY": ["Batajnica Air Base", "Zemun", "RS", false], - "BJZ": ["Badajoz Airport", "Badajoz", "ES", true], - "BKA": ["Baykit Airport", "Baykit", "RU", false], - "BKB": ["Nal Airport", "Bikaner", "IN", false], - "BKC": ["Buckland Airport", "Buckland", "US", true], - "BKD": ["Stephens County Airport", "Breckenridge", "US", false], - "BKE": ["Baker City Municipal Airport", "Baker City", "US", false], - "BKF": ["Lake Brooks Seaplane Base", "Katmai National Park", "US", true], - "BKG": ["Branson Airport", "Branson", "US", true], - "BKH": ["Barking Sands Airport", "Kekaha", "US", false], - "BKI": ["Kota Kinabalu International Airport", "Kota Kinabalu", "MY", true], - "BKJ": ["Boké Baralande Airport", "Boké", "GN", false], - "BKK": ["Suvarnabhumi Airport", "Bangkok", "TH", true], - "BKL": ["Burke Lakefront Airport", "Cleveland", "US", false], - "BKM": ["Bakalalan Airport", "Bakalalan", "MY", true], - "BKN": ["Balkanabat International Airport", "Balkanabat", "TM", true], - "BKO": ["Modibo Keita International Airport", "Bamako", "ML", true], - "BKP": ["Barkly Downs Airport", "", "AU", false], - "BKQ": ["Blackall Airport", "Blackall", "AU", true], - "BKR": ["Bokoro Airport", "Bokoro", "TD", false], - "BKS": ["Fatmawati Soekarno Airport", "Bengkulu", "ID", true], - "BKT": ["Allen C Perkinson Blackstone Army Air Field", "Blackstone", "US", false], - "BKU": ["Betioky Airport", "Betioky", "MG", false], - "BKV": ["Bailing Airport", "Bailingmiao", "CN", false], - "BKW": ["Raleigh County Memorial Airport", "Beaver", "US", true], - "BKX": ["Brookings Regional Airport", "Brookings", "US", false], - "BKY": ["Bukavu Kavumu Airport", "Kamakombe", "CD", false], - "BKZ": ["Bukoba Airport", "Bukoba", "TZ", true], - "BLA": ["General José Antonio Anzoategui International Airport", "Barcelona", "VE", true], - "BLB": ["Panamá Pacífico International Airport", "Panamá City", "PA", true], - "BLC": ["Bali Airport", "Bali", "CM", false], - "BLD": ["Boulder City Municipal Airport", "Boulder City", "US", true], - "BLE": ["Dala Airport", "Borlange", "SE", true], - "BLF": ["Mercer County Airport", "Bluefield", "US", false], - "BLG": ["Belaga Airport", "Belaga", "MY", false], - "BLH": ["Blythe Airport", "Blythe", "US", false], - "BLI": ["Bellingham International Airport", "Bellingham", "US", true], - "BLJ": ["Batna Mostefa Ben Boulaid Airport", "Batna", "DZ", true], - "BLK": ["Blackpool Airport", "Blackpool", "GB", false], - "BLL": ["Billund Airport", "Billund", "DK", true], - "BLM": ["Monmouth Executive Airport", "Belmar/Farmingdale", "US", false], - "BLN": ["Benalla Airport", "Benalla", "AU", false], - "BLO": ["Blönduós Airport", "Blönduós", "IS", false], - "BLP": ["Huallaga Airport", "Bellavista", "PE", false], - "BLQ": ["Bologna Guglielmo Marconi Airport", "Bologna", "IT", true], - "BLR": ["Kempegowda International Airport Bengaluru", "Bengaluru", "IN", true], - "BLS": ["Bollon Airport", "", "AU", false], - "BLT": ["Blackwater Airport", "", "AU", false], - "BLU": ["Blue Canyon Nyack Airport", "Emigrant Gap", "US", false], - "BLV": ["Scott AFB/Midamerica Airport", "Belleville", "US", true], - "BLW": ["Beledweyne Airport", "Beledweyne", "SO", true], - "BLX": ["Belluno Arturo dell'Oro Airport", "Belluno (BL)", "IT", false], - "BLY": ["Belmullet Aerodrome", "Belmullet", "IE", false], - "BLZ": ["Chileka International Airport", "Blantyre", "MW", true], - "BMA": ["Stockholm-Bromma Airport", "Stockholm", "SE", true], - "BMB": ["Bumba Airport", "Bumba", "CD", false], - "BMC": ["Brigham City Regional Airport", "Brigham City", "US", false], - "BMD": ["Belo sur Tsiribihina Airport", "Belo sur Tsiribihina", "MG", false], - "BME": ["Broome International Airport", "Broome", "AU", true], - "BMF": ["Bakouma Airport", "Bakouma", "CF", false], - "BMG": ["Monroe County Airport", "Bloomington", "US", false], - "BMH": ["Bomai Airport", "Bomai", "PG", false], - "BMI": ["Central Illinois Regional Airport at Bloomington-Normal", "Bloomington/Normal", "US", true], - "BMJ": ["Baramita Airport", "Baramita", "GY", false], - "BMK": ["Borkum Airport", "Borkum", "DE", true], - "BML": ["Berlin Regional Airport", "Berlin", "US", false], - "BMM": ["Bitam Airport", "Bitam", "GA", false], - "BMN": ["Bamarni Airport", "Bamarni", "IQ", false], - "BMO": ["Banmaw Airport", "Banmaw", "MM", true], - "BMP": ["Brampton Island Airport", "", "AU", false], - "BMQ": ["Bamburi Airport", "Bamburi", "KE", false], - "BMR": ["Baltrum Airport", "Baltrum", "DE", true], - "BMS": ["Sócrates Mariani Bittencourt Airport", "Brumado", "BR", false], - "BMT": ["Beaumont Municipal Airport", "Beaumont", "US", false], - "BMU": ["Sultan Muhammad Salahuddin Airport", "Bima", "ID", true], - "BMV": ["Buon Ma Thuot Airport", "Buon Ma Thuot", "VN", true], - "BMW": ["Bordj Badji Mokhtar Airport", "Bordj Badji Mokhtar", "DZ", true], - "BMX": ["Big Mountain Airport", "Big Mountain", "US", false], - "BMY": ["Île Art - Waala Airport", "Waala", "NC", true], - "BMZ": ["Bamu Airport", "Bamu", "PG", false], - "BNA": ["Nashville International Airport", "Nashville", "US", true], - "BNB": ["Boende Airport", "Boende", "CD", true], - "BNC": ["Beni Airport", "Beni", "CD", false], - "BND": ["Bandar Abbas International Airport", "Bandar Abbas", "IR", true], - "BNE": ["Brisbane International Airport", "Brisbane", "AU", true], - "BNF": ["Baranof Warm Springs Float and Seaplane Base", "Baranof", "US", false], - "BNG": ["Banning Municipal Airport", "Banning", "US", false], - "BNH": ["Cape Air Seaplanes on Boston Harbor Seaplane Base", "Boston", "US", false], - "BNI": ["Benin Airport", "Benin", "NG", true], - "BNK": ["Ballina Byron Gateway Airport", "Ballina", "AU", true], - "BNL": ["Barnwell Regional Airport", "Barnwell", "US", false], - "BNM": ["Bodinumu Airport", "Bodinumu", "PG", false], - "BNN": ["Brønnøysund Airport, Brønnøy", "Brønnøy", "NO", true], - "BNO": ["Burns Municipal Airport", "Burns", "US", false], - "BNR": ["Banfora Airport", "Banfora", "BF", false], - "BNS": ["Barinas Airport", "Barinas", "VE", true], - "BNT": ["Bundi Airport", "Bundi", "PG", false], - "BNU": ["Blumenau Airport", "Blumenau", "BR", false], - "BNV": ["Boana Airport", "Boana", "PG", false], - "BNW": ["Boone Municipal Airport", "Boone", "US", false], - "BNX": ["Banja Luka International Airport", "Mahovljani", "BA", true], - "BNY": ["Bellona/Anua Airport", "Anua", "SB", true], - "BOA": ["Boma Airport", "Boma", "CD", false], - "BOB": ["Bora Bora Airport", "Motu Mute", "PF", true], - "BOC": ["Bocas del Toro \"Isla Colón\" International Airport", "Isla Colón", "PA", true], - "BOD": ["Bordeaux–Mérignac Airport", "Bordeaux", "FR", true], - "BOE": ["Boundji Airport", "Boundji", "CG", false], - "BOG": ["El Dorado International Airport", "Bogota", "CO", true], - "BOH": ["Bournemouth Airport", "Bournemouth", "GB", true], - "BOI": ["Boise Air Terminal/Gowen Field", "Boise", "US", true], - "BOJ": ["Burgas Airport", "Burgas", "BG", true], - "BOK": ["Brookings Airport", "Brookings", "US", false], - "BOM": ["Chhatrapati Shivaji Maharaj International Airport", "Mumbai", "IN", true], - "BON": ["Flamingo International Airport", "Kralendijk", "BQ", true], - "BOO": ["Bodø Airport", "Bodø", "NO", true], - "BOP": ["Bouar Airport", "Bouar", "CF", false], - "BOR": ["Bokeo International Airport", "Ton Phueng", "LA", true], - "BOS": ["Boston Logan International Airport", "Boston", "US", true], - "BOT": ["Bosset Airport", "Bosset", "PG", true], - "BOU": ["Bourges airport", "Bourges", "FR", false], - "BOV": ["Boang Airport", "Boang Island", "PG", false], - "BOW": ["Bartow Executive Airport", "Bartow", "US", false], - "BOX": ["Borroloola Airport", "", "AU", false], - "BOY": ["Bobo Dioulasso Airport", "Bobo Dioulasso", "BF", true], - "BOZ": ["Bozoum Airport", "Bozoum", "CF", false], - "BPA": ["Borongan Airport", "Borongan City", "PH", false], - "BPC": ["Bamenda Airport", "Bamenda", "CM", false], - "BPD": ["Bapi Airstrip", "Bapi", "PG", false], - "BPE": ["Qinhuangdao Beidaihe Airport", "Qinhuangdao (Changli)", "CN", true], - "BPF": ["Batuna Aerodrome", "Batuna Mission Station", "SB", false], - "BPG": ["Barra do Garças Airport", "Barra do Garças", "BR", false], - "BPH": ["Bislig Airport", "Bislig", "PH", false], - "BPI": ["Miley Memorial Field", "Big Piney", "US", false], - "BPK": ["Biangabip Airport", "Biangabip", "PG", false], - "BPL": ["Bole Alashankou Airport", "Bole", "CN", true], - "BPM": ["Begumpet Airport", "Hyderabad", "IN", false], - "BPN": ["Sultan Aji Muhammad Sulaiman Sepinggan International Airport", "Balikpapan", "ID", true], - "BPS": ["Porto Seguro International Airport", "Porto Seguro", "BR", true], - "BPT": ["Jack Brooks Regional Airport", "Beaumont/Port Arthur", "US", true], - "BPX": ["Qamdo Bangda Airport", "Bangda", "CN", true], - "BPY": ["Besalampy Airport", "Besalampy", "MG", true], - "BQA": ["Dr. Juan C. Angara Airport", "Baler", "PH", false], - "BQB": ["Busselton Margaret River Regional Airport", "Busselton", "AU", true], - "BQE": ["Bubaque Airport", "Bubaque", "GW", false], - "BQG": ["Bogorodskoye Airport", "Bogorodskoye", "RU", true], - "BQH": ["London Biggin Hill Airport", "London", "GB", false], - "BQI": ["Bagani Airport", "Bagani", "NA", false], - "BQJ": ["Batagay Airport", "Batagay", "RU", true], - "BQK": ["Brunswick Golden Isles Airport", "Brunswick", "US", true], - "BQL": ["Boulia Airport", "", "AU", true], - "BQN": ["Rafael Hernández International Airport", "Aguadilla", "PR", true], - "BQO": ["Bouna Airport", "Bouna", "CI", false], - "BQQ": ["Barra Airport", "Barra", "BR", false], - "BQR": ["Maulana Prins Mandapar Airstrip", "Kendek", "ID", false], - "BQS": ["Ignatyevo Airport", "Blagoveschensk", "RU", true], - "BQT": ["Brest International Airport", "Brest", "BY", true], - "BQU": ["J F Mitchell Airport", "Bequia", "VC", true], - "BQV": ["Bartlett Cove Seaplane Base", "Gustavus", "US", false], - "BQW": ["Balgo Hill Airport", "Balgo", "AU", false], - "BRA": ["Dom Ricardo Weberberger Airport", "Barreiras", "BR", true], - "BRB": ["Barreirinhas National Airport", "Barreirinhas", "BR", false], - "BRC": ["Teniente Luis Candelaria International Airport", "San Carlos de Bariloche", "AR", true], - "BRD": ["Brainerd Lakes Regional Airport", "Brainerd", "US", true], - "BRE": ["Bremen Airport", "Bremen", "DE", true], - "BRG": ["Siau/Sitaro Airport", "Balirangen", "ID", false], - "BRH": ["Brahman Airstrip", "Brahman Mission", "PG", false], - "BRI": ["Bari Karol Wojtyła International Airport", "Bari", "IT", true], - "BRK": ["Bourke Airport", "", "AU", true], - "BRL": ["Southeast Iowa Regional Airport", "Burlington", "US", true], - "BRM": ["Jacinto Lara International Airport", "Barquisimeto", "VE", true], - "BRN": ["Bern Airport", "Bern", "CH", true], - "BRO": ["Brownsville South Padre Island International Airport", "Brownsville", "US", true], - "BRP": ["Biaru Airport", "Biaru", "PG", false], - "BRQ": ["Brno-Tuřany Airport", "Brno", "CZ", true], - "BRR": ["Barra Airport", "Eoligarry", "GB", true], - "BRS": ["Bristol Airport", "Bristol", "GB", true], - "BRT": ["Bathurst Island Airport", "Wurrumiyanga", "AU", false], - "BRU": ["Brussels Airport", "Zaventem", "BE", true], - "BRW": ["Wiley Post Will Rogers Memorial Airport", "Utqiaġvik", "US", true], - "BRX": ["Maria Montez International Airport", "Barahona", "DO", true], - "BRY": ["Samuels Field", "Bardstown", "US", false], - "BSA": ["Bender Qassim International Airport", "Bosaso", "SO", true], - "BSB": ["Presidente Juscelino Kubitschek International Airport", "Brasília", "BR", true], - "BSC": ["José Celestino Mutis Airport", "Bahía Solano", "CO", true], - "BSD": ["Baoshan Yunrui Airport", "Baoshan (Longyang)", "CN", true], - "BSE": ["Sematan Airport", "Sematan", "MY", false], - "BSF": ["Bradshaw Army Airfield", "Waimea (Kamuela)", "US", false], - "BSG": ["Bata International Airport", "Bata", "GQ", true], - "BSI": ["Balesin Island Airport", "Balesin", "PH", false], - "BSJ": ["Bairnsdale Airport", "Bairnsdale", "AU", false], - "BSK": ["Biskra - Mohamed Khider Airport", "Biskra", "DZ", true], - "BSL": ["EuroAirport Basel–Mulhouse–Freiburg", "Bâle / Mulhouse", "FR", true], - "BSM": ["Bishe Kola Air Base", "Amol", "IR", false], - "BSN": ["Bossangoa Airport", "Bossangoa", "CF", false], - "BSO": ["Basco Airport", "Basco", "PH", true], - "BSP": ["Bensbach Airport", "Bensbach", "PG", false], - "BSQ": ["Bisbee Municipal Airport", "Bisbee", "US", false], - "BSR": ["Basra International Airport", "Basra", "IQ", true], - "BSS": ["Balsas Airport", "Balsas", "BR", false], - "BST": ["Bost Airport", "Lashkar Gah", "AF", false], - "BSU": ["Basankusu Airport", "Basankusu", "CD", false], - "BSV": ["Besakoa Airport", "Besakoa", "MG", false], - "BSW": ["Boswell Bay Airport", "Boswell Bay", "US", false], - "BSX": ["Pathein Airport", "Pathein", "MM", true], - "BSY": ["Baardheere Airport", "Baardheere", "SO", false], - "BSZ": ["Manas International Airport", "Bishkek", "KG", true], - "BTA": ["Bertoua Airport", "Bertoua", "CM", false], - "BTB": ["Betou Airport", "Betou", "CG", false], - "BTC": ["Batticaloa International Airport", "Batticaloa", "LK", true], - "BTD": ["Brunette Downs Airport", "", "AU", false], - "BTF": ["Skypark Airport", "Bountiful", "US", false], - "BTG": ["Batangafo Airport", "Batangafo", "CF", false], - "BTH": ["Hang Nadim International Airport", "Batam", "ID", true], - "BTI": ["Barter Island Long Range Radar Station Airport", "Barter Island", "US", true], - "BTJ": ["Sultan Iskandar Muda International Airport", "Banda Aceh", "ID", true], - "BTK": ["Bratsk Airport", "Bratsk", "RU", true], - "BTL": ["Battle Creek Executive Airport at Kellogg Field", "Battle Creek", "US", false], - "BTM": ["Bert Mooney Airport", "Butte", "US", true], - "BTN": ["Marlboro County Jetport - H E Avent Field Airport", "Bennettsville", "US", false], - "BTO": ["Botopasi Airport", "Botopasi", "SR", false], - "BTP": ["Pittsburgh/Butler Regional Airport", "Butler", "US", false], - "BTQ": ["Butare Airport", "Butare", "RW", false], - "BTR": ["Baton Rouge Metropolitan Airport", "Baton Rouge", "US", true], - "BTS": ["M. R. Štefánik Airport", "Bratislava", "SK", true], - "BTT": ["Bettles Airport", "Bettles", "US", true], - "BTU": ["Bintulu Airport", "Bintulu", "MY", true], - "BTV": ["Patrick Leahy Burlington International Airport", "Burlington", "US", true], - "BTW": ["Bersujud Airport", "Batu Licin", "ID", true], - "BTX": ["Betoota Airport", "", "AU", false], - "BTY": ["Beatty Airport", "Beatty", "US", false], - "BTZ": ["Betong International Airport", "Betong", "TH", false], - "BUA": ["Buka Airport", "Buka Island", "PG", true], - "BUB": ["Cram Field", "Burwell", "US", false], - "BUC": ["Burketown Airport", "", "AU", true], - "BUD": ["Budapest Liszt Ferenc International Airport", "Budapest", "HU", true], - "BUF": ["Buffalo Niagara International Airport", "Buffalo", "US", true], - "BUG": ["Benguela Airport", "Benguela", "AO", false], - "BUI": ["Bokondini Airport", "Bokondini", "ID", true], - "BUJ": ["Bou Saada Airport", "Ouled Sidi Brahim", "DZ", false], - "BUK": ["Al-Bough Airport", "Al-Bough", "YE", false], - "BUL": ["Bulolo Airport", "Bulolo", "PG", false], - "BUM": ["Butler Memorial Airport", "Butler", "US", false], - "BUN": ["Gerardo Tobar López Airport", "Buenaventura", "CO", true], - "BUO": ["Burao Airport", "Burao", "SO", false], - "BUP": ["Bhatinda Air Force Station", "", "IN", false], - "BUQ": ["Joshua Mqabuko Nkomo International Airport", "Bulawayo", "ZW", true], - "BUR": ["Hollywood Burbank/Bob Hope Airport", "Burbank", "US", true], - "BUS": ["Alexander Kartveli Batumi International Airport", "Batumi", "GE", true], - "BUT": ["Bathpalathang Airport", "Jakar", "BT", true], - "BUU": ["Muara Bungo Airport", "Muara Bungo", "ID", true], - "BUV": ["Placeres Airport", "Bella Union", "UY", false], - "BUW": ["Betoambari Airport", "Bau Bau", "ID", false], - "BUX": ["Bunia Airport", "Bunia", "CD", true], - "BUY": ["Bunbury Airport", "", "AU", false], - "BUZ": ["Bushehr Airport", "Bushehr", "IR", true], - "BVA": ["Beauvais-Tillé airport", "Beauvais", "FR", true], - "BVB": ["Atlas Brasil Cantanhede International Airport", "Boa Vista", "BR", true], - "BVC": ["Aristides Pereira International Airport", "Rabil", "CV", true], - "BVE": ["Brive Souillac airport", "Brive", "FR", true], - "BVG": ["Berlevåg Airport", "Berlevåg", "NO", true], - "BVH": ["Brigadeiro Camarão Airport", "Vilhena", "BR", true], - "BVI": ["Birdsville Airport", "", "AU", true], - "BVJ": ["Bovanenkovo Airport", "Bovanenkovo", "RU", true], - "BVK": ["Huacaraje Airport", "Itenes", "BO", false], - "BVL": ["Baures Airport", "Baures", "BO", false], - "BVM": ["Belmonte Airport", "Belmonte", "BR", false], - "BVO": ["Bartlesville Municipal Airport", "Bartlesville", "US", false], - "BVS": ["Breves Airport", "Breves", "BR", true], - "BVU": ["Beluga Airport", "Beluga", "US", false], - "BVV": ["Burevestnik Airport", "Kurilsk", "RU", false], - "BVW": ["Batavia Downs Airport", "", "AU", false], - "BVX": ["Batesville Regional Airport", "Batesville", "US", false], - "BVY": ["Beverly Regional Airport", "Beverly / Danvers", "US", false], - "BVZ": ["Beverley Springs Airport", "Wunaamin Miliwundi Ranges", "AU", false], - "BWA": ["Gautam Buddha International Airport", "Siddharthanagar (Bhairahawa)", "NP", true], - "BWB": ["Barrow Island Airport", "", "AU", false], - "BWC": ["Brawley Municipal Airport", "Brawley", "US", false], - "BWD": ["Brownwood Regional Airport", "Brownwood", "US", false], - "BWE": ["Braunschweig-Wolfsburg Airport", "Braunschweig", "DE", false], - "BWF": ["Barrow Walney Island Airport", "Barrow-in-Furness", "GB", false], - "BWG": ["Bowling Green Warren County Regional Airport", "Bowling Green", "US", false], - "BWH": ["RMAF Butterworth Air Base", "Butterworth", "MY", false], - "BWI": ["Baltimore/Washington International Thurgood Marshall Airport", "Baltimore", "US", true], - "BWJ": ["Bawan Airport", "", "PG", false], - "BWK": ["Brač Airport", "Gornji Humac", "HR", true], - "BWL": ["Blackwell Tonkawa Municipal Airport", "Blackwell", "US", false], - "BWM": ["Bowman Regional Airport", "Bowman", "US", false], - "BWN": ["Brunei International Airport", "Bandar Seri Begawan", "BN", true], - "BWO": ["Balakovo Airport", "Balakovo", "RU", true], - "BWP": ["Bewani Airport", "Bewani", "PG", false], - "BWQ": ["Brewarrina Airport", "", "AU", false], - "BWT": ["Wynyard Airport", "Burnie", "AU", true], - "BWU": ["Sydney Bankstown Airport", "Sydney", "AU", false], - "BWW": ["Las Brujas Airport", "Cayo Santa Maria", "CU", false], - "BWX": ["Banyuwangi Airport", "Rogojampi, Banyuwangi", "ID", true], - "BXA": ["George R Carr Memorial Air Field", "Bogalusa", "US", false], - "BXB": ["Babo Airport", "Babo", "ID", false], - "BXD": ["Bade Airport", "Bade", "ID", false], - "BXE": ["Bakel Airport", "Bakel", "SN", false], - "BXF": ["Bellburn Airstrip", "Pumululu National Park", "AU", false], - "BXG": ["Bendigo Airport", "", "AU", true], - "BXH": ["Balkhash Airport", "Balkhash", "KZ", true], - "BXI": ["Boundiali Airport", "Boundiali", "CI", false], - "BXJ": ["Boralday Airport", "Boralday", "KZ", false], - "BXK": ["Buckeye Municipal Airport", "Buckeye", "US", false], - "BXL": ["Blue Lagoon Seaplane Base", "Nanuya Lailai Island", "FJ", false], - "BXM": ["Batom Airport", "Batom", "ID", false], - "BXN": ["Bodrum-Imsık Airport", "Bodrum", "TR", false], - "BXO": ["Buochs Airport", "Buochs", "CH", false], - "BXR": ["Bam Airport", "Bam", "IR", true], - "BXS": ["Borrego Valley Airport", "Borrego Springs", "US", false], - "BXT": ["LNG Badak Airport", "Bontang-Borneo Island", "ID", true], - "BXU": ["Bancasi Airport", "Butuan", "PH", true], - "BXV": ["Breiðdalsvík Airport", "Breiðdalsvík", "IS", false], - "BXW": ["Harun Thohir Airport", "Bawean", "ID", false], - "BXX": ["Borama Airport", "Borama", "SO", false], - "BXY": ["Baikonur Krayniy International Airport", "Baikonur", "KZ", true], - "BXZ": ["Bunsil Airport", "Bunsil - Umboi Island", "PG", false], - "BYA": ["Boundary Airport", "Boundary", "US", false], - "BYB": ["Dibba Airport", "Dibba al Baya", "OM", false], - "BYC": ["Yacuiba Airport", "Yacuíba", "BO", false], - "BYD": ["Al-Bayda Airport", "Al-Bayda", "YE", false], - "BYF": ["Albert Bray airport", "Albert", "FR", false], - "BYG": ["Johnson County Airport", "Buffalo", "US", false], - "BYH": ["Arkansas International Airport", "Blytheville", "US", false], - "BYI": ["Burley Municipal Airport", "Burley", "US", false], - "BYJ": ["Beja Airport / Airbase", "Beja", "PT", false], - "BYK": ["Bouaké Airport", "Bouaké", "CI", true], - "BYL": ["Bella Yella Airport", "Beliyela", "LR", false], - "BYM": ["Carlos Manuel de Cespedes Airport", "Bayamo", "CU", true], - "BYN": ["Bayankhongor Airport", "Bayankhongor", "MN", true], - "BYO": ["Bonito Airport", "Bonito", "BR", true], - "BYP": ["Barimunya Airport", "", "AU", false], - "BYR": ["Læsø Airport", "Læsø", "DK", true], - "BYS": ["Bicycle Lake Army Air Field", "Fort Irwin/Barstow", "US", false], - "BYT": ["Bantry Aerodrome", "Bantry", "IE", false], - "BYU": ["Bayreuth Airport", "Bindlach", "DE", false], - "BYV": ["Beira Lake Seaplane Base", "Colombo", "LK", false], - "BYW": ["Blakely Island Airport", "Blakely Island", "US", true], - "BYX": ["Baniyala Airport", "Baniyala", "AU", false], - "BZA": ["San Pedro Airport", "Bonanza", "NI", false], - "BZB": ["Bazaruto Island Airport", "Bazaruto Island", "MZ", false], - "BZC": ["Umberto Modiano Airport", "Cabo Frio", "BR", false], - "BZD": ["Balranald Airport", "", "AU", false], - "BZE": ["Philip S. W. Goldson International Airport", "Belize City", "BZ", true], - "BZF": ["Benton Field", "Redding", "US", false], - "BZG": ["Ignacy Jan Paderewski Bydgoszcz Airport", "Bydgoszcz", "PL", true], - "BZH": ["Bumi Hills Airport", "Bumi", "ZW", false], - "BZI": ["Balıkesir Airport", "Balıkesir", "TR", true], - "BZJ": ["Bozhou Airport (under construction)", "Bozhou", "CN", false], - "BZK": ["Bryansk International Airport", "Bryansk", "RU", false], - "BZL": ["Barisal Airport", "Barisal", "BD", true], - "BZM": ["Bemolanga Airport", "Bemolanga", "MG", false], - "BZN": ["Bozeman Yellowstone International Airport", "Bozeman", "US", true], - "BZO": ["Bolzano Airport", "Bolzano (BZ)", "IT", true], - "BZP": ["Bizant Airport", "Lakefield National Park", "AU", false], - "BZR": ["Béziers Vias airport", "Béziers", "FR", true], - "BZT": ["Eagle Air Park", "Brazoria", "US", false], - "BZU": ["Buta Zega Airport", "Buta", "CD", false], - "BZV": ["Maya-Maya International Airport", "Brazzaville", "CG", true], - "BZX": ["Bazhong Enyang Airport", "Bazhong", "CN", true], - "BZY": ["Bălți-Leadoveni International Airport", "Bălți", "MD", false], - "BZZ": ["RAF Brize Norton", "Carterton, Oxfordshire", "GB", false], - "CAA": ["El Aguacate Airport", "El Aguacate", "HN", false], - "CAB": ["Cabinda Airport", "Cabinda", "AO", true], - "CAC": ["Coronel Adalberto Mendes da Silva Airport", "Cascavel", "BR", true], - "CAD": ["Wexford County Airport", "Cadillac", "US", false], - "CAE": ["Columbia Metropolitan Airport", "Columbia", "US", true], - "CAF": ["Carauari Airport", "Carauari", "BR", true], - "CAG": ["Cagliari Elmas Airport", "Cagliari", "IT", true], - "CAH": ["Cà Mau Airport", "Ca Mau City", "VN", true], - "CAI": ["Cairo International Airport", "Cairo", "EG", true], - "CAJ": ["Canaima Airport", "Canaima", "VE", true], - "CAK": ["Akron Canton Regional Airport", "Akron", "US", true], - "CAL": ["Campbeltown Airport", "Campbeltown", "GB", true], - "CAM": ["Camiri Airport", "Camiri", "BO", false], - "CAN": ["Guangzhou Baiyun International Airport", "Guangzhou (Huadu)", "CN", true], - "CAO": ["Clayton Municipal Airpark", "Clayton", "US", false], - "CAP": ["Cap Haitien International Airport", "Cap Haitien", "HT", true], - "CAQ": ["Juan H White Airport", "Caucasia", "CO", false], - "CAR": ["Caribou Municipal Airport", "Caribou", "US", false], - "CAT": ["Cascais Airport", "Cascais", "PT", true], - "CAU": ["Caruaru Airport", "Caruaru", "BR", true], - "CAV": ["Cazombo Airport", "Cazombo", "AO", false], - "CAW": ["Bartolomeu Lisandro Airport", "Campos dos Goytacazes", "BR", true], - "CAX": ["Carlisle Lake District Airport", "Carlisle, Cumbria", "GB", false], - "CAY": ["Cayenne – Félix Eboué Airport", "Matoury", "GF", true], - "CAZ": ["Cobar Airport", "", "AU", true], - "CBA": ["Alpine Heliport", "West Kelowna", "CA", false], - "CBB": ["Jorge Wilsterman International Airport", "Cochabamba", "BO", true], - "CBC": ["Cherrabun Airport", "", "AU", false], - "CBD": ["Car Nicobar Air Force Base", "IAF Camp", "IN", false], - "CBE": ["Greater Cumberland Regional Airport", "Wiley Ford", "US", false], - "CBF": ["Council Bluffs Municipal Airport", "Council Bluffs", "US", false], - "CBG": ["Cambridge City Airport", "Cambridge, Cambridgeshire", "GB", false], - "CBH": ["Béchar Boudghene Ben Ali Lotfi Airport", "Béchar", "DZ", true], - "CBI": ["Cape Barren Island Airport", "Cape Barren Island", "AU", false], - "CBJ": ["Cabo Rojo Airport", "Cabo Rojo", "DO", false], - "CBK": ["Shalz Field", "Colby", "US", false], - "CBL": ["General Tomas de Heres Airport", "Ciudad Bolivar", "VE", false], - "CBM": ["Columbus Air Force Base", "Columbus", "US", false], - "CBN": ["Cakrabhuwana Airport", "Cirebon-Java Island", "ID", false], - "CBO": ["Cotabato (Awang) Airport", "Datu Odin Sinsuat", "PH", true], - "CBQ": ["Margaret Ekpo International Airport", "Calabar", "NG", true], - "CBR": ["Canberra Airport", "Canberra", "AU", true], - "CBS": ["Oro Negro Airport", "Cabimas", "VE", false], - "CBT": ["Catumbela Airport", "Catumbela", "AO", true], - "CBV": ["Coban Airport", "Coban", "GT", false], - "CBW": ["Campo Mourão Airport", "Campo Mourão", "BR", false], - "CBX": ["Condobolin Airport", "", "AU", false], - "CBY": ["Canobie Airport", "Canobie", "AU", false], - "CBZ": ["Binzhou Dagao General Aviation Airport", "Binzhou", "CN", false], - "CCA": ["Chimore Airport", "Chimore", "BO", true], - "CCB": ["Upland-Cable Airport", "Upland", "US", false], - "CCC": ["Jardines Del Rey Airport", "Cayo Coco", "CU", true], - "CCD": ["Century City Heliport", "Los Angeles", "US", false], - "CCE": ["Capital International Airport", "New Cairo", "EG", true], - "CCF": ["Carcassonne Salvaza Airport", "Carcassonne", "FR", true], - "CCG": ["Crane County Airport", "Crane", "US", false], - "CCH": ["Chile Chico Airport", "Chile Chico", "CL", false], - "CCI": ["Olavo Cecco Rigon Airport", "Concórdia", "BR", false], - "CCJ": ["Calicut International Airport", "Calicut", "IN", true], - "CCK": ["Cocos (Keeling) Islands Airport", "West Island", "CC", true], - "CCL": ["Chinchilla Airport", "Chinchilla", "AU", false], - "CCM": ["Forquilhinha - Criciúma Airport", "Criciúma", "BR", false], - "CCN": ["Chaghcharan Airport", "Chaghcharan", "AF", false], - "CCO": ["Carimagua Airport", "Puerto López", "CO", false], - "CCP": ["Carriel Sur International Airport", "Concepcion", "CL", true], - "CCR": ["Buchanan Field", "Concord", "US", true], - "CCS": ["Maiquetía Simón Bolívar International Airport", "Maiquetía", "VE", true], - "CCU": ["Netaji Subhash Chandra Bose International Airport", "Kolkata", "IN", true], - "CCV": ["Craig Cove Airport", "Craig Cove", "VU", true], - "CCW": ["Cowell Airport", "", "AU", false], - "CCX": ["Cáceres Airport", "Cáceres", "BR", false], - "CCY": ["Northeast Iowa Regional Airport", "Charles City", "US", false], - "CCZ": ["Chub Cay Airport", "Chub Cay", "BS", true], - "CDA": ["Cooinda Airport", "", "AU", false], - "CDB": ["Cold Bay Airport", "Cold Bay", "US", true], - "CDC": ["Cedar City Regional Airport", "Cedar City", "US", true], - "CDD": ["Cauquira Airport", "Cauquira", "HN", false], - "CDE": ["Chengde Puning Airport", "Chengde", "CN", true], - "CDG": ["Charles de Gaulle International Airport", "Paris (Roissy-en-France, Val-d'Oise)", "FR", true], - "CDH": ["Harrell Field", "Camden", "US", false], - "CDI": ["Cachoeiro do Itapemirim - Raimundo de Andrade Airport", "Cachoeiro do Itapemirim", "BR", false], - "CDJ": ["Conceição do Araguaia Airport", "Conceição do Araguaia", "BR", false], - "CDK": ["George T Lewis Airport", "Cedar Key", "US", false], - "CDL": ["Candle 2 Airport", "Candle", "US", false], - "CDN": ["Woodward Field", "Camden", "US", false], - "CDO": ["Cradock Airport", "Cradock", "ZA", false], - "CDP": ["Kadapa Airport", "Kadapa", "IN", true], - "CDQ": ["Croydon Airport", "", "AU", false], - "CDR": ["Chadron Municipal Airport", "Chadron", "US", true], - "CDS": ["Childress Municipal Airport", "Childress", "US", false], - "CDT": ["Castellón-Costa Azahar Airport", "Castellón de la Plana", "ES", true], - "CDU": ["Camden Airport", "Cobbitty", "AU", false], - "CDV": ["Merle K (Mudhole) Smith Airport", "Cordova", "US", true], - "CDW": ["Essex County Airport", "Caldwell", "US", false], - "CDY": ["Cagayan de Sulu Airport", "Mapun", "PH", false], - "CEA": ["Cessna Aircraft Field", "Wichita", "US", false], - "CEB": ["Mactan Cebu International Airport", "Cebu City/Lapu-Lapu City", "PH", true], - "CEC": ["Jack Mc Namara Field Airport", "Crescent City", "US", true], - "CED": ["Ceduna Airport", "", "AU", true], - "CEE": ["Cherepovets Airport", "Cherepovets", "RU", true], - "CEF": ["Westover Metropolitan Airport / Westover Air Reserve Base", "Chicopee", "US", false], - "CEG": ["Hawarden Airport", "Broughton", "GB", false], - "CEH": ["Chelinda Airfield", "", "MW", false], - "CEI": ["Mae Fah Luang - Chiang Rai International Airport", "Chiang Rai", "TH", true], - "CEK": ["Kurchatov Chelyabinsk International Airport", "Chelyabinsk", "RU", true], - "CEL": ["Canela Airport", "Canela", "BR", true], - "CEM": ["Central Airport", "Central", "US", true], - "CEN": ["Ciudad Obregón International Airport", "Ciudad Obregón", "MX", true], - "CEO": ["Waco Kungo Airport", "Waco Kungo", "AO", false], - "CEP": ["Concepción Airport", "Concepción", "BO", false], - "CEQ": ["Cannes Mandelieu Airport", "Cannes", "FR", false], - "CER": ["Cherbourg Manche airport", "Cherbourg", "FR", false], - "CES": ["Cessnock Airport", "", "AU", false], - "CET": ["Cholet Le Pontreau airfield", "Cholet", "FR", false], - "CEU": ["Oconee County Regional Airport", "Clemson", "US", false], - "CEV": ["Mettel Field", "Connersville", "US", false], - "CEW": ["Bob Sikes Airport", "Crestview", "US", false], - "CEX": ["Chena Hot Springs Airport", "Chena Hot Springs", "US", false], - "CEY": ["Murray-Calloway County Airport Kyle-Oakley Field", "Murray", "US", false], - "CEZ": ["Cortez Municipal Airport", "Cortez", "US", true], - "CFB": ["Cabo Frio Airport", "Cabo Frio", "BR", true], - "CFC": ["Doctor Carlos Alberto da Costa Neves Airport", "Caçador", "BR", false], - "CFD": ["Coulter Field", "Bryan", "US", false], - "CFE": ["Clermont-Ferrand Auvergne airport", "Clermont-Ferrand", "FR", true], - "CFF": ["Cafunfo Airport", "Cafunfo", "AO", false], - "CFG": ["Jaime Gonzalez Airport", "Cienfuegos", "CU", true], - "CFH": ["Clifton Hills Airport", "Clifton Hills", "AU", false], - "CFI": ["Camfield Airport", "", "AU", false], - "CFK": ["Chlef Aboubakr Belkaid International Airport", "Chlef", "DZ", true], - "CFM": ["Conklin (Leismer) Airport", "Conklin", "CA", false], - "CFN": ["Donegal Airport", "Donegal", "IE", true], - "CFO": ["Confresa Airport", "Confresa", "BR", false], - "CFP": ["Carpentaria Downs Airport", "Carpentaria Downs", "AU", false], - "CFQ": ["Creston Valley Regional Airport - Art Sutcliffe Field", "Creston", "CA", false], - "CFR": ["Caen Carpiquet airport", "Caen", "FR", true], - "CFS": ["Coffs Harbour Airport", "Coffs Harbour", "AU", true], - "CFT": ["Greenlee County Airport", "Clifton", "US", false], - "CFU": ["Corfu Ioannis Kapodistrias International Airport", "Kerkyra (Corfu)", "GR", true], - "CFV": ["Coffeyville Municipal Airport", "Coffeyville", "US", false], - "CGA": ["Craig Seaplane Base", "Craig", "US", true], - "CGB": ["Várzea Grande–Marechal Rondon International Airport", "Cuiabá", "BR", true], - "CGC": ["Cape Gloucester Airport", "Cape Gloucester", "PG", false], - "CGD": ["Changde Taohuayuan Airport", "Changde (Dingcheng)", "CN", true], - "CGE": ["Cambridge Dorchester Airport", "Cambridge", "US", false], - "CGF": ["Cuyahoga County Airport", "Cleveland", "US", false], - "CGG": ["Casiguran Airport", "Casiguran", "PH", false], - "CGH": ["Congonhas–Deputado Freitas Nobre Airport", "São Paulo", "BR", true], - "CGI": ["Cape Girardeau Regional Airport", "Cape Girardeau", "US", true], - "CGJ": ["Kasompe Airport", "Chingola", "ZM", false], - "CGK": ["Soekarno-Hatta International Airport", "Jakarta", "ID", true], - "CGL": ["Chagual Airport", "", "PE", false], - "CGM": ["Camiguin Airport", "Mambajao", "PH", true], - "CGN": ["Cologne Bonn Airport", "Köln (Cologne)", "DE", true], - "CGO": ["Zhengzhou Xinzheng International Airport", "Zhengzhou", "CN", true], - "CGP": ["Shah Amanat International Airport", "Chattogram (Chittagong)", "BD", true], - "CGQ": ["Changchun Longjia International Airport", "Changchun", "CN", true], - "CGR": ["Campo Grande Airport", "Campo Grande", "BR", true], - "CGS": ["College Park Airport", "College Park", "US", false], - "CGV": ["Caiguna Airport", "", "AU", false], - "CGX": ["Chunga Airstrip", "Chunga", "ZM", false], - "CGY": ["Laguindingan International Airport", "Laguindingan", "PH", true], - "CGZ": ["Casa Grande Municipal Airport", "Casa Grande", "US", false], - "CHA": ["Chattanooga Metropolitan Airport (Lovell Field)", "Chattanooga", "US", true], - "CHB": ["Chilas Airport", "Chilas", "PK", false], - "CHC": ["Christchurch International Airport", "Christchurch", "NZ", true], - "CHD": ["Mucugê Airport", "Mucugê", "BR", false], - "CHE": ["Tallinn Linnahall Heliport", "Tallinn", "EE", true], - "CHF": ["Jinhae Air Base", "Jinhae", "KR", false], - "CHG": ["Chaoyang Airport", "Shuangta, Chaoyang", "CN", true], - "CHH": ["Chachapoyas Airport", "Chachapoyas", "PE", true], - "CHJ": ["Chipinge Airport", "Chipinge", "ZW", false], - "CHK": ["Chickasha Municipal Airport", "Chickasha", "US", false], - "CHL": ["Challis Airport", "Challis", "US", false], - "CHM": ["FAP Lieutenant Jaime Andres de Montreuil Morales Airport", "Chimbote", "PE", true], - "CHN": ["Jeonju (G 703) Air Base", "Jeonju", "KR", false], - "CHO": ["Charlottesville Albemarle Airport", "Charlottesville", "US", true], - "CHP": ["Circle Hot Springs Airport", "Circle Hot Springs", "US", false], - "CHQ": ["Chania International Airport", "Souda", "GR", true], - "CHR": ["Châteauroux Déols airport", "Châteauroux", "FR", false], - "CHS": ["Charleston International Airport", "Charleston", "US", true], - "CHT": ["Inia William Tuuta Memorial Airport", "Te One", "NZ", true], - "CHU": ["Chuathbaluk Airport", "Chuathbaluk", "US", true], - "CHX": ["Changuinola Captain Manuel Niño International Airport", "Changuinola", "PA", true], - "CHY": ["Choiseul Bay Airport", "Choiseul Bay", "SB", true], - "CHZ": ["Chiloquin State Airport", "Chiloquin", "US", false], - "CIA": ["Ciampino–G. B. Pastine International Airport", "Rome", "IT", true], - "CIC": ["Chico Regional Airport", "Chico", "US", false], - "CID": ["The Eastern Iowa Airport", "Cedar Rapids", "US", true], - "CIE": ["Collie Airport", "Collie", "AU", false], - "CIF": ["Chifeng Yulong Airport", "Chifeng", "CN", true], - "CIG": ["Craig Moffat Airport", "Craig", "US", false], - "CIH": ["Changzhi Wangcun Airport", "Changzhi", "CN", true], - "CII": ["Aydın Çıldır Airport", "Aydın", "TR", false], - "CIJ": ["Capitán Aníbal Arab Airport", "Cobija", "BO", true], - "CIK": ["Chalkyitsik Airport", "Chalkyitsik", "US", true], - "CIL": ["Council Airport", "Council", "US", false], - "CIM": ["Cimitarra Airport", "Cimitarra", "CO", false], - "CIN": ["Arthur N Neu Airport", "Carroll", "US", false], - "CIO": ["Lieutenant Colonel Carmelo Peralta National Airport", "Concepción", "PY", false], - "CIP": ["Chipata Airport", "Chipata", "ZM", false], - "CIQ": ["Chiquimula Airport", "Chiquimula", "GT", false], - "CIR": ["Cairo Regional Airport", "Cairo", "US", false], - "CIS": ["Canton Island Airport", "Abariringa", "KI", false], - "CIT": ["Shymkent International Airport", "Shymkent", "KZ", true], - "CIU": ["Chippewa County International Airport", "Kincheloe", "US", true], - "CIW": ["Canouan Airport", "Canouan", "VC", true], - "CIX": ["Capitán FAP José A. Quiñones González International Airport", "Chiclayo", "PE", true], - "CIY": ["Comiso Airport", "Comiso", "IT", true], - "CIZ": ["Danilson Municipal Airport", "Coari", "BR", false], - "CJA": ["Mayor General FAP Armando Revoredo Iglesias Airport", "Cajamarca", "PE", true], - "CJB": ["Coimbatore International Airport", "Coimbatore", "IN", true], - "CJC": ["El Loa Airport", "Calama", "CL", true], - "CJD": ["Candilejas Airport", "Candilejas", "CO", false], - "CJF": ["Coondewanna Airport", "Coondewanna", "AU", false], - "CJH": ["Chilko Lake (Tsylos Park Lodge) Airport", "Chilko Lake", "CA", false], - "CJJ": ["Cheongju International Airport/Cheongju Air Base (K-59/G-513)", "Cheongju", "KR", true], - "CJL": ["Chitral Airport", "Chitral", "PK", true], - "CJM": ["Chumphon Airport", "Chumphon", "TH", true], - "CJN": ["Nusawiru Airport", "Cijulang", "ID", true], - "CJS": ["Abraham González International Airport", "Ciudad Juárez", "MX", true], - "CJT": ["San Antonio Copalar Airport", "Comitán", "MX", false], - "CJU": ["Jeju International Airport", "Jeju City", "KR", true], - "CJZ": ["Pedro Vieira Moreira Airport", "Cajazeiras", "BR", true], - "CKA": ["Kegelman AF Aux Field", "Cherokee", "US", false], - "CKB": ["North Central West Virginia Airport", "Bridgeport", "US", true], - "CKC": ["Cherkasy International Airport", "Cherkasy", "UA", false], - "CKD": ["Crooked Creek Airport", "Crooked Creek", "US", true], - "CKE": ["Lampson Field", "Lakeport", "US", false], - "CKG": ["Chongqing Jiangbei International Airport", "Chongqing", "CN", true], - "CKH": ["Chokurdakh Airport", "Chokurdah", "RU", true], - "CKI": ["Croker Island Airport", "", "AU", false], - "CKK": ["Sharp County Regional Airport", "Ash Flat", "US", false], - "CKL": ["Chkalovskiy Air Base", "Moscow", "RU", false], - "CKM": ["Fletcher Field", "Clarksdale", "US", false], - "CKN": ["Crookston Municipal Kirkwood Field", "Crookston", "US", false], - "CKO": ["Cornélio Procópio Airport", "Cornélio Procópio", "BR", false], - "CKR": ["Crane Island Airstrip", "Crane Island", "US", false], - "CKS": ["Carajás Airport", "Parauapebas", "BR", true], - "CKT": ["Sarakhs Airport", "Sarakhs", "IR", false], - "CKU": ["Cordova Municipal Airport", "Cordova", "US", false], - "CKV": ["Clarksville–Montgomery County Regional Airport", "Clarksville", "US", false], - "CKW": ["Christmas Creek Airport", "Christmas Creek Mine", "AU", true], - "CKX": ["Chicken Airport", "Chicken", "US", true], - "CKY": ["Ahmed Sékou Touré International Airport", "Conakry", "GN", true], - "CKZ": ["Çanakkale Airport", "Çanakkale", "TR", true], - "CLA": ["Comilla Airport", "Comilla", "BD", false], - "CLD": ["McClellan-Palomar Airport", "Carlsbad", "US", true], - "CLE": ["Cleveland Hopkins International Airport", "Cleveland", "US", true], - "CLG": ["New Coalinga Municipal Airport", "Coalinga", "US", false], - "CLH": ["Coolah Airport", "", "AU", false], - "CLI": ["Clintonville Municipal Airport", "Clintonville", "US", false], - "CLJ": ["Avram Iancu Cluj International Airport", "Cluj-Napoca", "RO", true], - "CLK": ["Clinton Regional Airport", "Clinton", "US", false], - "CLL": ["Easterwood Field", "College Station", "US", true], - "CLM": ["William R Fairchild International Airport", "Port Angeles", "US", false], - "CLN": ["Brig. Lysias Augusto Rodrigues Airport", "Carolina", "BR", false], - "CLO": ["Alfonso Bonilla Aragon International Airport", "Cali", "CO", true], - "CLP": ["Clarks Point Airport", "Clarks Point", "US", true], - "CLQ": ["Licenciado Miguel de la Madrid International Airport", "Colima", "MX", true], - "CLR": ["Cliff Hatfield Memorial Airport", "Calipatria", "US", false], - "CLS": ["Chehalis Centralia Airport", "Chehalis", "US", false], - "CLT": ["Charlotte Douglas International Airport", "Charlotte", "US", true], - "CLU": ["Columbus Municipal Airport", "Columbus", "US", false], - "CLV": ["Nelson Ribeiro Guimarães Airport", "Caldas Novas", "BR", true], - "CLW": ["Clearwater Air Park", "Clearwater", "US", false], - "CLX": ["Aeródromo de Clorinda", "Clorinda", "AR", false], - "CLY": ["Calvi Sainte Catherine Airport", "Calvi", "FR", true], - "CLZ": ["Calabozo Airport", "Guarico", "VE", false], - "CMA": ["Cunnamulla Airport", "", "AU", true], - "CMB": ["Bandaranaike International Colombo Airport", "Colombo", "LK", true], - "CMC": ["Camocim - Pinto Martins Airport", "Camocim", "BR", false], - "CMD": ["Cootamundra Airport", "", "AU", false], - "CME": ["Ciudad del Carmen International Airport", "Ciudad del Carmen", "MX", true], - "CMF": ["Chambéry Aix les Bains airport", "Chambéry", "FR", true], - "CMG": ["Corumbá International Airport", "Corumbá", "BR", true], - "CMH": ["John Glenn Columbus International Airport", "Columbus", "US", true], - "CMI": ["University of Illinois Willard Airport", "Savoy", "US", true], - "CMJ": ["Qimei Airport", "Qimei", "TW", false], - "CMK": ["Club Makokola Airport", "Club Makokola", "MW", false], - "CML": ["Camooweal Airport", "", "AU", false], - "CMM": ["Carmelita Airport", "Carmelita", "GT", false], - "CMN": ["Mohammed V International Airport", "Casablanca", "MA", true], - "CMO": ["Hobyo Airport", "Hobyo", "SO", false], - "CMP": ["Santana do Araguaia Airport", "Santana do Araguaia", "BR", false], - "CMQ": ["Clermont Airport", "", "AU", false], - "CMR": ["Colmar Houssen airport", "Colmar", "FR", false], - "CMS": ["Iskushuban Airport", "Iskushuban", "SO", false], - "CMT": ["New Cametá Airport", "Cametá", "BR", false], - "CMU": ["Chimbu Airport", "Kundiawa", "PG", true], - "CMV": ["Coromandel Airport", "Coromandel", "NZ", false], - "CMW": ["Ignacio Agramonte International Airport", "Camaguey", "CU", true], - "CMX": ["Houghton County Memorial Airport", "Hancock", "US", true], - "CMY": ["Sparta Fort McCoy Airport", "Sparta", "US", false], - "CMZ": ["Caia Airport", "Caia", "MZ", false], - "CNA": ["Cananea National Airport", "Cananea", "MX", false], - "CNB": ["Coonamble Airport", "", "AU", true], - "CNC": ["Coconut Island Airport", "", "AU", true], - "CND": ["Mihail Kogălniceanu International Airport", "Constanța", "RO", true], - "CNE": ["Fremont County Airport", "Penrose", "US", false], - "CNF": ["Tancredo Neves International Airport", "Belo Horizonte", "BR", true], - "CNG": ["Cognac-Châteaubernard (BA 709) Air Base", "Cognac/Châteaubernard", "FR", false], - "CNH": ["Claremont Municipal Airport", "Claremont", "US", false], - "CNI": ["Changhai Dachangshandao Airport", "Dalian (Changhai)", "CN", true], - "CNJ": ["Cloncurry Airport", "Cloncurry", "AU", true], - "CNK": ["Blosser Municipal Airport", "Concordia", "US", false], - "CNL": ["Sindal Airport", "Sindal", "DK", false], - "CNM": ["Cavern City Air Terminal", "Carlsbad", "US", true], - "CNN": ["Kannur International Airport", "Kannur", "IN", true], - "CNO": ["Chino Airport", "Chino", "US", false], - "CNP": ["Neerlerit Inaat Airport", "Neerlerit Inaat", "GL", true], - "CNQ": ["Corrientes Airport", "Corrientes", "AR", true], - "CNR": ["Chañaral Airport", "Chañaral", "CL", false], - "CNS": ["Cairns International Airport", "Cairns", "AU", true], - "CNT": ["Charata Airport", "Charata", "AR", false], - "CNU": ["Chanute Martin Johnson Airport", "Chanute", "US", false], - "CNV": ["Sócrates Rezende Airport", "Canavieiras", "BR", false], - "CNW": ["TSTC Waco Airport", "Waco", "US", false], - "CNX": ["Chiang Mai International Airport", "Chiang Mai", "TH", true], - "CNY": ["Canyonlands Regional Airport", "Moab", "US", true], - "CNZ": ["Cangamba Airport", "Cangamba", "AO", false], - "COA": ["Columbia Airport", "Columbia", "US", false], - "COB": ["Coolibah Airport", "Coolibah", "AU", false], - "COC": ["Comodoro Pierrestegui Airport", "Concordia", "AR", false], - "COD": ["Yellowstone Regional Airport", "Cody", "US", true], - "COE": ["Coeur D'Alene Airport - Pappy Boyington Field", "Coeur d'Alene", "US", false], - "COF": ["Patrick Space Force Base", "Cocoa Beach", "US", false], - "COG": ["Mandinga Airport", "Condoto", "CO", false], - "COH": ["Cooch Behar Airport", "Cooch Behar", "IN", false], - "COI": ["Merritt Island Airport", "Merritt Island", "US", false], - "COJ": ["Coonabarabran Airport", "", "AU", false], - "COK": ["Cochin International Airport", "Kochi", "IN", true], - "COL": ["Coll Airport", "Coll Island", "GB", true], - "COM": ["Coleman Municipal Airport", "Coleman", "US", false], - "CON": ["Concord Municipal Airport", "Concord", "US", false], - "COO": ["Cotonou Cadjehoun International Airport", "Cotonou", "BJ", true], - "COP": ["Cooperstown-Westville Airport", "Cooperstown", "US", false], - "COQ": ["Choibalsan Airport", "", "MN", true], - "COR": ["Ingeniero Aeronáutico Ambrosio L.V. Taravella International Airport", "Cordoba", "AR", true], - "COS": ["City of Colorado Springs Municipal Airport", "Colorado Springs", "US", true], - "COT": ["Cotulla-La Salle County Airport", "Cotulla", "US", false], - "COU": ["Columbia Regional Airport", "Columbia", "US", true], - "COV": ["Çukurova International Airport", "Tarsus", "TR", true], - "COW": ["Tambillos Airport", "Coquimbo", "CL", false], - "COY": ["Coolawanyah Airport", "Coolawanyah Station", "AU", false], - "COZ": ["Constanza - Expedición 14 de Junio National Airport", "Costanza", "DO", false], - "CPA": ["Cape Palmas Airport", "Harper", "LR", false], - "CPB": ["Capurganá Airport", "Acandí", "CO", false], - "CPC": ["Aviador C. Campos Airport", "Chapelco/San Martin de los Andes", "AR", true], - "CPD": ["Coober Pedy Airport", "Coober Pedy", "AU", true], - "CPE": ["Ingeniero Alberto Acuña Ongay International Airport", "Campeche", "MX", true], - "CPF": ["Ngloram Airport", "Blora", "ID", false], - "CPG": ["Carmen de Patagones Airport", "Carmen de Patagones", "AR", false], - "CPH": ["Copenhagen Kastrup Airport", "Copenhagen", "DK", true], - "CPI": ["Cape Orford Airport", "", "PG", false], - "CPL": ["General Navas Pardo Airport", "Chaparral", "CO", false], - "CPM": ["Compton Woodley Airport", "Compton", "US", false], - "CPN": ["Cape Rodney Airport", "Cape Rodney", "PG", false], - "CPO": ["Desierto de Atacama Airport", "Copiapo", "CL", true], - "CPP": ["Coposa Airport", "Pica", "CL", false], - "CPR": ["Casper-Natrona County International Airport", "Casper", "US", true], - "CPS": ["St Louis Downtown Airport", "Cahokia/St Louis", "US", false], - "CPT": ["Cape Town International Airport", "Cape Town", "ZA", true], - "CPU": ["Cururupu Airport", "Cururupu", "BR", false], - "CPV": ["Presidente João Suassuna Airport", "Campina Grande", "BR", true], - "CPX": ["Benjamin Rivera Noriega Airport", "Culebra", "PR", true], - "CQA": ["Canarana Airport", "Canarana", "BR", false], - "CQD": ["Shahrekord Airport", "Shahrekord", "IR", false], - "CQF": ["Calais Marck Airport", "Calais", "FR", false], - "CQM": ["Ciudad Real International Airport", "Ciudad Real", "ES", false], - "CQP": ["Cape Flattery Airport", "", "AU", false], - "CQS": ["Costa Marques Airport", "Costa Marques", "BR", false], - "CQT": ["Caquetania Airport", "Caquetania", "CO", false], - "CQW": ["Chongqing Xiannüshan Airport", "Wulong", "CN", true], - "CRA": ["Craiova International Airport", "Craiova", "RO", true], - "CRB": ["Collarenebri Airport", "", "AU", false], - "CRC": ["Santa Ana Airport", "Cartago", "CO", false], - "CRD": ["General Enrique Mosconi International Airport", "Comodoro Rivadavia", "AR", true], - "CRE": ["Grand Strand Airport", "North Myrtle Beach", "US", false], - "CRF": ["Carnot Airport", "Carnot", "CF", false], - "CRG": ["Jacksonville Executive at Craig Airport", "Jacksonville", "US", false], - "CRH": ["Cherrabah Airport", "Cherrabah Homestead Resort", "AU", false], - "CRI": ["Colonel Hill Airport", "Colonel Hill", "BS", true], - "CRJ": ["Coorabie Airport", "Coorabie", "AU", false], - "CRK": ["Clark International Airport / Clark Air Base", "Mabalacat", "PH", true], - "CRL": ["Brussels South Charleroi Airport", "Charleroi", "BE", true], - "CRM": ["Catarman National Airport", "Catarman", "PH", true], - "CRO": ["Corcoran Airport", "Corcoran", "US", false], - "CRP": ["Corpus Christi International Airport", "Corpus Christi", "US", true], - "CRQ": ["Caravelas Airport", "Caravelas", "BR", false], - "CRR": ["Ceres Airport", "Ceres", "AR", false], - "CRS": ["C David Campbell Field Corsicana Municipal Airport", "Corsicana", "US", false], - "CRT": ["Z M Jack Stell Field", "Crossett", "US", false], - "CRU": ["Lauriston Airport", "Carriacou Island", "GD", true], - "CRV": ["Crotone Sant'Anna Pythagoras Airport", "Isola di Capo Rizzuto (KR)", "IT", true], - "CRW": ["Yeager Airport", "Charleston", "US", true], - "CRX": ["Roscoe Turner Airport", "Corinth", "US", false], - "CRY": ["Carlton Hill Airport", "Carlton Hill", "AU", false], - "CRZ": ["Türkmenabat International Airport", "Türkmenabat", "TM", true], - "CSA": ["Colonsay Airstrip", "Colonsay", "GB", true], - "CSB": ["Caransebeş Airport", "Caransebeş", "RO", false], - "CSC": ["Mojica Airport", "Cañas", "CR", false], - "CSD": ["Cresswell Downs Airport", "Cresswell Downs", "AU", false], - "CSE": ["Crested Butte Airpark", "Crested Butte", "US", false], - "CSF": ["Creil Air Base", "Creil", "FR", false], - "CSG": ["Columbus Airport", "Columbus", "US", true], - "CSH": ["Solovki Airport", "Solovetsky Islands", "RU", true], - "CSI": ["Casino Airport", "", "AU", false], - "CSK": ["Cap Skirring Airport", "Cap Skirring", "SN", true], - "CSM": ["Clinton Sherman Airport", "Clinton", "US", false], - "CSN": ["Carson Airport", "Carson City", "US", false], - "CSO": ["Cochstedt Airport", "Hecklingen", "DE", false], - "CSQ": ["Creston Municipal Airport", "Creston", "US", false], - "CSR": ["Casuarito Airport", "Casuarito", "CO", false], - "CSS": ["Cassilândia Airport", "Cassilândia", "BR", false], - "CST": ["Castaway Island Seaplane Base", "Castaway Island", "FJ", false], - "CSU": ["Santa Cruz do Sul Airport", "Santa Cruz do Sul", "BR", false], - "CSV": ["Crossville Memorial Airport Whitson Field", "Crossville", "US", false], - "CSW": ["Cabo San Lucas International Airport", "Cabo San Lucas", "MX", true], - "CSX": ["Changsha Huanghua International Airport", "Changsha (Changsha)", "CN", true], - "CSY": ["Cheboksary Airport", "Cheboksary", "RU", true], - "CSZ": ["Brigadier D.H.E. Ruiz Airport", "Coronel Suarez", "AR", false], - "CTA": ["Catania-Fontanarossa Airport", "Catania", "IT", true], - "CTB": ["Cut Bank International Airport", "Cut Bank", "US", false], - "CTC": ["Coronel Felipe Varela International Airport", "Catamarca", "AR", true], - "CTD": ["Alonso Valderrama Airport", "Chitré", "PA", true], - "CTE": ["Cartí Airport", "Cartí Islands", "PA", false], - "CTF": ["Coatepeque Airport", "Coatepeque", "GT", false], - "CTG": ["Rafael Nuñez International Airport", "Cartagena", "CO", true], - "CTH": ["Chester County G O Carlson Airport", "Coatesville", "US", false], - "CTI": ["Cuito Cuanavale Airport", "Cuito Cuanavale", "AO", false], - "CTK": ["Canton Municipal Airport", "Canton", "US", false], - "CTL": ["Charleville Airport", "Charleville", "AU", true], - "CTM": ["Chetumal International Airport", "Chetumal", "MX", true], - "CTN": ["Cooktown Airport", "", "AU", true], - "CTO": ["Calverton Executive Airpark", "Calverton", "US", false], - "CTP": ["Carutapera Airport", "Carutapera", "BR", false], - "CTQ": ["Santa Vitória do Palmar Airport", "Santa Vitória do Palmar", "BR", false], - "CTR": ["Cattle Creek Airport", "", "AU", false], - "CTS": ["New Chitose Airport", "Sapporo", "JP", true], - "CTT": ["Le Castellet Airport", "Le Castellet, Var", "FR", false], - "CTU": ["Chengdu Shuangliu International Airport", "Chengdu (Shuangliu)", "CN", true], - "CTV": ["Catoca Airport", "Saurimo", "AO", false], - "CTW": ["Cottonwood Airport", "Cottonwood", "US", false], - "CTX": ["Cortland County Chase Field", "Cortland", "US", false], - "CTY": ["Cross City Airport", "Cross City", "US", false], - "CTZ": ["Sampson County Airport", "Clinton", "US", false], - "CUA": ["Ciudad Constitución National Airport", "Comondú", "MX", true], - "CUB": ["Jim Hamilton L.B. Owens Airport", "Columbia", "US", false], - "CUC": ["Camilo Daza International Airport", "Cúcuta", "CO", true], - "CUD": ["Caloundra Airport", "Caloundra", "AU", false], - "CUE": ["Mariscal Lamar Airport", "Cuenca", "EC", true], - "CUF": ["Cuneo International Airport", "Levaldigi (CN)", "IT", true], - "CUG": ["Cudal Airport", "", "AU", false], - "CUH": ["Cushing Municipal Airport", "Cushing", "US", false], - "CUI": ["Currillo Airport", "Currillo", "CO", false], - "CUJ": ["Culion Airport", "Culion", "PH", false], - "CUK": ["Caye Caulker Airport", "Caye Caulker", "BZ", true], - "CUL": ["Bachigualato Federal International Airport", "Culiacán", "MX", true], - "CUM": ["Antonio José de Sucre Airport", "Cumaná", "VE", true], - "CUN": ["Cancún International Airport", "Cancún", "MX", true], - "CUO": ["Carurú Airport", "Carurú", "CO", false], - "CUP": ["General Francisco Bermúdez Airport", "Carúpano", "VE", true], - "CUQ": ["Coen Airport", "Coen", "AU", true], - "CUR": ["Hato International Airport", "Willemstad", "CW", true], - "CUS": ["Columbus Airport", "Columbus", "US", false], - "CUT": ["Cutral-Co Airport", "Cutral-Co", "AR", false], - "CUU": ["General Roberto Fierro Villalobos International Airport", "Chihuahua", "MX", true], - "CUV": ["Casigua El Cubo Airport", "Casigua El Cubo", "VE", false], - "CUY": ["Cue Airport", "", "AU", false], - "CUZ": ["Alejandro Velasco Astete International Airport", "Cusco", "PE", true], - "CVB": ["Chungribu Airport", "Chungribu", "PG", false], - "CVC": ["Cleve Airport", "", "AU", false], - "CVE": ["Coveñas Airport", "Coveñas", "CO", false], - "CVF": ["Courchevel Altiport", "Saint-Bon", "FR", false], - "CVG": ["Cincinnati Northern Kentucky International Airport", "Cincinnati / Covington", "US", true], - "CVH": ["Caviahue Airport", "Lafontaine", "AR", false], - "CVI": ["Caleta Olivia Airport", "Caleta Olivia", "AR", false], - "CVJ": ["General Mariano Matamoros International Airport", "Temixco", "MX", false], - "CVL": ["Cape Vogel Airport", "", "PG", false], - "CVM": ["General Pedro Jose Mendez International Airport", "Ciudad Victoria", "MX", true], - "CVN": ["Clovis Municipal Airport", "Clovis", "US", true], - "CVO": ["Corvallis Municipal Airport", "Corvallis", "US", false], - "CVQ": ["Carnarvon Airport", "Carnarvon", "AU", true], - "CVS": ["Cannon Air Force Base", "Clovis", "US", false], - "CVU": ["Corvo Airport", "Corvo", "PT", true], - "CWA": ["Central Wisconsin Airport", "Mosinee", "US", true], - "CWB": ["Curitiba-Afonso Pena International Airport", "Curitiba", "BR", true], - "CWC": ["Chernivtsi International Airport", "Chernivtsi", "UA", false], - "CWF": ["Chennault International Airport", "Lake Charles", "US", false], - "CWI": ["Clinton Municipal Airport", "Clinton", "US", false], - "CWJ": ["Cangyuan Washan Airport", "Lincang (Cangyuan)", "CN", true], - "CWK": ["Chitrakoot Airport", "Chitrakoot", "IN", false], - "CWL": ["Cardiff International Airport", "Cardiff", "GB", true], - "CWR": ["Cowarie Airport", "", "AU", false], - "CWS": ["Center Island Airport", "Center Island", "US", true], - "CWT": ["Cowra Airport", "", "AU", false], - "CWW": ["Corowa Airport", "", "AU", false], - "CWX": ["Cochise County Airport", "Willcox", "US", false], - "CXA": ["Caicara del Orinoco Airport", "", "VE", false], - "CXB": ["Cox's Bazar Airport", "Cox's Bazar", "BD", true], - "CXC": ["Chitina Airport", "Chitina", "US", false], - "CXF": ["Coldfoot Airport", "Coldfoot", "US", false], - "CXH": ["Vancouver Harbour Water Aerodrome", "Vancouver", "CA", true], - "CXI": ["Cassidy International Airport", "Kiritimati", "KI", true], - "CXJ": ["Hugo Cantergiani Regional Airport", "Caxias Do Sul", "BR", true], - "CXL": ["Calexico International Airport", "Calexico", "US", false], - "CXN": ["Candala Airport", "Candala", "SO", false], - "CXO": ["Conroe-North Houston Regional Airport", "Houston", "US", false], - "CXP": ["Tunggul Wulung Airport", "Cilacap", "ID", true], - "CXQ": ["Christmas Creek Station Airport", "Wangkat Jungka", "AU", false], - "CXR": ["Cam Ranh International Airport / Cam Ranh Air Base", "Nha Trang/nha Trang aiurportCam Ranh", "VN", true], - "CXT": ["Charters Towers Airport", "", "AU", false], - "CXY": ["Cat Cay Airport", "North Cat Cay", "BS", false], - "CYA": ["Antoine-Simon International Airport", "Les Cayes", "HT", true], - "CYB": ["Charles Kirkconnell International Airport", "West End", "KY", true], - "CYC": ["Caye Chapel Airport", "Caye Chapel", "BZ", true], - "CYD": ["San Ignacio Town (Maya Flats) Airstrip", "Maya Flats", "BZ", false], - "CYF": ["Chefornak Airport", "Chefornak", "US", true], - "CYG": ["Corryong Airport", "", "AU", false], - "CYI": ["Chiayi Airport", "Shuishang", "TW", true], - "CYL": ["Coyoles Airport", "Coyoles", "HN", false], - "CYM": ["Chatham Seaplane Base", "Chatham", "US", true], - "CYO": ["Vilo Acuña International Airport", "Cayo Largo del Sur", "CU", true], - "CYP": ["Calbayog Airport", "Calbayog City", "PH", true], - "CYR": ["Colonia Laguna de Los Patos International Airport", "Colonia del Sacramento", "UY", false], - "CYS": ["Cheyenne Regional Jerry Olson Field", "Cheyenne", "US", true], - "CYT": ["Yakataga Airport", "Yakataga", "US", true], - "CYU": ["Cuyo Airport", "Cuyo", "PH", true], - "CYW": ["Captain Rogelio Castillo National Airport", "Celaya", "MX", false], - "CYX": ["Cherskiy Airport", "Cherskiy", "RU", true], - "CYZ": ["Cauayan Airport", "Cauayan City", "PH", true], - "CZA": ["Chichén Itzá International Airport", "Tinúm", "MX", false], - "CZB": ["Carlos Ruhl Airport", "Cruz Alta", "BR", false], - "CZC": ["Copper Center 2 Airport", "Copper Center", "US", false], - "CZE": ["José Leonardo Chirinos Airport", "Coro", "VE", true], - "CZF": ["Cape Romanzof LRRS Airport", "Cape Romanzof", "US", false], - "CZH": ["Corozal Airport", "Corozal", "BZ", true], - "CZJ": ["Corazón de Jesús Airport", "Tupile", "PA", false], - "CZK": ["Cascade Locks State Airport", "Cascade Locks", "US", false], - "CZL": ["Mohamed Boudiaf International Airport", "Constantine", "DZ", true], - "CZM": ["Cozumel International Airport", "Cozumel", "MX", true], - "CZN": ["Chisana Airport", "Chisana", "US", false], - "CZO": ["Chistochina Airport", "Chistochina", "US", false], - "CZP": ["Cape Pole Seaplane Base", "Cape Pole", "US", false], - "CZR": ["Chara Airport", "Chara", "RU", false], - "CZS": ["Cruzeiro do Sul Airport", "Cruzeiro Do Sul", "BR", true], - "CZT": ["Dimmit County Airport", "Carrizo Springs", "US", false], - "CZU": ["Las Brujas Airport", "Corozal", "CO", true], - "CZX": ["Changzhou Benniu International Airport", "Changzhou", "CN", true], - "CZY": ["Cluny Airport", "", "AU", false], - "DAA": ["Davison Army Air Field", "Fort Belvoir", "US", false], - "DAB": ["Daytona Beach International Airport", "Daytona Beach", "US", true], - "DAC": ["Hazrat Shahjalal International Airport", "Dhaka", "BD", true], - "DAD": ["Da Nang International Airport", "Da Nang", "VN", true], - "DAF": ["Daup Airport", "", "PG", false], - "DAG": ["Barstow Daggett Airport", "Daggett", "US", false], - "DAH": ["Dathina Airport", "Dathina", "YE", false], - "DAK": ["Dakhla Oasis Airport", "Dakhla Oases", "EG", false], - "DAL": ["Dallas Love Field", "Dallas", "US", true], - "DAM": ["Damascus International Airport", "Damascus", "SY", true], - "DAN": ["Danville Regional Airport", "Danville", "US", false], - "DAO": ["Dabo Airport", "", "PG", false], - "DAP": ["Darchula Airport", "Darchula", "NP", false], - "DAR": ["Julius Nyerere International Airport", "Dar es Salaam", "TZ", true], - "DAS": ["Great Bear Lake Airport", "Great Bear Lake", "CA", false], - "DAT": ["Datong Yungang International Airport", "Datong", "CN", true], - "DAU": ["Daru Airport", "Daru", "PG", true], - "DAV": ["Enrique Malek International Airport", "David", "PA", true], - "DAX": ["Dachuan Airport", "Dazhou (Daxian)", "CN", true], - "DAY": ["James M. Cox Dayton International Airport", "Dayton", "US", true], - "DAZ": ["Darwaz Airport", "Darwaz", "AF", false], - "DBA": ["Dalbandin Airport", "Dalbandin", "PK", true], - "DBB": ["El Alamein International Airport", "El Alamein", "EG", true], - "DBC": ["Baicheng Chang'an Airport", "Baicheng", "CN", true], - "DBD": ["Dhanbad Airport", "", "IN", false], - "DBK": ["Dutch Bay SPB", "Kalpitiya Island", "LK", false], - "DBM": ["Debre Markos Airport", "Debre Markos", "ET", true], - "DBN": ["W H 'Bud' Barron Airport", "Dublin", "US", false], - "DBO": ["Dubbo City Regional Airport", "Dubbo", "AU", true], - "DBP": ["Debepare Airport", "Debepare", "PG", false], - "DBQ": ["Dubuque Regional Airport", "Dubuque", "US", true], - "DBR": ["Darbhanga Airport", "Darbhanga", "IN", true], - "DBS": ["Dubois Municipal Airport", "Dubois", "US", false], - "DBT": ["Debre Tabor Airport", "Debre Tabor", "ET", false], - "DBU": ["Dambulu Oya Tank Seaplane Base", "Dambulla", "LK", false], - "DBV": ["Dubrovnik Ruđer Bošković Airport", "Dubrovnik", "HR", true], - "DBY": ["Dalby Airport", "", "AU", false], - "DCA": ["Ronald Reagan Washington National Airport", "Washington", "US", true], - "DCF": ["Canefield Airport", "Canefield", "DM", true], - "DCI": ["Decimomannu Air Base", "Decimomannu", "IT", false], - "DCK": ["Dahl Creek Airport", "Dahl Creek", "US", false], - "DCM": ["Castres Mazamet Airport", "Castres", "FR", true], - "DCN": ["RAAF Base Curtin", "Derby", "AU", false], - "DCT": ["Duncan Town Airport", "Duncan Town", "BS", false], - "DCU": ["Pryor Field Regional Airport", "Decatur", "US", false], - "DCY": ["Daocheng Yading Airport", "Garzê (Daocheng)", "CN", true], - "DDC": ["Dodge City Regional Airport", "Dodge City", "US", true], - "DDD": ["Dhaalu Atoll Airport", "Kudahuvadhoo", "MV", false], - "DDG": ["Dandong Langtou International Airport", "Dandong (Zhenxing)", "CN", true], - "DDI": ["Daydream Island Helipad", "Whitsundays", "AU", false], - "DDM": ["Dodoima Airport", "", "PG", false], - "DDN": ["Delta Downs Airport", "", "AU", false], - "DDR": ["Shigatse Tingri Airport", "Xigazê (Dingri)", "CN", true], - "DDU": ["Dadu West Airport", "Dadu", "PK", false], - "DEA": ["Dera Ghazi Khan Airport", "Dera Ghazi Khan", "PK", true], - "DEB": ["Debrecen International Airport", "Debrecen", "HU", true], - "DEC": ["Decatur Airport", "Decatur", "US", true], - "DED": ["Dehradun Jolly Grant Airport", "Dehradun (Jauligrant)", "IN", true], - "DEE": ["Yuzhno-Kurilsk Mendeleyevo Airport", "Yuzhno-Kurilsk", "RU", true], - "DEF": ["Dezful Airport", "Dezful", "IR", true], - "DEH": ["Decorah Municipal Airport", "Decorah", "US", false], - "DEI": ["Denis Island Airport", "Denis Island", "SC", false], - "DEJ": ["Tongren Dejiang Airport (Under Construction)", "Tongren", "CN", false], - "DEL": ["Indira Gandhi International Airport", "New Delhi", "IN", true], - "DEM": ["Dembidollo Airport", "Dembidollo", "ET", true], - "DEN": ["Denver International Airport", "Denver", "US", true], - "DEP": ["Daporijo Airport", "Daporijo", "IN", false], - "DEQ": ["Deqing Moganshan Airport", "Huzhou", "CN", false], - "DER": ["Derim Airport", "Derim", "PG", false], - "DES": ["Desroches Airport", "Desroches Island", "SC", false], - "DET": ["Coleman A. Young Municipal Airport", "Detroit", "US", false], - "DEX": ["Nop Goliat Dekai Airport", "Dekai", "ID", true], - "DEZ": ["Deir ez-Zor Airport", "Deir ez-Zor", "SY", false], - "DFA": ["Shangluo Airport", "Shangluo (Danfeng)", "CN", false], - "DFI": ["Defiance Memorial Airport", "Defiance", "US", false], - "DFP": ["Drumduff Airport", "Palmer", "AU", false], - "DFW": ["Dallas Fort Worth International Airport", "Dallas-Fort Worth", "US", true], - "DGA": ["Dangriga Airport", "Dangriga", "BZ", true], - "DGD": ["Dalgaranga Gold Mine Airport", "", "AU", false], - "DGE": ["Mudgee Airport", "Mudgee", "AU", false], - "DGF": ["Douglas Lake Airport", "Douglas Lake", "CA", false], - "DGH": ["Deoghar Airport", "Deoghar", "IN", true], - "DGL": ["Douglas Municipal Airport", "Douglas", "US", false], - "DGM": ["Dandugama Seaplane Base", "Colombo", "LK", false], - "DGN": ["Dahlgren Naval Surface Warfare Center Airport", "Dahlgren", "US", false], - "DGO": ["General Guadalupe Victoria International Airport", "Durango", "MX", true], - "DGR": ["Dargaville Aerodrome", "Dargaville", "NZ", false], - "DGT": ["Sibulan Airport", "Dumaguete City", "PH", true], - "DGU": ["Dedougou Airport", "Dedougou", "BF", false], - "DGW": ["Converse County Airport", "Douglas", "US", false], - "DHA": ["King Abdulaziz Air Base", "Dhahran", "SA", false], - "DHB": ["Deer Harbor SPB", "Deer Harbor", "US", true], - "DHD": ["Durham Downs Airport", "", "AU", false], - "DHF": ["Al Dhafra Air Base", "", "AE", false], - "DHG": ["Dalnegorsk Airport", "Dalnegorsk", "RU", false], - "DHH": ["Barkol Dahe Airport", "Barkol", "CN", false], - "DHI": ["Dhangarhi Airport", "Dhangarhi", "NP", false], - "DHM": ["Kangra Airport", "Kangra", "IN", true], - "DHN": ["Dothan Regional Airport", "Dothan", "US", true], - "DHR": ["De Kooy Airfield / Den Helder Naval Air Station", "Den Helder", "NL", false], - "DHT": ["Dalhart Municipal Airport", "Dalhart", "US", false], - "DHX": ["Dhoho International Airport", "Kediri", "ID", true], - "DIA": ["Doha International Airport", "Doha", "QA", true], - "DIB": ["Dibrugarh Airport", "Dibrugarh", "IN", true], - "DIE": ["Arrachart Airport", "Antisiranana", "MG", true], - "DIG": ["Diqing Shangri-La Airport", "Diqing (Shangri-La)", "CN", true], - "DIJ": ["Dijon Longvic airport", "Dijon", "FR", false], - "DIK": ["Dickinson Theodore Roosevelt Regional Airport", "Dickinson", "US", true], - "DIL": ["Presidente Nicolau Lobato International Airport", "Dili", "TL", true], - "DIM": ["Dimbokro Airport", "Dimbokro", "CI", false], - "DIN": ["Dien Bien Phu Airport", "Dien Bien Phu", "VN", true], - "DIO": ["Diomede Heliport", "Diomede", "US", true], - "DIP": ["Diapaga Airport", "Diapaga", "BF", false], - "DIQ": ["Brigadeiro Cabral Airport", "Divinópolis", "BR", false], - "DIR": ["Aba Tenna Dejazmach Yilma International Airport", "Dire Dawa", "ET", true], - "DIS": ["Ngot Nzoungou Airport", "Dolisie", "CG", false], - "DIU": ["Diu Airport", "Diu", "IN", true], - "DIW": ["Mawella Lagoon Seaplane Base", "Dickwella", "LK", false], - "DIY": ["Diyarbakır Airport", "Diyarbakır", "TR", true], - "DJA": ["Djougou Airport", "Djougou", "BJ", false], - "DJB": ["Sultan Thaha Airport", "Jambi", "ID", true], - "DJE": ["Djerba Zarzis International Airport", "Mellita", "TN", true], - "DJG": ["Tiska Djanet Airport", "Djanet", "DZ", true], - "DJJ": ["Dortheys Hiyo Eluay International Airport", "Sentani", "ID", true], - "DJM": ["Djambala Airport", "Djambala", "CG", false], - "DJN": ["Delta Junction Airport", "Delta Junction", "US", false], - "DJO": ["Daloa Airport", "", "CI", false], - "DJR": ["Dajarra Airport", "", "AU", false], - "DJT": ["President Donald J. Trump International Airport", "West Palm Beach", "US", true], - "DJU": ["Djúpivogur Airport", "Djúpivogur", "IS", false], - "DKA": ["Umaru Musa Yar'adua Airport", "Katsina", "NG", true], - "DKI": ["Dunk Island Airport", "Dunk Island", "AU", false], - "DKK": ["Chautauqua County-Dunkirk Airport", "Dunkirk", "US", false], - "DKR": ["Léopold Sédar Senghor International Airport", "Dakar", "SN", false], - "DKS": ["Dikson Airport", "Dikson", "RU", false], - "DKV": ["Docker River Airport", "Kaltukatjara", "AU", false], - "DLA": ["Douala International Airport", "Douala", "CM", true], - "DLC": ["Dalian Zhoushuizi International Airport", "Dalian (Ganjingzi)", "CN", true], - "DLE": ["Dole Tavaux Airport", "Dole", "FR", true], - "DLF": ["Laughlin Air Force Base", "Del Rio", "US", false], - "DLG": ["Dillingham Airport", "Dillingham", "US", true], - "DLH": ["Duluth International Airport", "Duluth", "US", true], - "DLI": ["Lien Khuong Airport", "Da Lat", "VN", true], - "DLK": ["Dulkaninna Airport", "Dulkaninna", "AU", false], - "DLL": ["Dillon County Airport", "Dillon", "US", false], - "DLM": ["Dalaman International Airport", "Dalaman", "TR", true], - "DLN": ["Dillon Airport", "Dillon", "US", false], - "DLR": ["Dalnerechensk Airport", "Dalnerechensk", "RU", true], - "DLS": ["Columbia Gorge Regional Airport", "Dallesport / The Dalles", "US", false], - "DLU": ["Dali Fengyi Airport", "Dali (Xiaguan)", "CN", true], - "DLV": ["Delissaville Airport", "", "AU", false], - "DLY": ["Dillon's Bay Airport", "Dillon's Bay", "VU", false], - "DLZ": ["Dalanzadgad Airport", "Dalanzadgad", "MN", true], - "DMA": ["Davis Monthan Air Force Base", "Tucson", "US", false], - "DMB": ["Taraz International Airport", "Taraz", "KZ", true], - "DMD": ["Doomadgee Airport", "", "AU", true], - "DME": ["Domodedovo International Airport", "Moscow", "RU", true], - "DMK": ["Don Mueang International Airport", "Bangkok", "TH", true], - "DMM": ["King Fahd International Airport", "Ad Dammam", "SA", true], - "DMN": ["Deming Municipal Airport", "Deming", "US", false], - "DMO": ["Sedalia Memorial Airport", "Sedalia", "US", false], - "DMT": ["Diamantino Airport", "Diamantino", "BR", false], - "DMU": ["Dimapur Airport", "Dimapur", "IN", true], - "DNA": ["Kadena Air Base", "Okinawa", "JP", false], - "DNB": ["Dunbar Airport", "Maramie", "AU", false], - "DND": ["Dundee Airport", "Dundee", "GB", true], - "DNF": ["Martuba Air Base", "Martuba", "LY", false], - "DNG": ["Doongan Airport", "Drysdale River", "AU", false], - "DNH": ["Dunhuang Mogao International Airport", "Dunhuang", "CN", true], - "DNK": ["Dnipro International Airport", "Dnipro", "UA", false], - "DNL": ["Daniel Field", "Augusta", "US", false], - "DNN": ["Dalton Municipal Airport", "Dalton", "US", false], - "DNO": ["Dianópolis Airport", "Dianópolis", "BR", false], - "DNP": ["Tulsipur Airport", "Dang", "NP", false], - "DNQ": ["Deniliquin Airport", "Deniliquin", "AU", false], - "DNR": ["Dinard Pleurtuit Saint-Malo airport", "Dinard", "FR", false], - "DNS": ["Denison Municipal Airport", "Denison", "US", false], - "DNU": ["Dinangat Airport", "Dinangat", "PG", false], - "DNV": ["Vermilion Regional Airport", "Danville", "US", false], - "DNX": ["Galegu Airport", "Dinder", "SD", false], - "DNZ": ["Çardak Airport", "Denizli", "TR", true], - "DOA": ["Doany Airport", "Doany", "MG", false], - "DOB": ["Rar Gwamar Airport", "Dobo-Warmar Island", "ID", false], - "DOD": ["Dodoma Airport", "Dodoma", "TZ", true], - "DOE": ["Djumu-Djomoe Airport", "Djumu-Djomoe", "SR", false], - "DOG": ["Dongola Airport", "Dongola", "SD", true], - "DOH": ["Hamad International Airport", "Doha", "QA", true], - "DOI": ["Doini Airport", "Castori Islets", "PG", false], - "DOL": ["Deauville Normandie airport", "Deauville", "FR", true], - "DOM": ["Douglas-Charles Airport", "Marigot", "DM", true], - "DON": ["Dos Lagunas Airport", "Dos Lagunas", "GT", false], - "DOO": ["Dorobisoro Airport", "Dorobisoro", "PG", false], - "DOP": ["Dolpa Airport", "Dolpa", "NP", true], - "DOR": ["Dori Airport", "Dori", "BF", false], - "DOU": ["Dourados Airport", "Dourados", "BR", false], - "DOV": ["Dover Civil Air Terminal/Dover Air Force Base", "Dover", "US", true], - "DOX": ["Dongara Airport", "Port Denison", "AU", false], - "DOY": ["Dongying Shengli Airport", "Dongying (Kenli)", "CN", true], - "DPA": ["Dupage Airport", "Chicago/West Chicago", "US", false], - "DPB": ["Pampa Guanaco Airport", "Bahia Inutil", "CL", false], - "DPE": ["Dieppe-Saint-Aubin Airport", "Saint-Aubin-sur-Scie", "FR", false], - "DPG": ["Michael AAF (Dugway Proving Ground) Airport", "Dugway Proving Ground", "US", false], - "DPL": ["Dipolog Airport", "Dipolog", "PH", true], - "DPO": ["Devonport Airport", "Devonport", "AU", true], - "DPS": ["Denpasar I Gusti Ngurah Rai International Airport", "Kuta, Badung", "ID", true], - "DPT": ["Deputatskiy Airport", "Deputatskiy", "RU", true], - "DQA": ["Daqing Sartu Airport", "Daqing", "CN", true], - "DQM": ["Duqm International Airport", "Duqm", "OM", true], - "DRA": ["Desert Rock Airport", "Mercury", "US", false], - "DRB": ["Derby Airport", "Derby", "AU", false], - "DRC": ["Dirico Airport", "Dirico", "AO", false], - "DRD": ["Dorunda Airport", "Dorunda Outstation", "AU", false], - "DRE": ["Drummond Island Airport", "Drummond Island", "US", false], - "DRF": ["Drift River Airport", "Kenai", "US", false], - "DRG": ["Deering Airport", "Deering", "US", true], - "DRH": ["Dabra Airport", "Dabra", "ID", false], - "DRI": ["Beauregard Regional Airport", "DeRidder", "US", false], - "DRJ": ["Drietabbetje Airport", "Drietabbetje", "SR", true], - "DRK": ["Drake Bay Airport", "Puntarenas", "CR", false], - "DRN": ["Dirranbandi Airport", "", "AU", false], - "DRO": ["Durango La Plata County Airport", "Durango", "US", true], - "DRP": ["Bicol International Airport", "Legazpi", "PH", true], - "DRR": ["Durrie Airport", "", "AU", false], - "DRS": ["Dresden Airport", "Dresden", "DE", true], - "DRT": ["Del Rio International Airport", "Del Rio", "US", false], - "DRU": ["Drummond Airport", "Drummond", "US", false], - "DRV": ["Dharavandhoo Airport", "Baa Atoll", "MV", true], - "DRW": ["Darwin International Airport / RAAF Darwin", "Darwin", "AU", true], - "DRY": ["Drysdale River Airport", "Drysdale River", "AU", false], - "DSC": ["Dschang Airport", "Dschang", "CM", false], - "DSD": ["La Désirade Airport", "Grande Anse", "GP", true], - "DSE": ["Kombolcha Airport", "Dessie", "ET", true], - "DSG": ["Dilasag Airport", "Dilasag", "PH", false], - "DSI": ["Destin Executive Airport", "Destin", "US", true], - "DSK": ["Dera Ismael Khan Airport [IN-ACTIVE]", "Dera Ismael Khan", "PK", false], - "DSM": ["Des Moines International Airport", "Des Moines", "US", true], - "DSN": ["Ordos Ejin Horo International Airport", "Ordos", "CN", true], - "DSO": ["Sondok Airport", "Sŏndŏng-ni", "KP", true], - "DSS": ["Blaise Diagne International Airport", "Dakar", "SN", true], - "DST": ["Dubai Seaplane Terminal", "", "AE", false], - "DSV": ["Dansville Municipal Airport", "Dansville", "US", false], - "DSX": ["Dongsha Island (Pratas Island) Airport", "Kaohsiung (Cijin - Pratas Island)", "TW", false], - "DSY": ["Dara Sakor International Airport", "Ta Noun", "KH", true], - "DTA": ["Delta Municipal Airport", "Delta", "US", false], - "DTB": ["Silangit Airport", "Siborong-Borong", "ID", true], - "DTD": ["Datadawai Airport", "Datadawai-Borneo Island", "ID", true], - "DTE": ["Daet Airport", "Daet", "PH", false], - "DTH": ["Furnace Creek Airport", "Death Valley", "US", false], - "DTI": ["Diamantina Airport", "Diamantina", "BR", false], - "DTL": ["Detroit Lakes Airport", "Detroit Lakes", "US", false], - "DTM": ["Dortmund Airport", "Dortmund", "DE", true], - "DTN": ["Shreveport Downtown Airport", "Shreveport", "US", false], - "DTR": ["Decatur Shores Airport", "Decatur", "US", true], - "DTU": ["Wudalianchi Dedu Airport", "Heihe", "CN", true], - "DTW": ["Detroit Metropolitan Wayne County Airport", "Detroit", "US", true], - "DTX": ["McKinney National Airport", "Dallas", "US", false], - "DUA": ["Durant Regional Airport - Eaker Field", "Durant", "US", false], - "DUB": ["Dublin Airport", "Dublin", "IE", true], - "DUC": ["Halliburton Field", "Duncan", "US", false], - "DUD": ["Dunedin International Airport", "Dunedin", "NZ", true], - "DUE": ["Dundo Airport", "Chitato", "AO", true], - "DUF": ["Pine Island Airport", "Corolla", "US", false], - "DUG": ["Bisbee Douglas International Airport", "Douglas Bisbee", "US", false], - "DUJ": ["DuBois Regional Airport", "Dubois", "US", true], - "DUK": ["Mubatuba Airport", "Mubatuba", "ZA", false], - "DUM": ["Pinang Kampai Airport", "Dumai", "ID", true], - "DUQ": ["Duncan Airport", "Duncan", "CA", false], - "DUR": ["King Shaka International Airport", "Durban", "ZA", true], - "DUS": ["Düsseldorf Airport", "Düsseldorf", "DE", true], - "DUT": ["Tom Madsen (Dutch Harbor) Airport", "Unalaska", "US", true], - "DVD": ["Andavadoaka Airport", "Andavadoaka", "MG", false], - "DVK": ["Diavik Airport", "Diavik", "CA", false], - "DVL": ["Devils Lake Regional Airport", "Devils Lake", "US", true], - "DVN": ["Davenport Municipal Airport", "Davenport", "US", false], - "DVO": ["Francisco Bangoy International Airport", "Davao", "PH", true], - "DVP": ["Davenport Downs Airport", "", "AU", false], - "DVR": ["Daly River Airport", "Nauiyu", "AU", false], - "DVT": ["Phoenix Deer Valley Airport", "Phoenix", "US", false], - "DWA": ["Dwangwa Airport", "Dwangwa", "MW", false], - "DWB": ["Soalala Airport", "Soalala", "MG", true], - "DWC": ["Al Maktoum International Airport", "Dubai(Jebel Ali)", "AE", true], - "DWD": ["Dawadmi Domestic Airport", "Dawadmi", "SA", true], - "DWH": ["David Wayne Hooks Memorial Airport", "Houston", "US", false], - "DWO": ["Diyawanna Oya Seaplane Base", "Sri Jayawardenepura Kotte", "LK", true], - "DWR": ["Dwyer Airport", "Reg", "AF", false], - "DWS": ["Morin Dawa Banner Airport", "Hulunbuir", "CN", false], - "DXB": ["Dubai International Airport", "Dubai", "AE", true], - "DXD": ["Dixie Airport", "New Dixie", "AU", false], - "DXE": ["Bruce Campbell Field", "Madison", "US", false], - "DXJ": ["Xiangxi Biancheng Airport", "Xiangxi", "CN", true], - "DXN": ["Noida International Airport", "Gautam Buddha Nagar", "IN", true], - "DXR": ["Danbury Municipal Airport", "Danbury", "US", false], - "DYA": ["Dysart Airport", "", "AU", false], - "DYG": ["Zhangjiajie Hehua International Airport", "Zhangjiajie (Yongding)", "CN", true], - "DYL": ["Doylestown Airport", "Doylestown", "US", false], - "DYM": ["Diamantina Lakes Airport", "Diamantina Lakes", "AU", false], - "DYR": ["Ugolny Yuri Ryktheu Airport", "Anadyr", "RU", true], - "DYS": ["Dyess Air Force Base", "Abilene", "US", false], - "DYU": ["Dushanbe International Airport", "Dushanbe", "TJ", true], - "DYW": ["Daly Waters Airport", "Daly Waters", "AU", false], - "DZA": ["Dzaoudzi Pamandzi International Airport", "Dzaoudzi", "YT", true], - "DZH": ["Dazhou Jinya Airport", "Dazhou (Dachuan)", "CN", true], - "DZI": ["Las Flores Airport", "Codazzi", "CO", false], - "DZN": ["Zhezkazgan National Airport", "Zhezkazgan", "KZ", true], - "DZO": ["Santa Bernardina International Airport", "Durazno", "UY", false], - "DZU": ["Dazu Air Base", "Dazu", "CN", false], - "EAA": ["Eagle Airport", "Eagle", "US", true], - "EAB": ["Abs Airport", "Abs", "YE", false], - "EAE": ["Siwo Airport", "Emae Island", "VU", false], - "EAL": ["Elenak Airport", "Mejato Island", "MH", false], - "EAM": ["Najran Domestic Airport", "Najran", "SA", true], - "EAN": ["Phifer Airfield", "Wheatland", "US", false], - "EAR": ["Kearney Regional Airport", "Kearney", "US", true], - "EAS": ["San Sebastián Airport", "Hondarribia", "ES", true], - "EAT": ["Pangborn Memorial Airport", "Wenatchee", "US", true], - "EAU": ["Chippewa Valley Regional Airport", "Eau Claire", "US", true], - "EAX": ["Eduard Alexander Gummels Airport", "Kwatta", "SR", true], - "EBA": ["Marina di Campo Airport", "Campo nell'Elba (LI)", "IT", true], - "EBB": ["Entebbe International Airport", "Entebbe", "UG", true], - "EBD": ["El-Obeid Airport", "El-Obeid", "SD", true], - "EBG": ["El Bagre Airport", "El Bagre", "CO", false], - "EBH": ["El Bayadh Airport", "El Bayadh", "DZ", true], - "EBJ": ["Esbjerg Airport", "Esbjerg", "DK", true], - "EBL": ["Erbil International Airport", "Arbil", "IQ", true], - "EBM": ["El Borma Airport", "El Borma", "TN", false], - "EBN": ["Ebadon Airport", "Ebadon Island", "MH", false], - "EBO": ["Ebon Airport", "Ebon Atoll", "MH", false], - "EBS": ["Webster City Municipal Airport", "Webster City", "US", false], - "EBU": ["Saint-Étienne-Bouthéon Airport", "Andrézieux-Bouthéon, Loire", "FR", false], - "EBW": ["Ebolowa Airport", "Ebolowa", "CM", false], - "ECA": ["Iosco County Airport", "East Tawas", "US", false], - "ECG": ["Elizabeth City Regional Airport & Coast Guard Air Station", "Elizabeth City", "US", false], - "ECH": ["Echuca Airport", "", "AU", false], - "ECI": ["Costa Esmeralda Airport", "Tola", "NI", false], - "ECN": ["Ercan International Airport", "Tymbou (Kirklar)", "CY", true], - "ECO": ["El Encanto Airport", "El Encanto", "CO", false], - "ECP": ["Northwest Florida Beaches International Airport", "Panama City Beach", "US", true], - "ECR": ["El Charco Airport", "El Charco", "CO", false], - "ECS": ["Mondell Field", "Newcastle", "US", false], - "EDA": ["Edna Bay Seaplane Base", "Edna Bay", "US", true], - "EDB": ["Al Dabbah Airport", "Al Dabbah", "SD", false], - "EDD": ["Erldunda Airport", "Ghan", "AU", false], - "EDE": ["Northeastern Regional Airport", "Edenton", "US", false], - "EDF": ["Elmendorf Air Force Base", "Anchorage", "US", false], - "EDI": ["Edinburgh Airport", "Ingliston, Edinburgh", "GB", true], - "EDK": ["Captain Jack Thomas El Dorado Airport", "El Dorado", "US", false], - "EDL": ["Eldoret International Airport", "Eldoret", "KE", true], - "EDM": ["La Roche-sur-Yon Les Ajoncs Airport", "La Roche-sur-Yon", "FR", false], - "EDN": ["Yedinka Airport", "Yedinka", "RU", false], - "EDO": ["Balıkesir Koca Seyit Airport", "Edremit", "TR", true], - "EDQ": ["Erandique Airport", "Erandique", "HN", false], - "EDR": ["Pormpuraaw Airport", "Pormpuraaw", "AU", true], - "EDW": ["Edwards Air Force Base", "Edwards", "US", false], - "EEA": ["Planalto Serrano Regional Airport", "Correia Pinto", "BR", false], - "EED": ["Needles Airport", "Needles", "US", false], - "EEK": ["Eek Airport", "Eek", "US", true], - "EEN": ["Dillant Hopkins Airport", "Keene", "US", false], - "EES": ["Berenice International Airport / Banas Cape Air Base", "Berenice Troglodytica", "EG", false], - "EFD": ["Ellington Airport", "Houston", "US", false], - "EFG": ["Efogi Airport", "Efogi", "PG", false], - "EFK": ["Northeast Kingdom International Airport", "Newport", "US", false], - "EFL": ["Kefallinia Airport", "Kefallinia Island", "GR", true], - "EFW": ["Jefferson Municipal Airport", "Jefferson", "US", false], - "EGA": ["Engati Airstrip", "Engati", "PG", false], - "EGC": ["Bergerac Dordogne-Périgord airport", "Bergerac", "FR", true], - "EGE": ["Eagle County Regional Airport", "Eagle", "US", true], - "EGH": ["El Jora Airport", "El Jora", "EG", false], - "EGI": ["Duke Field", "Crestview", "US", false], - "EGL": ["Negele Airport", "Negele Borana", "ET", false], - "EGM": ["Sege Airport", "Sege", "SB", false], - "EGN": ["Geneina Airport", "Geneina", "SD", false], - "EGO": ["Belgorod International Airport", "Belgorod", "RU", false], - "EGP": ["Maverick County Memorial International Airport", "Eagle Pass", "US", false], - "EGS": ["Egilsstaðir Airport", "Egilsstaðir", "IS", true], - "EGV": ["Eagle River Union Airport", "Eagle River", "US", false], - "EGX": ["Egegik Airport", "Egegik", "US", true], - "EHL": ["El Bolsón Airfield", "El Bolsón", "AR", false], - "EHM": ["Cape Newenham LRRS Airport", "Cape Newenham", "US", false], - "EHU": ["Ezhou Huahu International Airport", "Ezhou", "CN", true], - "EIB": ["Eisenach-Kindel Airport", "Hörselberg-Hainich", "DE", false], - "EIE": ["Yeniseysk Airport", "Yeniseysk", "RU", true], - "EIH": ["Einasleigh Airport", "Einasleigh", "AU", false], - "EIK": ["Yeysk Airport", "Yeysk", "RU", false], - "EIL": ["Eielson Air Force Base", "Fairbanks", "US", false], - "EIN": ["Eindhoven Airport", "Eindhoven", "NL", true], - "EIS": ["Terrance B. Lettsome International Airport", "Beef Island", "VG", true], - "EIY": ["Ein Yahav Airfield", "Sapir", "IL", false], - "EJA": ["Yariguíes Airport", "Barrancabermeja", "CO", true], - "EJH": ["Al Wajh Domestic Airport", "Al Wajh", "SA", true], - "EJN": ["Ejin Banner Taolai Airport", "Ejin Banner", "CN", false], - "EJT": ["Enejit Airport", "Enejit Island", "MH", true], - "EKA": ["Murray Field", "Eureka", "US", false], - "EKB": ["Ekibastuz Airport", "Ekibastuz", "KZ", false], - "EKD": ["Elkedra Airport", "", "AU", false], - "EKI": ["Elkhart Municipal Airport", "Elkhart", "US", false], - "EKN": ["Elkins-Randolph County Regional Airport", "Elkins", "US", false], - "EKO": ["Elko Regional Airport", "Elko", "US", true], - "EKS": ["Shakhtyorsk Airport", "Shakhtyorsk", "RU", true], - "EKT": ["Eskilstuna Airport", "Eskilstuna", "SE", false], - "EKX": ["Addington Field / Elizabethtown Regional Airport", "Elizabethtown", "US", false], - "ELA": ["Eagle Lake Airport", "Eagle Lake", "US", false], - "ELB": ["Las Flores Airport", "El Banco", "CO", false], - "ELC": ["Elcho Island Airport", "Elcho Island", "AU", true], - "ELD": ["South Arkansas Regional Airport at Goodwin Field", "El Dorado", "US", true], - "ELE": ["EL Real Airport", "El Real de Santa María", "PA", false], - "ELF": ["El Fasher Airport", "El Fasher", "SD", true], - "ELG": ["El Golea Airport", "El Menia", "DZ", true], - "ELH": ["North Eleuthera Airport", "North Eleuthera", "BS", true], - "ELI": ["Elim Airport", "Elim", "US", true], - "ELJ": ["El Recreo Airport", "Ruperto Polania", "CO", false], - "ELK": ["Elk City Regional Business Airport", "Elk City", "US", false], - "ELL": ["Ellisras Matimba Airport", "Ellisras", "ZA", false], - "ELM": ["Elmira Corning Regional Airport", "Elmira/Corning", "US", true], - "ELN": ["Bowers Field", "Ellensburg", "US", false], - "ELO": ["El Dorado Airport", "El Dorado", "AR", false], - "ELP": ["El Paso International Airport", "El Paso", "US", true], - "ELQ": ["Prince Naif bin Abdulaziz International Airport", "Qassim", "SA", true], - "ELR": ["Elelim Airport", "Elelim", "ID", false], - "ELS": ["King Phalo Airport", "East London", "ZA", true], - "ELT": ["At Tur Airport", "At Tur", "EG", false], - "ELU": ["Guemar Airport - مطار قمار بالوادي", "Guemar", "DZ", true], - "ELV": ["Elfin Cove Seaplane Base", "Elfin Cove", "US", true], - "ELW": ["Ellamar Seaplane Base", "Ellamar", "US", false], - "ELX": ["El Tigre Airport", "El Tigre", "VE", false], - "ELY": ["Ely Airport Yelland Field", "Ely", "US", false], - "ELZ": ["Wellsville Municipal Airport - Tarantine Field", "Wellsville", "US", false], - "EMA": ["East Midlands Airport", "Nottingham, Leicestershire", "GB", true], - "EMD": ["Emerald Airport", "Emerald", "AU", true], - "EME": ["Emden Airport", "Emden", "DE", true], - "EMG": ["Empangeni Airport", "Empangeni", "ZA", false], - "EMI": ["Emirau Airport", "Emirau Island", "PG", false], - "EMK": ["Emmonak Airport", "Emmonak", "US", true], - "EML": ["Emmen Air Base", "Emmen", "CH", false], - "EMM": ["Kemmerer Municipal Airport", "Kemmerer", "US", false], - "EMN": ["Néma Airport", "Néma", "MR", false], - "EMO": ["Emo River Airstrip", "Emo Mission", "PG", false], - "EMP": ["Emporia Municipal Airport", "Emporia", "US", false], - "EMR": ["El Mirador Heliport", "El Mirador", "GT", false], - "EMS": ["Embessa Airport", "Embessa", "PG", false], - "EMT": ["San Gabriel Valley Airport", "El Monte", "US", false], - "EMX": ["El Maitén Airport", "El Maitén", "AR", false], - "EMY": ["El Minya Airport", "El Minya", "EG", false], - "ENA": ["Kenai Municipal Airport", "Kenai", "US", true], - "ENB": ["Eneabba Airport", "Eneabba", "AU", false], - "ENC": ["Nancy-Essey Airport", "Tomblaine, Meurthe-et-Moselle", "FR", false], - "END": ["Vance Air Force Base", "Enid", "US", false], - "ENE": ["H. Hasan Aroeboesman (Ende) Airport", "Ende", "ID", true], - "ENF": ["Enontekio Airport", "Enontekio", "FI", true], - "ENG": ["Enggano Airport", "Enggano", "ID", false], - "ENH": ["Enshi Xujiaping Airport", "Enshi (Enshi)", "CN", true], - "ENI": ["El Nido Airport", "El Nido", "PH", true], - "ENJ": ["El Naranjo Airport", "El Naranjo", "GT", false], - "ENK": ["Enniskillen/St Angelo Airport", "Enniskillen, Fermanagh and Omagh", "GB", false], - "ENL": ["Centralia Municipal Airport", "Centralia", "US", false], - "ENN": ["Nenana Municipal Airport", "Nenana", "US", false], - "ENO": ["Teniente Ramon A. Ayub Gonzalez International Airport", "Encarnación", "PY", true], - "ENS": ["Twente Airport", "Enschede", "NL", false], - "ENT": ["Eniwetok Airport", "Eniwetok Atoll", "MH", true], - "ENU": ["Akanu Ibiam International Airport", "Enegu", "NG", true], - "ENV": ["Wendover Airport", "Wendover", "US", false], - "ENW": ["Kenosha Regional Airport", "Kenosha", "US", false], - "ENY": ["Yan'an Nanniwan Airport", "Yan'an (Baota)", "CN", true], - "EOH": ["Enrique Olaya Herrera Airport", "Medellín", "CO", true], - "EOI": ["Eday Airport", "Eday", "GB", true], - "EOK": ["Keokuk Municipal Airport", "Keokuk", "US", false], - "EOR": ["El Dorado Airport", "Bolivar", "VE", false], - "EOS": ["Neosho Hugh Robinson Airport", "Neosho", "US", false], - "EOZ": ["Elorza Airport", "", "VE", false], - "EPA": ["El Palomar Airport", "El Palomar", "AR", false], - "EPG": ["Browns Airport", "Weeping Water", "US", false], - "EPH": ["Ephrata Municipal Airport", "Ephrata", "US", false], - "EPL": ["Épinal Mirecourt Airport", "Épinal", "FR", false], - "EPR": ["Esperance Airport", "Esperance", "AU", true], - "EPT": ["Eliptamin Airport", "Eliptamin", "PG", false], - "EPU": ["Pärnu Airport", "Pärnu", "EE", true], - "EQS": ["Esquel Brigadier Antonio Parodi International Airport", "Esquel", "AR", true], - "ERA": ["Erigavo Airport", "Erigavo", "SO", false], - "ERB": ["Ernabella Airport", "", "AU", false], - "ERC": ["Erzincan Airport", "Erzincan", "TR", true], - "ERD": ["Berdyansk Airport", "Berdyansk", "UA", false], - "ERE": ["Erave Airport", "Erave", "PG", false], - "ERF": ["Erfurt-Weimar Airport", "Erfurt", "DE", true], - "ERG": ["Yerbogachen Airport", "Erbogachen", "RU", false], - "ERH": ["Moulay Ali Cherif Airport", "Errachidia", "MA", true], - "ERI": ["Erie International Tom Ridge Field", "Erie", "US", true], - "ERL": ["Erenhot Saiwusu International Airport", "Erenhot", "CN", true], - "ERM": ["Erechim Airport", "Erechim", "BR", false], - "ERN": ["Eirunepé Airport", "Eirunepé", "BR", false], - "ERQ": ["Elrose Airport", "Elrose Mine", "AU", false], - "ERR": ["Errol Airport", "Errol", "US", false], - "ERS": ["Eros Airport", "Windhoek", "NA", true], - "ERT": ["Erdenet Airport", "Erdenet", "MN", false], - "ERU": ["Erume Airport", "Erume", "PG", false], - "ERV": ["Kerrville / Kerr County Airport at Louis Schreiner Field", "Kerrville", "US", false], - "ERZ": ["Erzurum International Airport", "Erzurum", "TR", true], - "ESA": ["Esa'ala Airport", "Esa'ala", "PG", false], - "ESB": ["Esenboğa International Airport", "Ankara", "TR", true], - "ESC": ["Delta County Airport", "Escanaba", "US", true], - "ESD": ["Orcas Island Airport", "Eastsound", "US", true], - "ESE": ["Ensenada International Airport / El Ciprés Air Base", "Ensenada", "MX", false], - "ESF": ["Esler Army Airfield / Esler Regional Airport", "Alexandria", "US", false], - "ESG": ["Dr. Luis María Argaña International Airport", "Mariscal Estigarribia", "PY", false], - "ESH": ["Brighton City Airport", "Brighton, East Sussex", "GB", false], - "ESK": ["Eskişehir Air Base", "Eskişehir", "TR", false], - "ESL": ["Elista Airport", "Elista", "RU", true], - "ESM": ["Carlos Concha Torres International Airport", "Tachina", "EC", true], - "ESN": ["Easton Airport / Newnam Field", "Easton", "US", false], - "ESO": ["Ohkay Owingeh Airport", "Espanola", "US", false], - "ESR": ["Ricardo García Posada Airport", "El Salvador", "CL", true], - "ESS": ["Essen-Mülheim Airport", "Essen", "DE", false], - "EST": ["Estherville Municipal Airport", "Estherville", "US", false], - "ESU": ["Essaouira-Mogador Airport", "Essaouira", "MA", true], - "ESW": ["Easton State Airport", "Easton", "US", false], - "ETB": ["West Bend Municipal Airport", "West Bend", "US", false], - "ETD": ["Etadunna Airport", "Etadunna", "AU", false], - "ETE": ["Metema Airport", "Metema", "ET", false], - "ETL": ["Svetlaya Airport", "Svetlaya", "RU", false], - "ETM": ["Ramon International Airport", "Eilat", "IL", true], - "ETN": ["Eastland Municipal Airport", "Eastland", "US", false], - "ETR": ["Santa Rosa - Artillery Colonel Victor Larrea International Airport", "Santa Rosa", "EC", true], - "ETS": ["Enterprise Municipal Airport", "Enterprise", "US", false], - "ETZ": ["Metz-Nancy-Lorraine Airport", "Goin", "FR", true], - "EUA": ["Kaufana Airport", "Eua Island", "TO", true], - "EUC": ["Eucla Airport", "", "AU", false], - "EUE": ["Eureka Airport", "Eureka", "US", false], - "EUF": ["Weedon Field", "Eufaula", "US", false], - "EUG": ["Eugene Airport", "Eugene", "US", true], - "EUM": ["Neumünster Airport", "Neumünster", "DE", false], - "EUN": ["Laayoune Hassan I International Airport", "El Aaiún", "EH", true], - "EUO": ["Paratebueno Airport", "Paratebueno", "CO", false], - "EUQ": ["Evelio Javier Airport", "San Jose", "PH", false], - "EUX": ["F. D. Roosevelt Airport", "Oranjestad", "BQ", true], - "EVD": ["Eva Downs Airport", "Eva Downs", "AU", false], - "EVE": ["Harstad/Narvik Airport", "Evenes", "NO", true], - "EVG": ["Sveg Airport", "Sveg", "SE", true], - "EVH": ["Evans Head Aerodrome", "", "AU", false], - "EVM": ["Eveleth–Virginia Municipal Airport", "Eveleth", "US", false], - "EVN": ["Zvartnots International Airport", "Yerevan", "AM", true], - "EVV": ["Evansville Regional Airport", "Evansville", "US", true], - "EVW": ["Evanston-Uinta County Airport-Burns Field", "Evanston", "US", false], - "EVX": ["Évreux-Fauville (BA 105) Air Base", "Fauville, Eure", "FR", false], - "EWB": ["New Bedford Regional Airport", "New Bedford", "US", true], - "EWE": ["Ewer Asmat Airport", "Agats", "ID", false], - "EWI": ["Enarotali Airport", "Enarotali", "ID", false], - "EWK": ["Newton City-County Airport", "Newton", "US", false], - "EWN": ["Coastal Carolina Regional Airport", "New Bern", "US", true], - "EWO": ["Ewo Airport", "Ewo", "CG", false], - "EWR": ["Newark Liberty International Airport", "Newark", "US", true], - "EXI": ["Excursion Inlet Seaplane Base", "Excursion Inlet", "US", true], - "EXM": ["Exmouth Airport", "Exmouth", "AU", false], - "EXT": ["Exeter International Airport", "Exeter, Devon", "GB", true], - "EYA": ["Zeya Airport", "Zeya", "RU", false], - "EYK": ["Beloyarskiy Airport", "", "RU", true], - "EYL": ["Yélimané Airport", "Yélimané", "ML", false], - "EYP": ["El Alcaravan - Yopal Airport", "Yopal", "CO", true], - "EYR": ["Yerington Municipal Airport", "Yerington", "US", false], - "EYS": ["Eliye Springs Airport", "Eliye Springs", "KE", false], - "EYW": ["Key West International Airport", "Key West", "US", true], - "EZE": ["Ezeiza International Airport - Ministro Pistarini", "Buenos Aires (Ezeiza)", "AR", true], - "EZS": ["Elazığ Airport", "Elazığ", "TR", true], - "EZV": ["Berezovo Airport", "", "RU", false], - "FAA": ["Faranah Airport", "Faranah", "GN", false], - "FAB": ["Farnborough Airport", "Farnborough, Hampshire", "GB", false], - "FAC": ["Faaite Airport", "Faaite", "PF", false], - "FAE": ["Vágar Airport", "Vágar", "FO", true], - "FAF": ["Felker Army Air Field", "Newport News (Fort Eustis)", "US", false], - "FAG": ["Fagurhólsmýri Airport", "Fagurhólsmýri", "IS", false], - "FAH": ["Farah Airport", "Farah", "AF", false], - "FAI": ["Fairbanks International Airport", "Fairbanks", "US", true], - "FAK": ["False Island Seaplane Base", "False Island", "US", false], - "FAM": ["Farmington Regional Airport", "Farmington", "US", false], - "FAO": ["Faro - Gago Coutinho International Airport", "Faro", "PT", true], - "FAQ": ["Frieda River Airport", "Frieda River", "PG", false], - "FAR": ["Hector International Airport", "Fargo", "US", true], - "FAT": ["Fresno Yosemite International Airport", "Fresno", "US", true], - "FAU": ["Fahud Airport", "Fahud", "OM", false], - "FAV": ["Fakarava Airport", "", "PF", true], - "FAY": ["Fayetteville Regional Airport - Grannis Field", "Fayetteville", "US", true], - "FAZ": ["Fasa Airport", "Fasa", "IR", false], - "FBA": ["Fonte Boa Airport", "Fonte Boa", "BR", false], - "FBD": ["Fayzabad Airport", "Fayzabad", "AF", true], - "FBE": ["Paulo Abdala Airport", "Francisco Beltrão", "BR", true], - "FBG": ["Simmons Army Air Field", "Fort Bragg", "US", false], - "FBK": ["Ladd Army Airfield", "Fairbanks", "US", false], - "FBL": ["Faribault Municipal Airport-Liz Wall Strohfus Field", "Faribault", "US", false], - "FBM": ["Lubumbashi International Airport", "Lubumbashi", "CD", true], - "FBR": ["Fort Bridger Airport", "Fort Bridger", "US", false], - "FBS": ["Friday Harbor Seaplane Base", "Friday Harbor", "US", true], - "FBY": ["Fairbury Municipal Airport", "Fairbury", "US", false], - "FCA": ["Glacier Park International Airport", "Kalispell", "US", true], - "FCB": ["Ficksburg Sentraoes Airport", "Ficksburg", "ZA", false], - "FCH": ["Fresno Chandler Executive Airport", "Fresno", "US", false], - "FCM": ["Flying Cloud Airport", "Minneapolis", "US", false], - "FCN": ["Sea-Airport Cuxhaven/Nordholz / Nordholz Naval Airbase", "Wurster Nordseeküste", "DE", true], - "FCO": ["Rome–Fiumicino Leonardo da Vinci International Airport", "Rome", "IT", true], - "FCS": ["Butts AAF (Fort Carson) Air Field", "Fort Carson", "US", false], - "FCY": ["Forrest City Municipal Airport", "Forrest City", "US", false], - "FDA": ["Fundación Airport", "Fundación", "CO", false], - "FDE": ["Førde Airport, Bringeland", "Førde", "NO", true], - "FDF": ["Martinique Aimé Césaire International Airport", "Fort-de-France", "MQ", true], - "FDH": ["Bodensee Airport Friedrichshafen", "Friedrichshafen", "DE", true], - "FDK": ["Frederick Municipal Airport", "Frederick", "US", false], - "FDR": ["Frederick Regional Airport", "Frederick", "US", false], - "FDU": ["Bandundu Airport", "Bandundu", "CD", true], - "FDY": ["Findlay Airport", "Findlay", "US", false], - "FEB": ["Sanfebagar Airport", "Sanfebagar", "NP", false], - "FEC": ["João Durval Carneiro Airport", "Feira de Santana", "BR", false], - "FEG": ["Fergana International Airport", "Fergana", "UZ", true], - "FEJ": ["Feijó Airport", "Feijó", "BR", false], - "FEN": ["Fernando de Noronha Airport", "Fernando de Noronha", "BR", true], - "FEP": ["Albertus Airport", "Freeport", "US", false], - "FET": ["Fremont Municipal Airport", "Fremont", "US", false], - "FEZ": ["Fes Saïss International Airport", "Saïss", "MA", true], - "FFA": ["First Flight Airport", "Kill Devil Hills", "US", false], - "FFD": ["RAF Fairford", "Fairford, Gloucestershire", "GB", false], - "FFL": ["Fairfield Municipal Airport", "Fairfield", "US", false], - "FFM": ["Fergus Falls Municipal Airport - Einar Mickelson Field", "Fergus Falls", "US", false], - "FFO": ["Wright-Patterson Air Force Base", "Dayton", "US", false], - "FFT": ["Capital City Airport", "Frankfort", "US", false], - "FFU": ["Futaleufú Airport", "Futaleufu", "CL", false], - "FGD": ["Fderik Airport", "Fderik", "MR", false], - "FGI": ["Fagali'i Airport", "Apia", "WS", false], - "FGL": ["Fox Heliport", "Fox", "NZ", false], - "FGU": ["Fangatau Airport", "Fangatau", "PF", true], - "FHU": ["Sierra Vista Municipal Airport / Libby Army Air Field", "Fort Huachuca / Sierra Vista", "US", false], - "FHZ": ["Fakahina Airport", "Fakahina", "PF", true], - "FID": ["Elizabeth Field", "Fishers Island", "US", false], - "FIE": ["Fair Isle Airport", "Fair Isle", "GB", true], - "FIG": ["Fria Airport", "Fria", "GN", false], - "FIH": ["Ndjili International Airport", "Kinshasa", "CD", true], - "FIK": ["Finke Airport", "Finke", "AU", false], - "FIL": ["Fillmore Municipal Airport", "Fillmore", "US", false], - "FIN": ["Finschhafen Airport", "Buki", "PG", false], - "FIZ": ["Fitzroy Crossing Airport", "", "AU", true], - "FJR": ["Fujairah International Airport", "Fujairah", "AE", true], - "FKB": ["Karlsruhe Baden-Baden Airport", "Rheinmünster", "DE", true], - "FKI": ["Bangoka International Airport", "Kisangani", "CD", true], - "FKJ": ["Fukui Airport", "Fukui", "JP", false], - "FKL": ["Venango Regional Airport", "Franklin", "US", false], - "FKN": ["Franklin Regional Airport", "Franklin", "US", false], - "FKQ": ["Fakfak Airport", "Fakfak", "ID", true], - "FKS": ["Fukushima Airport", "Sukagawa", "JP", true], - "FLA": ["Gustavo Artunduaga Paredes Airport", "Florencia", "CO", true], - "FLB": ["Cangapara Airport", "Floriano", "BR", false], - "FLD": ["Fond du Lac County Airport", "Fond du Lac", "US", false], - "FLF": ["Flensburg-Schäferhaus Airport", "Flensburg", "DE", false], - "FLG": ["Flagstaff Pulliam Airport", "Flagstaff", "US", true], - "FLH": ["Flotta Isle Airstrip", "Flotta Isle", "GB", false], - "FLL": ["Fort Lauderdale Hollywood International Airport", "Fort Lauderdale", "US", true], - "FLM": ["Aeródromo de Filadelfia", "Filadelfia", "PY", false], - "FLN": ["Hercílio Luz International Airport", "Florianópolis", "BR", true], - "FLO": ["Florence Regional Airport", "Florence", "US", true], - "FLP": ["Marion County Regional Airport", "Flippin", "US", false], - "FLR": ["Florence Airport, Peretola", "Firenze (FI)", "IT", true], - "FLS": ["Flinders Island Airport", "Whitemark", "AU", true], - "FLT": ["Flat Airport", "Flat", "US", false], - "FLV": ["Sherman Army Air Field", "Fort Leavenworth", "US", false], - "FLW": ["Flores Airport", "Santa Cruz das Flores", "PT", true], - "FLX": ["Fallon Municipal Airport", "Fallon", "US", false], - "FLY": ["Finley Airport", "", "AU", false], - "FLZ": ["Dr. Ferdinand Lumban Tobing Airport", "Sibolga (Pinangsori)", "ID", true], - "FMA": ["Formosa National Airport", "Formosa", "AR", true], - "FME": ["Fort Meade Executive Airport", "Fort Meade(Odenton)", "US", false], - "FMG": ["Flamingo Airport", "Brasilito", "CR", false], - "FMH": ["Cape Cod Coast Guard Air Station", "Falmouth", "US", false], - "FMI": ["Kalemie Airport", "Kalemie", "CD", true], - "FMM": ["Memmingen Allgau Airport", "Memmingen", "DE", true], - "FMN": ["Four Corners Regional Airport", "Farmington", "US", false], - "FMO": ["Münster Osnabrück Airport", "Greven", "DE", true], - "FMS": ["Fort Madison Municipal Airport", "Fort Madison", "US", false], - "FMT": ["Faresmaathoda Airport", "Faresmaathodaa", "MV", true], - "FMU": ["Florence Municipal Airport", "Florence", "US", false], - "FMY": ["Page Field", "Fort Myers", "US", false], - "FNA": ["Lungi International Airport", "Freetown (Lungi-Town)", "SL", true], - "FNB": ["Neubrandenburg Trollenhagen Airport", "Trollenhagen", "DE", false], - "FNC": ["Cristiano Ronaldo International Airport", "Funchal", "PT", true], - "FND": ["Funadhoo Airport", "Funadhoo", "MV", true], - "FNE": ["Fane Airport", "Fane Mission", "PG", false], - "FNG": ["Fada N'gourma Airport", "Fada N'gourma", "BF", false], - "FNH": ["Fincha Airport", "Fincha", "ET", false], - "FNI": ["Nîmes-Arles-Camargue Airport", "Nîmes/Garons", "FR", true], - "FNJ": ["Pyongyang Sunan International Airport", "Pyongyang", "KP", true], - "FNL": ["Northern Colorado Regional Airport", "Loveland", "US", false], - "FNR": ["Funter Bay Seaplane Base", "Funter Bay", "US", true], - "FNT": ["Bishop International Airport", "Flint", "US", true], - "FNU": ["Oristano-Fenosu Airport", "Oristano", "IT", false], - "FOA": ["Foula Airfield", "Foula", "GB", true], - "FOB": ["Fort Bragg Airport", "Fort Bragg", "US", false], - "FOC": ["Fuzhou Changle International Airport", "Fuzhou (Changle)", "CN", true], - "FOD": ["Fort Dodge Regional Airport", "Fort Dodge", "US", true], - "FOE": ["Topeka Regional Airport", "Topeka", "US", false], - "FOG": ["Foggia Gino Lisa Airport", "Foggia (FG)", "IT", true], - "FOK": ["Francis S Gabreski Airport", "Westhampton Beach", "US", false], - "FOM": ["Foumban Nkounja Airport", "Foumban", "CM", false], - "FON": ["La Fortuna Arenal Airport", "La Fortuna", "CR", true], - "FOO": ["Kornasoren Airport", "Nomfor", "ID", false], - "FOR": ["Pinto Martins International Airport", "Fortaleza", "BR", true], - "FOS": ["Forrest Airport", "", "AU", false], - "FOT": ["Forster (Wallis Is) Airport", "", "AU", false], - "FOU": ["Fougamou Airport", "Fougamou", "GA", false], - "FOY": ["Foya Airport", "Foya", "LR", false], - "FPO": ["Grand Bahama International Airport", "Freeport", "BS", true], - "FPR": ["Treasure Coast International Airport", "Fort Pierce", "US", false], - "FPY": ["Perry-Foley Airport", "Perry", "US", false], - "FRA": ["Frankfurt Main Airport", "Frankfurt am Main", "DE", true], - "FRB": ["Forbes Airport", "Forbes", "AU", false], - "FRC": ["Tenente Lund Pressoto Airport", "Franca", "BR", false], - "FRD": ["Friday Harbor Airport", "Friday Harbor", "US", true], - "FRE": ["Fera/Maringe Airport", "Fera Island", "SB", true], - "FRG": ["Republic Airport", "East Farmingdale", "US", false], - "FRH": ["French Lick Municipal Airport", "French Lick", "US", false], - "FRI": ["Marshall Army Air Field", "Fort Riley (Junction City)", "US", false], - "FRK": ["Frégate Island Airport (UNUSABLE)", "Frégate Island", "SC", false], - "FRL": ["Forlì-Luigi Ridolfi International Airport", "Forlì (FC)", "IT", true], - "FRM": ["Fairmont Municipal Airport", "Fairmont", "US", false], - "FRN": ["Bryant Army Airfield", "Fort Richardson", "US", false], - "FRO": ["Florø Airport", "Florø", "NO", true], - "FRQ": ["Feramin Airport", "Feramin", "PG", false], - "FRR": ["Front Royal Warren County Airport", "Front Royal", "US", false], - "FRS": ["Mundo Maya International Airport", "San Benito", "GT", true], - "FRT": ["El Avellano Airport", "Frutillar", "CL", false], - "FRW": ["Phillip Gaonwe Matante International Airport", "Francistown", "BW", true], - "FRY": ["Eastern Slopes Regional Airport", "Fryeburg", "US", false], - "FRZ": ["Fritzlar Army Airfield", "Fritzlar", "DE", false], - "FSC": ["Figari Sud-Corse Airport", "Figari", "FR", true], - "FSD": ["Sioux Falls Regional Airport", "Sioux Falls", "US", true], - "FSH": ["Syekh Hamzah Fansyuri Airport", "Singkil", "ID", true], - "FSI": ["Henry Post Army Air Field", "Fort Sill", "US", false], - "FSK": ["Fort Scott Municipal Airport", "Fort Scott", "US", false], - "FSL": ["Fossil Downs Airport", "Fossil Downs Station", "AU", false], - "FSM": ["Fort Smith Regional Airport", "Fort Smith", "US", true], - "FSP": ["Saint-Pierre Pointe-Blanche Airport", "Saint-Pierre", "PM", true], - "FST": ["Fort Stockton Pecos County Airport", "Fort Stockton", "US", false], - "FSU": ["Fort Sumner Municipal Airport", "Fort Sumner", "US", false], - "FSZ": ["Mount Fuji Shizuoka Airport", "Makinohara / Shimada", "JP", true], - "FTA": ["Futuna Airport", "Futuna Island", "VU", true], - "FTE": ["El Calafate - Commander Armando Tola International Airport", "El Calafate", "AR", true], - "FTI": ["Fitiuta Airport", "Fitiuta Village", "AS", true], - "FTK": ["Godman Army Air Field", "Fort Knox", "US", false], - "FTU": ["Tôlanaro Airport", "Tôlanaro", "MG", true], - "FTW": ["Fort Worth Meacham International Airport", "Fort Worth", "US", true], - "FTX": ["Owando Airport", "Owando", "CG", false], - "FTY": ["Fulton County Airport Brown Field", "Atlanta", "US", false], - "FUB": ["Fulleborn Airport", "Fulleborn", "PG", false], - "FUE": ["Fuerteventura Airport", "El Matorral", "ES", true], - "FUG": ["Fuyang Xiguan Airport", "Yingzhou, Fuyang", "CN", true], - "FUJ": ["Fukue Airport", "Goto", "JP", true], - "FUK": ["Fukuoka Airport", "Fukuoka", "JP", true], - "FUL": ["Fullerton Municipal Airport", "Fullerton", "US", false], - "FUM": ["Fuma Airport", "Fuma", "PG", false], - "FUN": ["Funafuti International Airport", "Funafuti", "TV", true], - "FUO": ["Foshan Shadi Airport", "Foshan (Nanhai)", "CN", true], - "FUT": ["Pointe Vele Airport", "Futuna Island", "WF", true], - "FVL": ["Flora Valley Airport", "", "AU", false], - "FVM": ["Fuvahmulah Airport", "Fuvahmulah Island", "MV", false], - "FVR": ["Oombulgurri Airport", "Oombulgurri", "AU", false], - "FWA": ["Fort Wayne International Airport", "Fort Wayne", "US", true], - "FWH": ["NAS Fort Worth JRB / Carswell Field", "Fort Worth", "US", false], - "FWL": ["Farewell Airport", "Farewell", "US", false], - "FXE": ["Fort Lauderdale Executive Airport", "Fort Lauderdale", "US", false], - "FXO": ["Cuamba Airport", "Cuamba", "MZ", false], - "FXY": ["Forest City Municipal Airport", "Forest City", "US", false], - "FYJ": ["Fuyuan Dongji Airport", "Fuyuan", "CN", true], - "FYM": ["Fayetteville Municipal Airport", "Fayetteville", "US", false], - "FYN": ["Fuyun Koktokay Airport", "Fuyun", "CN", true], - "FYT": ["Faya-Largeau Airport", "Faya-Largeau", "TD", false], - "FYU": ["Fort Yukon Airport", "Fort Yukon", "US", true], - "FYV": ["Drake Field", "Fayetteville", "US", false], - "FZL": ["Fuzuli International Airport", "Fuzuli", "AZ", false], - "GAA": ["Guamal Airport", "Guamal", "CO", false], - "GAB": ["Gabbs Airport", "Gabbs", "US", false], - "GAC": ["Celaque Gracias Airport", "Gracias", "HN", false], - "GAD": ["Northeast Alabama Regional Airport", "Gadsden", "US", false], - "GAE": ["Gabès Matmata International Airport", "Gabès", "TN", false], - "GAF": ["Gafsa Ksar International Airport", "Gafsa", "TN", false], - "GAG": ["Gage Heibeck Airport", "Gage", "US", false], - "GAH": ["Gayndah Airport", "Gayndah", "AU", false], - "GAI": ["Montgomery County Airpark", "Gaithersburg", "US", false], - "GAJ": ["Yamagata Airport", "Higashine", "JP", true], - "GAL": ["Edward G. Pitka Sr Airport", "Galena", "US", true], - "GAM": ["Gambell Airport", "Gambell", "US", true], - "GAN": ["Gan International Airport", "Gan", "MV", true], - "GAO": ["Mariana Grajales Airport", "Guantánamo", "CU", false], - "GAP": ["Gusap Airport", "Gusap", "PG", false], - "GAQ": ["Gao International Airport", "Gao", "ML", true], - "GAR": ["Garaina Airport", "Garaina", "PG", false], - "GAS": ["Garissa Airport", "Garissa", "KE", false], - "GAT": ["Gap-Tallard Airfield", "Tallard, Hautes-Alpes", "FR", false], - "GAU": ["Lokpriya Gopinath Bordoloi International Airport", "Guwahati", "IN", true], - "GAV": ["Gag Island Airport", "Gag Island", "ID", false], - "GAW": ["Gangaw Airport", "Gangaw", "MM", false], - "GAX": ["Gamba Airport", "Gamba", "GA", true], - "GAY": ["Gaya Airport", "Gaya", "IN", true], - "GAZ": ["Guasopa Airport", "Woodlark (Muyua) Island", "PG", false], - "GBA": ["Cotswold Airport", "Cirencester, Gloucestershire", "GB", false], - "GBB": ["Gabala International Airport", "Gabala", "AZ", true], - "GBC": ["Gasuke Airport", "Gasuke", "PG", false], - "GBD": ["Great Bend Municipal Airport", "Great Bend", "US", false], - "GBE": ["Sir Seretse Khama International Airport", "Gaborone", "BW", true], - "GBF": ["Negarbo(Negabo) Airport", "Negarbo", "PG", false], - "GBG": ["Galesburg Regional HW Timmons Airport", "Galesburg", "US", false], - "GBH": ["Galbraith Lake Airport", "Galbraith Lake", "US", false], - "GBI": ["Kalaburagi Airport", "Kalaburagi", "IN", true], - "GBJ": ["Marie-Galante Airport", "Grand-Bourg", "GP", true], - "GBK": ["Gbangbatok Airport", "Gbangbatok", "SL", false], - "GBL": ["South Goulburn Is Airport", "", "AU", false], - "GBM": ["Garbaharey Airport", "Garbaharey", "SO", false], - "GBP": ["Gamboola Airport", "Gamboola", "AU", false], - "GBR": ["Walter J. Koladza Airport", "Great Barrington", "US", false], - "GBT": ["Gorgan Airport", "Gorgan", "IR", false], - "GBU": ["Khashm El Girba Airport", "Khashm El Girba", "SD", false], - "GBV": ["Gibb River Airport", "Gibb", "AU", false], - "GBW": ["Ginbata Airport", "Ginbata", "AU", false], - "GBZ": ["Great Barrier Aerodrome", "Claris", "NZ", true], - "GCA": ["Guacamayas Airport", "Guacamayas", "CO", false], - "GCC": ["Northeast Wyoming Regional Airport", "Gillette", "US", true], - "GCD": ["Grand Coulee Dam Airport", "Electric City", "US", false], - "GCH": ["Gachsaran Airport", "Gachsaran", "IR", true], - "GCI": ["Guernsey Airport", "Saint Peter Port", "GG", true], - "GCJ": ["Grand Central Airport", "Midrand", "ZA", false], - "GCK": ["Garden City Regional Airport", "Garden City", "US", true], - "GCM": ["Owen Roberts International Airport", "George Town", "KY", true], - "GCN": ["Grand Canyon National Park Airport", "Grand Canyon - Tusayan", "US", true], - "GCT": ["Grand Canyon Bar Ten Airstrip", "Littlefield", "US", false], - "GCV": ["Gravatai Airport", "Gravatai", "BR", false], - "GCW": ["Grand Canyon West Airport", "Peach Springs", "US", false], - "GCY": ["Greeneville Municipal Airport", "Greeneville", "US", false], - "GDB": ["Gondia Airport", "Gondia", "IN", true], - "GDC": ["Donaldson Field Airport", "Greenville", "US", false], - "GDD": ["Gordon Downs Airport", "Gordon Downs", "AU", false], - "GDE": ["Gode Airport", "Gode", "ET", true], - "GDH": ["Golden Horn Lodge Seaplane Base", "Golden Horn Lodge", "US", false], - "GDI": ["Gordil Airport", "Melle", "CF", false], - "GDL": ["Guadalajara International Airport", "Guadalajara", "MX", true], - "GDM": ["Gardner Municipal Airport", "Gardner", "US", false], - "GDN": ["Gdańsk Lech Wałęsa Airport", "Gdańsk", "PL", true], - "GDO": ["Guasdualito Airport", "Guasdualito", "VE", false], - "GDP": ["Guadalupe Airport", "Guadalupe", "BR", false], - "GDQ": ["Gondar Airport", "Azezo", "ET", true], - "GDR": ["Angra dos Reis Airport", "Angra dos Reis", "BR", false], - "GDT": ["JAGS McCartney International Airport", "Cockburn Town", "TC", true], - "GDV": ["Dawson Community Airport", "Glendive", "US", true], - "GDW": ["Gladwin Zettel Memorial Airport", "Gladwin", "US", false], - "GDX": ["Sokol Airport", "Magadan", "RU", true], - "GDZ": ["Gelendzhik Airport", "Gelendzhik", "RU", true], - "GEA": ["Nouméa Magenta Airport", "Nouméa", "NC", true], - "GEB": ["Gebe Airport", "Gebe Island", "ID", false], - "GEC": ["Geçitkale Airbase", "Lefkoniko (Geçitkale)", "CY", false], - "GED": ["Delaware Coastal Airport", "Georgetown", "US", false], - "GEE": ["George Town Airport", "George Town", "AU", false], - "GEF": ["Geva Airport", "Liangia", "SB", false], - "GEG": ["Spokane International Airport", "Spokane", "US", true], - "GEL": ["Santo Ângelo Airport", "Santo Ângelo", "BR", true], - "GEM": ["President Obiang Nguema International Airport", "Mengomeyén", "GQ", true], - "GEO": ["Cheddi Jagan International Airport", "Georgetown", "GY", true], - "GER": ["Rafael Cabrera Airport", "Nueva Gerona", "CU", true], - "GES": ["General Santos International Airport", "General Santos", "PH", true], - "GET": ["Geraldton Airport", "Moonyoonooka", "AU", true], - "GEV": ["Gällivare Airport", "Gällivare", "SE", true], - "GEW": ["Gewoia Airport", "Gewoia", "PG", false], - "GEY": ["South Big Horn County Airport", "Greybull", "US", false], - "GFA": ["Malmstrom Air Force Base", "Great Falls", "US", false], - "GFD": ["Pope Field", "Greenfield", "US", false], - "GFE": ["Grenfell Airport", "Grenfell", "AU", false], - "GFF": ["Griffith Airport", "Griffith", "AU", true], - "GFK": ["Grand Forks International Airport", "Grand Forks", "US", true], - "GFL": ["Floyd Bennett Memorial Airport", "Glens Falls", "US", false], - "GFN": ["Clarence Valley Regional Airport", "Grafton", "AU", false], - "GFO": ["Bartica A Airport", "Bartica", "GY", false], - "GFR": ["Granville Airport", "Bréville-sur-Mer, Manche", "FR", false], - "GFY": ["Grootfontein Airport", "Grootfontein", "NA", false], - "GGB": ["Frederico Carlos Müller Airport", "Água Boa", "BR", false], - "GGC": ["Lumbala Airport", "Lumbala N'guimbo", "AO", false], - "GGD": ["Gregory Downs Airport", "", "AU", false], - "GGE": ["Georgetown County Airport", "Georgetown", "US", false], - "GGF": ["Almeirim Airport", "Almeirim", "BR", true], - "GGG": ["East Texas Regional Airport", "Longview", "US", true], - "GGH": ["Engenheiro Gastão de Mesquita Filho Airport", "Cianorte", "BR", false], - "GGJ": ["Guaíra Airport", "Guaíra", "BR", true], - "GGM": ["Kakamega Airport", "Kakamega", "KE", false], - "GGN": ["Gagnoa Airport", "Gagnoa", "CI", false], - "GGO": ["Guiglo Airport", "Guiglo", "CI", false], - "GGR": ["Garowe Airport", "Garowe", "SO", true], - "GGS": ["Gobernador Gregores Airport", "Gobernador Gregores", "AR", true], - "GGT": ["Exuma International Airport", "Moss Town", "BS", true], - "GGW": ["Glasgow Valley County Airport Wokal Field", "Glasgow", "US", true], - "GHA": ["Noumérat - Moufdi Zakaria Airport", "El Atteuf", "DZ", true], - "GHB": ["Governor's Harbour Airport", "Governor's Harbour", "BS", true], - "GHC": ["Great Harbour Cay Airport", "Bullocks Harbour", "BS", false], - "GHE": ["Garachiné Airport", "Garachiné", "PA", false], - "GHF": ["Giebelstadt Airport", "Giebelstadt", "DE", false], - "GHK": ["Gahcho Kue", "Kennedy Lake", "CA", false], - "GHM": ["Centerville Municipal Airport", "Centerville", "US", false], - "GHN": ["Guanghan Airport", "Deyang (Guanghan)", "CN", false], - "GHS": ["Melalan Airport", "Melak-Borneo Island", "ID", false], - "GHT": ["Ghat Airport", "Ghat", "LY", false], - "GHU": ["Gualeguaychu Airport", "Gualeguaychu", "AR", false], - "GHV": ["Brașov-Ghimbav International Airport", "Brașov (Ghimbav)", "RO", true], - "GIB": ["Gibraltar Airport", "Gibraltar", "GI", true], - "GIC": ["Boigu Island Airport", "Boigu Island", "AU", true], - "GID": ["Gitega Airport", "Gitega", "BI", true], - "GIF": ["Winter Haven Regional Airport - Gilbert Field", "Winter Haven", "US", false], - "GIG": ["Rio de Janeiro Galeão – Tom Jobim International Airport", "Rio De Janeiro", "BR", true], - "GII": ["Siguiri Airport", "Siguiri", "GN", false], - "GIL": ["Gilgit Airport", "Gilgit", "PK", true], - "GIM": ["Miele Mimbale Airport", "Miele Mimbale", "GA", false], - "GIR": ["Santiago Vila Airport", "Girardot", "CO", false], - "GIS": ["Gisborne Airport", "Gisborne", "NZ", true], - "GIT": ["Mchauru Airport", "Geita", "TZ", false], - "GIU": ["Sigiriya Air Force Base", "Sigiriya", "LK", false], - "GIY": ["Giyani Airport", "Giyani", "ZA", false], - "GIZ": ["Jizan Regional Airport / King Abdullah bin Abdulaziz Airport", "Jizan", "SA", true], - "GJA": ["La Laguna Airport", "Guanaja", "HN", true], - "GJL": ["Jijel Ferhat Abbas Airport", "Tahir", "DZ", true], - "GJM": ["Guajará-Mirim Airport", "Guajará-Mirim", "BR", false], - "GJR": ["Gjögur Airport", "Gjögur", "IS", false], - "GJT": ["Grand Junction Regional Airport", "Grand Junction", "US", true], - "GKA": ["Goroka Airport", "Goronka", "PG", true], - "GKD": ["Gökçeada Airport", "Gökçeada", "TR", false], - "GKE": ["Geilenkirchen Air Base", "Geilenkirchen", "DE", false], - "GKH": ["Palungtar Airport", "Gorkha", "NP", false], - "GKK": ["Kooddoo Airport", "Huvadhu Atoll", "MV", true], - "GKL": ["Great Keppel Is Airport", "Great Keppel Island", "AU", false], - "GKN": ["Gulkana Airport", "Gulkana", "US", true], - "GKT": ["Gatlinburg-Pigeon Forge Airport", "Sevierville", "US", false], - "GLA": ["Glasgow Airport", "Glasgow", "GB", true], - "GLB": ["San Carlos Apache Airport", "Globe", "US", false], - "GLC": ["Geladi Airport", "Geladi", "ET", false], - "GLD": ["Goodland Municipal Airport", "Goodland", "US", false], - "GLE": ["Gainesville Municipal Airport", "Gainesville", "US", false], - "GLF": ["Golfito Airport", "Golfito", "CR", true], - "GLG": ["Glengyle Airport", "", "AU", false], - "GLH": ["Mid Delta Regional Airport", "Greenville", "US", true], - "GLI": ["Glen Innes Airport", "Glen Innes", "AU", false], - "GLK": ["Galcaio Airport", "Galcaio", "SO", true], - "GLL": ["Gol Airport", "Klanten flyplass", "NO", false], - "GLM": ["Glenormiston Airport", "", "AU", false], - "GLN": ["Guelmim Airport", "Guelmim", "MA", false], - "GLO": ["Gloucestershire Airport", "Staverton, Gloucestershire", "GB", false], - "GLP": ["Gulgubip Airport", "Gulgubip", "PG", false], - "GLR": ["Gaylord Regional Airport", "Gaylord", "US", false], - "GLS": ["Scholes International At Galveston Airport", "Galveston", "US", false], - "GLT": ["Gladstone Airport", "Gladstone", "AU", true], - "GLU": ["Gelephu Airport", "Gelephu", "BT", false], - "GLV": ["Golovin Airport", "Golovin", "US", true], - "GLW": ["Glasgow Municipal Airport", "Glasgow", "US", false], - "GLX": ["Gamarmalamo Airport", "Galela", "ID", false], - "GLZ": ["Gilze Rijen Air Base", "Rijen", "NL", false], - "GMA": ["Gemena Airport", "Gemena", "CD", true], - "GMB": ["Gambela Airport", "Gambela", "ET", true], - "GMC": ["Guerima Airport", "Puerto Carreño", "CO", false], - "GMD": ["Ben Slimane Airport", "Ben Slimane", "MA", false], - "GME": ["Gomel Airport", "Gomel", "BY", true], - "GMI": ["Gasmata Island Airport", "Gasmata Island", "PG", true], - "GMM": ["Gamboma Airport", "Gamboma", "CG", false], - "GMN": ["Greymouth Airport", "", "NZ", false], - "GMO": ["Gombe Lawanti International Airport", "Gombe", "NG", true], - "GMP": ["Seoul Gimpo International Airport", "Seoul", "KR", true], - "GMQ": ["Golog Maqên Airport", "Golog (Maqên)", "CN", true], - "GMR": ["Totegegie Airport", "", "PF", true], - "GMS": ["Antônio Guerreiro Airport", "Guimarães", "BR", false], - "GMT": ["Granite Mountain Air Station", "Granite Mountain", "US", false], - "GMU": ["Greenville Downtown Airport", "Greenville", "US", false], - "GMV": ["Monument Valley Airport", "Oljato-Monument Valley", "US", false], - "GMZ": ["La Gomera Airport", "Alajero, La Gomera Island", "ES", true], - "GNA": ["Hrodna Airport", "Hrodna", "BY", false], - "GNB": ["Grenoble Alpes Isère Airport", "Grenoble", "FR", true], - "GND": ["Maurice Bishop International Airport", "Saint George's", "GD", true], - "GNE": ["Ghent/Industry Zone Heliport", "Gent", "BE", false], - "GNF": ["Gansner Field", "Quincy", "US", false], - "GNG": ["Gooding Municipal Airport", "Gooding", "US", false], - "GNI": ["Lüdao Airport", "Lüdao", "TW", false], - "GNJ": ["Ganja International Airport", "Ganja", "AZ", true], - "GNM": ["Guanambi Airport", "Guanambi", "BR", false], - "GNN": ["Ghinnir Airport", "Ghinnir", "ET", false], - "GNR": ["Dr. Arturo H. Illia Airport", "General Roca", "AR", false], - "GNS": ["Binaka Airport", "Gunungsitoli", "ID", true], - "GNT": ["Grants-Milan Municipal Airport", "Grants", "US", false], - "GNU": ["Goodnews Airport", "Goodnews", "US", true], - "GNV": ["Gainesville Regional Airport", "Gainesville", "US", true], - "GNY": ["Şanlıurfa GAP Airport", "Şanlıurfa", "TR", true], - "GNZ": ["Ghanzi Airport", "Ghanzi", "BW", false], - "GOA": ["Genoa Cristoforo Colombo Airport", "Genova (GE)", "IT", true], - "GOB": ["Robe Airport", "Goba", "ET", false], - "GOC": ["Gora Airstrip", "Gora", "PG", false], - "GOE": ["Gonaili Airport", "Gonaili", "PG", false], - "GOG": ["Gobabis Airport", "Gobabis", "NA", false], - "GOH": ["Nuuk International Airport", "Nuuk", "GL", true], - "GOI": ["Goa Dabolim International Airport", "Vasco da Gama", "IN", true], - "GOJ": ["Nizhny Novgorod / Strigino International Airport", "Nizhny Novgorod", "RU", true], - "GOK": ["Guthrie-Edmond Regional Airport", "Guthrie", "US", false], - "GOL": ["Gold Beach Municipal Airport", "Gold Beach", "US", false], - "GOM": ["Goma International Airport", "Goma", "CD", true], - "GON": ["Groton New London Airport", "Groton", "US", false], - "GOO": ["Goondiwindi Airport", "", "AU", false], - "GOP": ["Gorakhpur Airport", "Gorakhpur", "IN", true], - "GOQ": ["Golmud Airport", "Golmud", "CN", true], - "GOR": ["Gore Airport", "Gore", "ET", false], - "GOS": ["Somersby Airstrip", "Gosford", "AU", false], - "GOT": ["Göteborg Landvetter Airport", "Göteborg", "SE", true], - "GOU": ["Garoua International Airport", "Garoua", "CM", true], - "GOV": ["Gove Airport", "Nhulunbuy", "AU", true], - "GOX": ["Manohar International Airport", "Mopa", "IN", true], - "GOY": ["Tura Mountain Airport", "Tura", "RU", true], - "GOZ": ["Gorna Oryahovitsa Airport", "Gorna Oryahovitsa", "BG", false], - "GPA": ["Patras Araxos Agamemnon Airport", "Patras", "GR", true], - "GPB": ["Tancredo Thomas de Faria Airport", "Guarapuava", "BR", false], - "GPD": ["Mount Gordon Airport", "Mount Gordon Mine", "AU", false], - "GPI": ["Guapi Airport", "Guapi", "CO", true], - "GPL": ["Guapiles Airport", "Pococi", "CR", false], - "GPN": ["Garden Point Airport", "Pirlangimpi", "AU", false], - "GPO": ["General Pico Airport", "General Pico", "AR", false], - "GPS": ["Seymour Galapagos Ecological Airport", "Isla Baltra", "EC", true], - "GPT": ["Gulfport Biloxi International Airport", "Gulfport", "US", true], - "GPZ": ["Grand Rapids Itasca Co-Gordon Newstrom field", "Grand Rapids", "US", false], - "GQQ": ["Galion Municipal Airport", "Galion", "US", false], - "GRA": ["Gamarra Airport", "Puerto Mosquito", "CO", false], - "GRB": ["Austin Straubel International Airport", "Green Bay", "US", true], - "GRC": ["Grand Cess Airport", "Grand Cess", "LR", false], - "GRD": ["Greenwood County Airport", "Greenwood", "US", false], - "GRE": ["Greenville Airport", "Greenville", "US", false], - "GRF": ["Gray Army Air Field", "Joint Base Lewis McChord", "US", false], - "GRG": ["Gardez Airport", "Gardez", "AF", false], - "GRI": ["Central Nebraska Regional Airport", "Grand Island", "US", true], - "GRJ": ["George Airport", "George", "ZA", true], - "GRK": ["Killeen Regional Airport / Robert Gray Army Airfield", "Fort Cavazos", "US", true], - "GRL": ["Garasa Airport", "Au", "PG", false], - "GRM": ["Grand Marais Cook County Airport", "Grand Marais", "US", false], - "GRN": ["Gordon Municipal Airport", "Gordon", "US", false], - "GRO": ["Girona-Costa Brava Airport", "Girona", "ES", true], - "GRP": ["Gurupi Airport", "Gurupi", "BR", false], - "GRQ": ["Groningen Airport Eelde", "Groningen", "NL", true], - "GRR": ["Gerald R. Ford International Airport", "Grand Rapids", "US", true], - "GRS": ["Grosseto Corrado Baccarini Air Base / Grosseto Airport", "Grosseto (GR)", "IT", false], - "GRT": ["Gujrat Airport", "Gujrat", "PK", false], - "GRU": ["São Paulo/Guarulhos–Governor André Franco Montoro International Airport", "São Paulo", "BR", true], - "GRV": ["Akhmat Kadyrov Grozny International Airport", "Grozny", "RU", true], - "GRW": ["Graciosa Airport", "Santa Cruz da Graciosa", "PT", true], - "GRX": ["F.G.L. Airport Granada-Jaén Airport", "Granada", "ES", true], - "GRY": ["Grímsey Airport", "Grímsey/Sandvík", "IS", true], - "GRZ": ["Graz Airport", "Feldkirchen bei Graz", "AT", true], - "GSA": ["Long Pasia Airport", "Long Miau", "MY", false], - "GSB": ["Seymour Johnson Air Force Base", "Goldsboro", "US", false], - "GSC": ["Gascoyne Junction Airport", "Gascoyne Junction", "AU", false], - "GSE": ["Säve Airport", "Göteborg", "SE", false], - "GSH": ["Goshen Municipal Airport", "Goshen", "US", false], - "GSI": ["Grand-Santi Airport", "Grand-Santi", "GF", false], - "GSJ": ["San José Airport", "Puerto San José", "GT", false], - "GSL": ["Taltheilei Narrows Airport", "Taltheilei Narrows", "CA", false], - "GSM": ["Qeshm International Airport", "Qeshm(Dayrestan)", "IR", true], - "GSN": ["Mount Gunson Airport", "Mount Gunson", "AU", false], - "GSO": ["Piedmont Triad International Airport", "Greensboro", "US", true], - "GSP": ["Greenville-Spartanburg International Airport", "Greenville/Greer/Spartanburg", "US", true], - "GSQ": ["El Owainat East International Airport", "Sharq El Owainat", "EG", false], - "GSR": ["Gardo Airport", "Gardo", "SO", false], - "GSS": ["Sabi Sabi Airport", "Belfast", "ZA", false], - "GST": ["Gustavus Airport", "Gustavus", "US", true], - "GSV": ["Gagarin International Airport", "Saratov", "RU", true], - "GTA": ["Gatokae Aerodrome", "Gatokae", "SB", true], - "GTC": ["Chato/Geita Airport", "Chato District", "TZ", false], - "GTE": ["Groote Eylandt Airport", "Groote Eylandt", "AU", true], - "GTF": ["Great Falls International Airport", "Great Falls", "US", true], - "GTG": ["Grantsburg Municipal Airport", "Grantsburg", "US", false], - "GTI": ["Rügen Airport", "Dreschvitz", "DE", false], - "GTK": ["Sungai Tekai Airport", "Sungai Tekai", "MY", false], - "GTN": ["Glentanner Airport", "Glentanner Station", "NZ", false], - "GTO": ["Jalaluddin Airport", "Gorontalo", "ID", true], - "GTP": ["Grants Pass Airport", "Grants Pass", "US", false], - "GTR": ["Golden Triangle Regional Airport", "Columbus/W Point/Starkville", "US", true], - "GTS": ["The Granites Airport", "The Granites Gold Mine", "AU", false], - "GTT": ["Georgetown Airport", "Georgetown", "AU", false], - "GTY": ["Gettysburg Regional Airport", "Gettysburg", "US", false], - "GTZ": ["Kirawira B Aerodrome", "Grumeti Game Reserve", "TZ", false], - "GUA": ["La Aurora International Airport", "Guatemala City", "GT", true], - "GUB": ["Guerrero Negro Airport", "San Quintín", "MX", true], - "GUC": ["Gunnison Crested Butte Regional Airport", "Gunnison", "US", true], - "GUD": ["Goundam Airport", "Goundam", "ML", false], - "GUE": ["Guriaso (Keraso) Airport", "Guriaso", "PG", false], - "GUF": ["Jack Edwards National Airport", "Gulf Shores", "US", false], - "GUG": ["Guari Airport", "Guari", "PG", false], - "GUH": ["Gunnedah Airport", "", "AU", false], - "GUI": ["Guiria Airport", "", "VE", false], - "GUJ": ["Edu Chaves Field", "Guaratinguetá", "BR", false], - "GUL": ["Goulburn Airport", "", "AU", false], - "GUM": ["Antonio B. Won Pat International Airport", "Hagåtña", "GU", true], - "GUO": ["Guriel Airport", "Guriel", "SO", false], - "GUP": ["Gallup Municipal Airport", "Gallup", "US", true], - "GUQ": ["Guanare Airport", "Guanare", "VE", false], - "GUR": ["Gurney Airport", "Gurney", "PG", true], - "GUS": ["Grissom Air Reserve Base", "Peru", "US", false], - "GUU": ["Grundarfjörður Airport", "Grundarfjörður", "IS", false], - "GUV": ["Mougulu Airport", "Mougulu", "PG", false], - "GUW": ["Atyrau International Airport", "Atyrau", "KZ", true], - "GUX": ["Guna Airport", "", "IN", false], - "GUY": ["Guymon Municipal Airport", "Guymon", "US", false], - "GUZ": ["Guarapari Airport", "Guarapari", "BR", true], - "GVA": ["Geneva International Airport", "Geneva", "CH", true], - "GVE": ["Gordonsville Municipal Airport", "Gordonsville", "US", false], - "GVI": ["Green River Airport", "Green River", "PG", false], - "GVL": ["Lee Gilmer Memorial Airport", "Gainesville", "US", false], - "GVN": ["Sovetskaya Gavan (Maygatka) Airport", "Sovetskaya Gavan", "RU", false], - "GVP": ["Greenvale Airport", "", "AU", false], - "GVR": ["Coronel Altino Machado Airport", "Governador Valadares", "BR", true], - "GVT": ["Majors Airport", "Greenville", "US", false], - "GVX": ["Gävle–Sandviken Airport", "Sandviken", "SE", false], - "GWA": ["Gwa Airport", "Gwa", "MM", false], - "GWD": ["New Gwadar International Airport", "Gurandani", "PK", true], - "GWE": ["Josiah Tungamirai Air Force Base", "Gweru", "ZW", false], - "GWL": ["Gwalior Airport", "Gwalior", "IN", true], - "GWO": ["Greenwood–Leflore Airport", "Greenwood", "US", false], - "GWS": ["Sumers Airpark", "Glenwood Springs", "US", false], - "GWT": ["Westerland Sylt Airport", "Sylt", "DE", true], - "GWV": ["Glendale Fokker Field", "Glendale", "US", false], - "GXF": ["Seiyun Hadhramaut International Airport", "Seiyun", "YE", true], - "GXG": ["Negage Airport", "Negage", "AO", true], - "GXH": ["Gannan Xiahe Airport", "Gannan (Xiahe)", "CN", true], - "GXM": ["Kuala Kurun Airport", "Kuala Kurun", "ID", false], - "GXQ": ["Teniente Vidal Airport", "Coyhaique", "CL", false], - "GXX": ["Yagoua Airport", "Yagoua", "CM", false], - "GXY": ["Greeley–Weld County Airport", "Greeley", "US", false], - "GYA": ["Guayaramerín Airport", "Guayaramerín", "BO", true], - "GYB": ["Port Hedland/Wodgina Airport", "Wodgina", "AU", false], - "GYD": ["Heydar Aliyev International Airport", "Baku", "AZ", true], - "GYE": ["José Joaquín de Olmedo International Airport", "Guayaquil", "EC", true], - "GYG": ["Magan Airport", "Magan", "RU", false], - "GYI": ["Gisenyi Airport", "Gisenyi", "RW", false], - "GYL": ["Argyle Airport", "", "AU", false], - "GYM": ["General José María Yáñez International Airport", "Guaymas", "MX", true], - "GYN": ["Santa Genoveva International Airport", "Goiânia", "BR", true], - "GYO": ["Blangkejeren Airport", "Kabupaten Gayo Lues", "ID", false], - "GYP": ["Gympie Airport", "", "AU", false], - "GYR": ["Phoenix Goodyear Airport", "Goodyear", "US", false], - "GYS": ["Guangyuan Panlong Airport", "Guangyuan (Lizhou)", "CN", true], - "GYU": ["Guyuan Liupanshan Airport", "Guyuan (Yuanzhou)", "CN", true], - "GYY": ["Gary/Chicago International Airport", "Gary", "US", true], - "GYZ": ["Gruyere Airport", "Cosmo Newbery", "AU", true], - "GZG": ["Garze Gesar Airport", "Garzê (Garzê)", "CN", true], - "GZI": ["Ghazni Airport", "Ghazni", "AF", false], - "GZM": ["Xewkija Heliport", "Gozo", "MT", true], - "GZO": ["Nusatupe Airport", "Gizo", "SB", true], - "GZP": ["Gazipaşa-Alanya Airport", "Gazipaşa", "TR", true], - "GZT": ["Gaziantep Oğuzeli International Airport", "Gaziantep", "TR", true], - "GZW": ["Qazvin Airport", "Qazvin", "IR", false], - "HAA": ["Hasvik Airport", "Hasvik", "NO", true], - "HAB": ["Marion County Rankin Fite Airport", "Hamilton", "US", false], - "HAC": ["Hachijojima Airport", "Hachijojima", "JP", true], - "HAD": ["Halmstad Airport", "Halmstad", "SE", true], - "HAE": ["Lava Falls Heliport", "Supai Village", "US", false], - "HAF": ["Half Moon Bay Airport", "Half Moon Bay", "US", false], - "HAH": ["Prince Said Ibrahim International Airport", "Moroni", "KM", true], - "HAI": ["Three Rivers Municipal Dr Haines Airport", "Three Rivers", "US", false], - "HAJ": ["Hannover Airport", "Hannover", "DE", true], - "HAK": ["Haikou Meilan International Airport", "Haikou (Meilan)", "CN", true], - "HAL": ["Halali Airport", "Halali", "NA", true], - "HAM": ["Hamburg Helmut Schmidt Airport", "Hamburg", "DE", true], - "HAN": ["Noi Bai International Airport", "Hanoi (Soc Son)", "VN", true], - "HAO": ["Butler Co Regional Airport - Hogan Field", "Hamilton", "US", false], - "HAP": ["Happy Bay Helipad", "Long Island", "AU", false], - "HAQ": ["Hanimaadhoo International Airport", "Haa Dhaalu Atoll", "MV", true], - "HAR": ["Capital City Airport", "Harrisburg", "US", false], - "HAS": ["Hail International Airport", "Hail", "SA", true], - "HAT": ["Heathlands Airport", "Shelburne", "AU", false], - "HAU": ["Haugesund Airport, Karmøy", "Karmøy", "NO", true], - "HAV": ["José Martí International Airport", "Havana", "CU", true], - "HAW": ["Haverfordwest Airport", "Haverfordwest", "GB", false], - "HAY": ["Hacaritama Airport", "Aguachica", "CO", false], - "HAZ": ["Hatzfeldhaven Airport", "Hatzfeldhaven", "PG", false], - "HBA": ["Hobart International Airport", "Hobart (Cambridge)", "AU", true], - "HBB": ["Industrial Airpark", "Hobbs", "US", false], - "HBD": ["Habi Airport", "Habi", "PG", false], - "HBE": ["Alexandria International Airport", "Alexandria", "EG", true], - "HBG": ["Hattiesburg Bobby L Chain Municipal Airport", "Hattiesburg", "US", false], - "HBH": ["Entrance Island Seaplane Base", "Entrance Island", "US", false], - "HBK": ["Holbrook Municipal Airport", "Holbrook", "US", false], - "HBQ": ["Haibei Qilian Airport", "Haibei (Qilian)", "CN", true], - "HBR": ["Hobart Regional Airport", "Hobart", "US", false], - "HBT": ["Hambantota Seaplane Base", "Hambantota", "LK", true], - "HBU": ["Bulgan Sum Airport", "Bulgan", "MN", false], - "HBX": ["Hubballi Airport", "Hubballi", "IN", true], - "HCA": ["Big Spring Mc Mahon-Wrinkle Airport", "Big Spring", "US", false], - "HCC": ["Columbia County Airport", "Hudson", "US", false], - "HCJ": ["Hechi Jinchengjiang Airport", "Hechi (Jinchengjiang)", "CN", true], - "HCM": ["Eil Airport", "Eyl", "SO", false], - "HCN": ["Hengchun Airport", "Hengchun", "TW", false], - "HCQ": ["Halls Creek Airport", "", "AU", false], - "HCR": ["Holy Cross Airport", "Holy Cross", "US", true], - "HCW": ["Cheraw Municipal Airport/Lynch Bellinger Field", "Cheraw", "US", false], - "HCZ": ["Chenzhou Beihu Airport", "Chenzhou", "CN", true], - "HDD": ["Hyderabad Airport", "Hyderabad", "PK", true], - "HDE": ["Brewster Field", "Holdrege", "US", false], - "HDF": ["Heringsdorf Airport", "Zirchow", "DE", true], - "HDG": ["Handan Airport", "Handan", "CN", true], - "HDH": ["Kawaihapai Airfield", "Mokuleia", "US", false], - "HDK": ["Kulhudhuffushi Airport", "Kulhudhuffushi", "MV", true], - "HDM": ["Hamadan Airport", "Hamadan", "IR", true], - "HDN": ["Yampa Valley Airport", "Hayden", "US", true], - "HDO": ["Hindon Airport / Hindon Air Force Station", "Ghaziabad", "IN", true], - "HDR": ["Havadarya Airport", "Bandar Abbas", "IR", false], - "HDS": ["Eastgate Airport / Air Force Base Hoedspruit", "Hoedspruit", "ZA", true], - "HDY": ["Hat Yai International Airport", "Hat Yai", "TH", true], - "HEA": ["Herat - Khwaja Abdullah Ansari International Airport", "Guzara", "AF", true], - "HEB": ["Hinthada Airport", "Hinthada", "MM", false], - "HED": ["Herendeen Bay Airport", "Herendeen Bay", "US", false], - "HEE": ["Thompson-Robbins Airport", "West Helena", "US", false], - "HEG": ["Heglig Airport", "Heglig", "SD", false], - "HEH": ["Heho Airport", "Heho", "MM", true], - "HEI": ["Heide-Büsum Airport", "Oesterdeichstrich", "DE", true], - "HEK": ["Heihe Aihui Airport", "Heihe", "CN", true], - "HEL": ["Helsinki Vantaa Airport", "Helsinki (Vantaa)", "FI", true], - "HEN": ["Hernesaari Heliport", "Helsinki", "FI", false], - "HEO": ["Haelogo Airport", "Haelogo", "PG", false], - "HER": ["Heraklion International Nikos Kazantzakis Airport", "Heraklion", "GR", true], - "HES": ["Hermiston Municipal Airport", "Hermiston", "US", false], - "HET": ["Hohhot Baita International Airport", "Hohhot", "CN", true], - "HEW": ["Dongyang Hengdian General Airport", "Jinhua (Dongyang)", "CN", false], - "HEY": ["Hanchey Army (Fort Rucker) Heliport", "Fort Rucker Ozark", "US", false], - "HEZ": ["Natchez–Adams County Airport / Hardy-Anders Field", "Natchez", "US", false], - "HFA": ["Uri Michaeli Haifa International Airport", "Haifa", "IL", true], - "HFD": ["Hartford Brainard Airport", "Hartford", "US", false], - "HFE": ["Hefei Xinqiao International Airport", "Hefei", "CN", true], - "HFF": ["Mackall Army Air Field", "Hoffman", "US", false], - "HFN": ["Hornafjörður Airport", "Höfn", "IS", true], - "HFS": ["Hagfors Airport", "Råda", "SE", true], - "HFT": ["Hammerfest Airport", "Hammerfest", "NO", true], - "HGA": ["Egal International Airport", "Hargeisa", "SO", true], - "HGD": ["Hughenden Airport", "Hughenden", "AU", true], - "HGE": ["Higuerote Airport", "Higuerote", "VE", false], - "HGH": ["Hangzhou Xiaoshan International Airport", "Hangzhou", "CN", true], - "HGI": ["Itanagar Donyi Polo Hollongi Airport", "Hollongi", "IN", true], - "HGL": ["Helgoland-Düne Airport", "Helgoland", "DE", true], - "HGN": ["Mae Hong Son Airport", "Mae Hong Son", "TH", true], - "HGO": ["Korhogo Airport", "Korhogo", "CI", true], - "HGR": ["Hagerstown Regional Richard A Henson Field", "Hagerstown", "US", true], - "HGS": ["Hastings Airport", "Freetown", "SL", false], - "HGT": ["Tusi (Hunter Liggett) Army Heliport", "Jolon", "US", false], - "HGU": ["Mount Hagen Kagamuga Airport", "Mount Hagen", "PG", true], - "HGZ": ["Hog River Airport", "Hogatza", "US", false], - "HHE": ["JMSDF Hachinohe Air Base / Hachinohe Airport", "Hachinohe", "JP", false], - "HHH": ["Hilton Head Airport", "Hilton Head Island", "US", true], - "HHI": ["Wheeler Army Airfield", "Wahiawa", "US", false], - "HHN": ["Frankfurt-Hahn Airport", "Frankfurt am Main (Lautzenhausen)", "DE", true], - "HHP": ["Shun Tak Heliport", "Central and Western", "HK", true], - "HHQ": ["Hua Hin Airport", "Hua Hin", "TH", true], - "HHR": ["Jack Northrop Field Hawthorne Municipal Airport", "Hawthorne", "US", true], - "HHZ": ["Hikueru Airport", "Hikueru", "PF", true], - "HIA": ["Huai'an Lianshui Airport", "Huai'an", "CN", true], - "HIB": ["Range Regional Airport", "Hibbing", "US", true], - "HID": ["Horn Island Airport", "Horn", "AU", true], - "HIE": ["Mount Washington Regional Airport", "Whitefield", "US", false], - "HIF": ["Hill Air Force Base", "Ogden", "US", false], - "HIG": ["Highbury Airport", "Highbury", "AU", false], - "HII": ["Lake Havasu City Airport", "Lake Havasu City", "US", true], - "HIJ": ["Hiroshima Airport", "Hiroshima", "JP", true], - "HIL": ["Shilavo Airport", "Shilavo", "ET", true], - "HIM": ["Hingurakgoda Air Force Base", "Polonnaruwa Town", "LK", false], - "HIN": ["Sacheon Airport / Sacheon Air Base", "Sacheon", "KR", true], - "HIO": ["Portland Hillsboro Airport", "Portland", "US", false], - "HIP": ["Headingly Airport", "", "AU", false], - "HIR": ["Honiara International Airport", "Honiara", "SB", true], - "HIS": ["Hayman Island Resort Seaplane Base", "Hayman Island", "AU", true], - "HIT": ["Haivaro Airport", "Haivaro", "PG", false], - "HIW": ["Hiroshima Heliport", "Hiroshima", "JP", false], - "HJB": ["Hejing Bayinbuluke Airport", "Hejing", "CN", true], - "HJJ": ["Huaihua Zhijiang Airport", "Huaihua", "CN", true], - "HJR": ["Khajuraho Airport", "Khajuraho", "IN", true], - "HJT": ["Khujirt Airport", "Khujirt", "MN", false], - "HKA": ["Blytheville Municipal Airport", "Blytheville", "US", false], - "HKB": ["Healy Lake Airport", "Healy Lake", "US", false], - "HKD": ["Hakodate Airport", "Hakodate", "JP", true], - "HKG": ["Hong Kong International Airport", "Hong Kong", "HK", true], - "HKK": ["Hokitika Airfield", "", "NZ", true], - "HKN": ["Hoskins Airport", "Kimbe", "PG", true], - "HKR": ["Mara North Conservancy Airstrip", "Mara Rianta", "KE", false], - "HKS": ["Hawkins Field", "Jackson", "US", false], - "HKT": ["Phuket International Airport", "Phuket", "TH", true], - "HKY": ["Hickory Regional Airport", "Hickory", "US", false], - "HLA": ["Lanseria International Airport", "Johannesburg", "ZA", true], - "HLB": ["Batesville Airport", "Batesville", "US", false], - "HLC": ["Hill City Municipal Airport", "Hill City", "US", false], - "HLD": ["Hulunbuir Hailar Airport", "Hailar", "CN", true], - "HLE": ["Saint Helena International Airport", "Jamestown", "SH", true], - "HLF": ["Hultsfred Airport", "Hultsfred", "SE", false], - "HLG": ["Wheeling Ohio County Airport", "Wheeling", "US", false], - "HLH": ["Ulanhot Yilelite Airport", "Ulanhot", "CN", true], - "HLI": ["Hollister Municipal Airport", "Hollister", "US", false], - "HLJ": ["Zhaodong Beidahuang General Airport", "Suihua", "CN", false], - "HLL": ["Hillside Airport", "Hillside", "AU", false], - "HLM": ["Park Township Airport", "Holland", "US", false], - "HLN": ["Helena Regional Airport", "Helena", "US", true], - "HLP": ["Halim Perdanakusuma International Airport", "Jakarta", "ID", true], - "HLR": ["Hood Army Air Field", "Fort Cavazos (Killeen)", "US", false], - "HLS": ["St Helens Airport", "", "AU", false], - "HLT": ["Hamilton Airport", "", "AU", false], - "HLV": ["Helenvale Airport", "Helenvale", "AU", false], - "HLW": ["Hluhluwe Airport", "Hluhluwe", "ZA", false], - "HLZ": ["Hamilton International Airport", "Hamilton", "NZ", true], - "HMA": ["Khanty Mansiysk Airport", "Khanty-Mansiysk", "RU", true], - "HMB": ["Suhaj International Airport", "Suhaj", "EG", true], - "HME": ["Hassi Messaoud-Oued Irara Krim Belkacem Airport", "Hassi Messaoud", "DZ", true], - "HMG": ["Hermannsburg Airport", "Hermannsburg (Ntaria)", "AU", false], - "HMI": ["Hami Airport", "Hami", "CN", true], - "HMJ": ["Khmelnytskyi Airport", "Khmelnytskyi", "UA", false], - "HMN": ["Holloman Air Force Base", "Alamogordo", "US", false], - "HMO": ["General Ignacio L. Pesqueira International Airport", "Hermosillo", "MX", true], - "HMR": ["Hamar Lufthavn, Stavsberg", "Hamar", "NO", false], - "HMS": ["Haji Muhammad Sidik Airport", "Muara Teweh", "ID", true], - "HMT": ["Hemet Ryan Airport", "Hemet", "US", false], - "HMV": ["Hemavan Airport", "Hemavan", "SE", true], - "HMY": ["Seosan Air Base", "Seosan", "KR", false], - "HNA": ["Iwate Hanamaki Airport", "Hanamaki", "JP", true], - "HNB": ["Huntingburg Airport", "Huntingburg", "US", false], - "HNC": ["Billy Mitchell Airport", "Hatteras", "US", false], - "HND": ["Tokyo Haneda International Airport", "Tokyo", "JP", true], - "HNG": ["Hainanzhou Gonghe Airport", "Hainan (Gonghe)", "CN", false], - "HNH": ["Hoonah Airport", "Hoonah", "US", true], - "HNI": ["Heiweni Airport", "Heiweni", "PG", false], - "HNL": ["Daniel K. Inouye International Airport", "Honolulu, Oahu", "US", true], - "HNM": ["Hana Airport", "Hana", "US", true], - "HNN": ["Honinabi Airport", "Honinabi", "PG", false], - "HNS": ["Haines Airport", "Haines", "US", true], - "HNY": ["Hengyang Nanyue Airport", "Hengyang", "CN", true], - "HOA": ["Hola Airport", "Hola", "KE", false], - "HOB": ["Lea County Regional Airport", "Hobbs", "US", true], - "HOC": ["Komako Airport", "Komako", "PG", false], - "HOE": ["Ban Huoeisay Airport", "Huay Xai", "LA", false], - "HOF": ["Al-Ahsa International Airport", "Hofuf", "SA", true], - "HOG": ["Frank Pais International Airport", "Holguin", "CU", true], - "HOH": ["Hohenems-Dornbirn Airfield", "Hohenems", "AT", false], - "HOI": ["Hao Airport", "Otepa", "PF", true], - "HOK": ["Hooker Creek Airport", "Lajamanu", "AU", true], - "HOM": ["Homer Airport", "Homer", "US", true], - "HON": ["Huron Regional Airport", "Huron", "US", false], - "HOO": ["Nhon Co Airfield", "Đăk R'Lấp", "VN", false], - "HOP": ["Campbell Army Airfield (Fort Campbell)", "Fort Campbell", "US", false], - "HOQ": ["Hof-Plauen Airport", "Hof", "DE", false], - "HOR": ["Horta Airport", "Horta", "PT", true], - "HOS": ["Chos Malal Airport", "Chos Malal", "AR", false], - "HOT": ["Memorial Field Airport", "Hot Springs", "US", true], - "HOU": ["William P. Hobby Airport", "Houston", "US", true], - "HOV": ["Ørsta-Volda Airport, Hovden", "Ørsta", "NO", true], - "HOX": ["Hommalinn Airport", "Hommalinn", "MM", false], - "HPA": ["Lifuka Island Airport", "Lifuka", "TO", true], - "HPB": ["Hooper Bay Airport", "Hooper Bay", "US", true], - "HPE": ["Hope Vale Airport", "Hope Vale", "AU", false], - "HPG": ["Shennongjia Hongping Airport", "Shennongjia (Hongping)", "CN", true], - "HPH": ["Cat Bi International Airport", "Haiphong (Hai An)", "VN", true], - "HPN": ["Westchester County Airport", "White Plains", "US", true], - "HPR": ["Pretoria Central Heliport", "Pretoria", "ZA", false], - "HPT": ["Hampton Municipal Airport", "Hampton", "US", false], - "HPV": ["Princeville Airport", "Hanalei", "US", false], - "HPY": ["Baytown Airport", "Baytown", "US", false], - "HQL": ["Tashikuergan Hongqilafu Airport", "Tashikuergan", "CN", true], - "HQM": ["Bowerman Airport", "Hoquiam", "US", false], - "HQQ": ["Anyang Hongqiqu Airport", "Anyang", "CN", true], - "HRA": ["Mnichovo Hradiště Airport", "Mnichovo Hradiště", "CZ", false], - "HRB": ["Harbin Taiping International Airport", "Harbin", "CN", true], - "HRC": ["Sary Su Airport", "Zhayrem", "KZ", false], - "HRE": ["Robert Gabriel Mugabe International Airport", "Harare", "ZW", true], - "HRF": ["Hoarafushi Airport", "", "MV", true], - "HRG": ["Hurghada International Airport", "Hurghada", "EG", true], - "HRH": ["Aligarh Airport", "Aligarh", "IN", false], - "HRI": ["Mattala Rajapaksa International Airport", "Mattala", "LK", true], - "HRK": ["Kharkiv International Airport", "Kharkiv", "UA", false], - "HRL": ["Valley International Airport", "Harlingen", "US", true], - "HRM": ["Hassi R'Mel Airport", "Hassi R'Mel", "DZ", false], - "HRO": ["Boone County Airport", "Harrison", "US", true], - "HRR": ["Herrera Airport", "Campiña", "CO", false], - "HRS": ["Harrismith Airport", "Harrismith", "ZA", false], - "HRY": ["Henbury Airport", "Ghan", "AU", false], - "HRZ": ["Walter Bündchen Airport", "Horizontina", "BR", false], - "HSA": ["Hazrat Sultan International Airport", "Turkıstan", "KZ", true], - "HSB": ["Harrisburg-Raleigh Airport", "Harrisburg", "US", false], - "HSC": ["Shaoguan Danxia Airport", "Shaoguan", "CN", true], - "HSF": ["Suifenhe Dongning Airport", "Suifenhe", "CN", false], - "HSG": ["Kyushu Saga International Airport", "Saga", "JP", true], - "HSH": ["Henderson Executive Airport", "Las Vegas", "US", false], - "HSI": ["Hastings Municipal Airport", "Hastings", "US", false], - "HSJ": ["Zhengzhou Shangjie Airport", "Zhengzhou", "CN", false], - "HSK": ["Huesca-Pirineos Airport", "Monflorite/Alcalá del Obispo", "ES", false], - "HSL": ["Huslia Airport", "Huslia", "US", true], - "HSM": ["Horsham Airport", "", "AU", false], - "HSN": ["Zhoushan Putuoshan International Airport", "Zhoushan", "CN", true], - "HSP": ["Ingalls Field", "Hot Springs", "US", false], - "HSR": ["Rajkot International Airport", "Rajkot", "IN", true], - "HSS": ["Maharaja Agrasen International Airport", "Hisar", "IN", true], - "HST": ["Homestead Air Reserve Base", "Homestead", "US", false], - "HSV": ["Huntsville International Airport", "Huntsville", "US", true], - "HSZ": ["Hsinchu Air Base", "Hsinchu City", "TW", false], - "HTA": ["Chita-Kadala International Airport", "Chita", "RU", true], - "HTG": ["Khatanga Airport", "Khatanga", "RU", true], - "HTH": ["Hawthorne Industrial Airport", "Hawthorne", "US", false], - "HTI": ["Hamilton Island Airport", "Hamilton Island", "AU", true], - "HTL": ["Roscommon County - Blodgett Memorial Airport", "Houghton Lake", "US", false], - "HTM": ["Khatgal Airport", "Hatgal", "MN", false], - "HTN": ["Hotan Airport", "Hotan", "CN", true], - "HTO": ["Town of East Hampton Airport", "East Hampton", "US", false], - "HTR": ["Hateruma Airport", "Taketomi", "JP", false], - "HTS": ["Tri-State Airport / Milton J. Ferguson Field", "Huntington", "US", true], - "HTT": ["Huatugou Airport", "Mengnai", "CN", true], - "HTU": ["Hopetoun Airport", "", "AU", false], - "HTV": ["Huntsville Regional Airport", "Huntsville", "US", false], - "HTW": ["Lawrence County Airpark", "South Point", "US", false], - "HTY": ["Hatay Airport", "Antakya", "TR", true], - "HTZ": ["Hato Corozal Airport", "Hato Corozal", "CO", false], - "HUA": ["Redstone Army Air Field", "Redstone Arsnl Huntsville", "US", false], - "HUB": ["Humbert River Airport", "", "AU", false], - "HUC": ["Dr Hermenegildo Ortiz Quinones Airport", "Humacao", "PR", false], - "HUD": ["Humboldt Municipal Airport", "Humboldt", "US", false], - "HUE": ["Humera Airport", "Akwi", "ET", true], - "HUF": ["Terre Haute Regional Airport, Hulman Field", "Terre Haute", "US", false], - "HUG": ["Huehuetenango Airport", "Huehuetenango", "GT", true], - "HUH": ["Huahine-Fare Airport", "Fare", "PF", true], - "HUI": ["Phu Bai International Airport", "Huế", "VN", true], - "HUJ": ["Stan Stamper Municipal Airport", "Hugo", "US", false], - "HUK": ["Hukuntsi Airport", "Hukuntsi", "BW", false], - "HUL": ["Houlton International Airport", "Houlton", "US", false], - "HUM": ["Houma Terrebonne Airport", "Houma", "US", false], - "HUN": ["Hualien Chiashan Airport", "Hualien City", "TW", true], - "HUO": ["Holingol Huolinhe Airport", "Holingol", "CN", true], - "HUQ": ["Hon Airport", "", "LY", false], - "HUS": ["Hughes Airport", "Hughes", "US", true], - "HUT": ["Hutchinson Municipal Airport", "Hutchinson", "US", false], - "HUU": ["Alferez Fap David Figueroa Fernandini Airport", "Huánuco", "PE", true], - "HUV": ["Hudiksvall Airport", "Hudiksvall", "SE", false], - "HUW": ["Humaitá Airport", "Humaitá", "BR", false], - "HUX": ["Bahías de Huatulco International Airport", "Huatulco", "MX", true], - "HUY": ["Humberside Airport", "Grimsby, Lincolnshire", "GB", true], - "HUZ": ["Huizhou Pingtan Airport", "Huizhou (Pingtan)", "CN", true], - "HVA": ["Analalava Airport", "Analalava", "MG", false], - "HVB": ["Hervey Bay Airport", "Hervey Bay", "AU", true], - "HVD": ["Khovd Airport", "Khovd", "MN", true], - "HVE": ["Hanksville Airport", "Hanksville", "US", false], - "HVG": ["Honningsvåg Airport, Valan", "Honningsvåg", "NO", true], - "HVK": ["Hólmavík Airport", "Hólmavík", "IS", false], - "HVN": ["Tweed New Haven Airport", "New Haven", "US", true], - "HVR": ["Havre City County Airport", "Havre", "US", true], - "HVS": ["Hartsville Regional Airport", "Hartsville", "US", false], - "HWA": ["Hawabango Airport", "Hawabango", "PG", false], - "HWD": ["Hayward Executive Airport", "Hayward", "US", false], - "HWI": ["Hawk Inlet Seaplane Base", "Hawk Inlet", "US", false], - "HWK": ["Wilpena Pound Airport", "Hawker", "AU", false], - "HWN": ["Hwange National Park Airport", "Gwayi River Farms", "ZW", false], - "HWO": ["North Perry Airport", "Hollywood", "US", false], - "HWR": ["Halwara International Airport", "Halwara", "IN", true], - "HXD": ["Haixi Delingha Airport", "Delingha", "CN", true], - "HXX": ["Hay Airport", "", "AU", false], - "HYA": ["Cape Cod Gateway Airport", "Hyannis", "US", true], - "HYC": ["Wycombe Air Park", "Marlow, Buckinghamshire", "GB", false], - "HYD": ["Rajiv Gandhi International Airport", "Hyderabad", "IN", true], - "HYF": ["Hayfields Airport", "Bainyik", "PG", false], - "HYG": ["Hydaburg Seaplane Base", "Hydaburg", "US", true], - "HYL": ["Hollis Clark Bay Seaplane Base", "Hollis", "US", true], - "HYN": ["Taizhou Luqiao Airport", "Taizhou (Luqiao)", "CN", true], - "HYR": ["Sawyer County Airport", "Hayward", "US", false], - "HYS": ["Hays Regional Airport", "Hays", "US", true], - "HYV": ["Hyvinkää Airfield", "Hyvinkää", "FI", false], - "HZA": ["Heze Mudan Airport", "Heze (Dingtao)", "CN", true], - "HZB": ["Merville-Calonne Airport", "Merville, Nord", "FR", false], - "HZG": ["Hanzhong Chenggu Airport", "Hanzhong (Chenggu)", "CN", true], - "HZH": ["Liping Airport", "Liping", "CN", true], - "HZK": ["Húsavík Airport", "Húsavík", "IS", false], - "HZL": ["Hazleton Municipal Airport", "Hazleton", "US", false], - "HZP": ["Fort Mackay / Horizon Airport", "Fort Mackay", "CA", false], - "HZU": ["Chengdu Huaizhou Airport", "Chengdu (Jintang)", "CN", false], - "HZV": ["Hazyview Airport", "Hazyview", "ZA", false], - "IAA": ["Igarka Airport", "Igarka", "RU", true], - "IAB": ["McConnell Air Force Base", "Wichita", "US", false], - "IAD": ["Washington Dulles International Airport", "Dulles", "US", true], - "IAG": ["Niagara Falls International Airport", "Niagara Falls", "US", true], - "IAH": ["George Bush Intercontinental Airport", "Houston", "US", true], - "IAL": ["Salinas Municipal Airport", "Salinas", "BR", false], - "IAM": ["Zarzaitine - In Aménas Airport", "In Aménas", "DZ", true], - "IAN": ["Bob Baker Memorial Airport", "Kiana", "US", true], - "IAO": ["Siargao Airport", "Del Carmen", "PH", true], - "IAQ": ["Bahregan Airport", "Imam Hassan", "IR", false], - "IAR": ["Golden Ring Yaroslavl International Airport", "Tunoshna", "RU", true], - "IAS": ["Iaşi International Airport", "Iaşi", "RO", true], - "IAX": ["Miangas Airport", "Miangas", "ID", false], - "IBA": ["Ibadan Airport", "Ibadan", "NG", true], - "IBB": ["General Villamil Airport", "Puerto Villamil", "EC", true], - "IBE": ["Perales Airport", "Ibagué", "CO", true], - "IBI": ["Iboki Airport", "Iboki", "PG", false], - "IBL": ["Indigo Bay Lodge Airport", "Bazaruto Island", "MZ", false], - "IBM": ["Iron Bridge Mine Airport", "Japal Camp", "AU", false], - "IBO": ["Ibo Airport", "Ibo", "MZ", false], - "IBP": ["Iberia Airport", "Iberia", "PE", false], - "IBR": ["Ibaraki Airport", "Omitama", "JP", true], - "IBZ": ["Ibiza Airport", "Ibiza (Eivissa)", "ES", true], - "ICA": ["Ikabaru Airport", "Ikabarú", "VE", false], - "ICC": ["Andrés Miguel Salazar Marcano Airport", "Isla de Coche", "VE", true], - "ICI": ["Cicia Airport", "Cicia", "FJ", true], - "ICK": ["Nieuw Nickerie - Major Henk Fernandes Airport", "Nieuw Nickerie", "SR", false], - "ICL": ["Schenck Field", "Clarinda", "US", false], - "ICN": ["Incheon International Airport", "Seoul", "KR", true], - "ICO": ["Sicogon Airstrip", "Carles", "PH", false], - "ICS": ["Cascade Airport", "Cascade", "US", false], - "ICT": ["Wichita Dwight D. Eisenhower National Airport", "Wichita", "US", true], - "ICY": ["Icy Bay Airport", "Icy Bay", "US", false], - "IDA": ["Idaho Falls Regional Airport", "Idaho Falls", "US", true], - "IDB": ["Idre Airport", "Idre", "SE", false], - "IDF": ["Idiofa Airport", "Idiofa", "CD", false], - "IDG": ["Ida Grove Municipal Airport", "Ida Grove", "US", false], - "IDH": ["Idaho County Airport", "Grangeville", "US", false], - "IDI": ["Indiana County–Jimmy Stewart Airport", "Indiana", "US", false], - "IDK": ["Indulkana Airport", "Indulkana", "AU", false], - "IDN": ["Indagen Airport", "Indagen", "PG", false], - "IDO": ["Santa Izabel do Morro Airport", "Cristalândia", "BR", false], - "IDP": ["Independence Municipal Airport", "Independence", "US", false], - "IDR": ["Devi Ahilya Bai Holkar International Airport", "Indore", "IN", true], - "IDY": ["Île d'Yeu Airport", "Île d'Yeu", "FR", false], - "IEG": ["Zielona Góra-Babimost Airport", "Nowe Kramsko", "PL", true], - "IEJ": ["Iejima Airport", "Ie", "JP", false], - "IES": ["Riesa-Göhlis Airport", "Riesa", "DE", false], - "IEV": ["Igor Sikorsky Kyiv International Airport (Zhuliany)", "Kyiv", "UA", false], - "IFA": ["Iowa Falls Municipal Airport", "Iowa Falls", "US", false], - "IFF": ["Iffley Airport", "", "AU", false], - "IFH": ["Hesa Airport", "Hesa", "IR", false], - "IFJ": ["Ísafjörður Airport", "Ísafjörður", "IS", true], - "IFL": ["Innisfail Airport", "", "AU", false], - "IFN": ["Isfahan Shahid Beheshti International Airport", "Isfahan", "IR", true], - "IFO": ["Ivano-Frankivsk International Airport", "Ivano-Frankivsk", "UA", false], - "IFP": ["Laughlin Bullhead International Airport", "Bullhead City", "US", false], - "IFU": ["Ifuru Airport", "Ifuru Island", "MV", false], - "IGA": ["Inagua Airport", "Matthew Town", "BS", true], - "IGB": ["Ingeniero Jacobacci - Captain H R Bordón Airport", "Ingeniero Jacobacci", "AR", false], - "IGD": ["Iğdır Airport", "Iğdır", "TR", true], - "IGE": ["Tchongorove Airport", "Iguela", "GA", false], - "IGG": ["Igiugig Airport", "Igiugig", "US", true], - "IGH": ["Ingham Airport", "", "AU", false], - "IGL": ["Çiğli Airbase", "Çiğli", "TR", false], - "IGM": ["Kingman Airport", "Kingman", "US", false], - "IGN": ["Maria Cristina (Iligan) Airport", "Balo-i", "PH", false], - "IGO": ["Jaime Ortiz Betancourt Airport", "Chigorodó", "CO", false], - "IGR": ["Cataratas Del Iguazú International Airport", "Puerto Iguazu", "AR", true], - "IGS": ["Ingolstadt Manching Airport", "Manching", "DE", false], - "IGT": ["Magas Airport", "Sunzha", "RU", true], - "IGU": ["Cataratas International Airport", "Foz do Iguaçu", "BR", true], - "IHC": ["Inhaca Airport", "Inhaca", "MZ", false], - "IHO": ["Ihosy Airport", "Ihosy", "MG", false], - "IHR": ["Iranshahr Airport", "Iranshahr", "IR", false], - "IHU": ["Ihu Airport", "Ihu", "PG", false], - "IIA": ["Inishmaan Aerodrome", "Inis Meáin", "IE", true], - "IIL": ["Ilam Airport", "Ilam", "IR", false], - "IIS": ["Nissan Island Airport", "Nissan Island", "PG", false], - "IJK": ["Izhevsk Airport", "Izhevsk", "RU", true], - "IJU": ["João Batista Bos Filho Airport", "Ijuí", "BR", false], - "IJX": ["Jacksonville Municipal Airport", "Jacksonville", "US", false], - "IKA": ["Imam Khomeini International Airport", "Tehran", "IR", true], - "IKB": ["Wilkes County Airport", "North Wilkesboro", "US", false], - "IKE": ["Ikerasak Heliport", "Ikerasak", "GL", true], - "IKG": ["Karakol International Airport", "Karakol", "KG", true], - "IKI": ["Iki Airport", "Iki", "JP", true], - "IKK": ["Greater Kankakee Airport", "Kankakee", "US", false], - "IKL": ["Ikela Airport", "Ikela", "CD", false], - "IKO": ["Nikolski Air Station", "Nikolski", "US", true], - "IKP": ["Inkerman Airport", "Inkerman", "AU", false], - "IKS": ["Tiksi Airport", "Tiksi", "RU", true], - "IKT": ["Irkutsk International Airport", "Irkutsk", "RU", true], - "IKU": ["Issyk-Kul International Airport", "Tamchy", "KG", true], - "ILA": ["Ilaga Airport", "Ilaga", "ID", false], - "ILD": ["Lleida-Alguaire Airport", "Lleida", "ES", true], - "ILE": ["Skylark Field", "Killeen", "US", false], - "ILF": ["Ilford Airport", "Ilford", "CA", true], - "ILG": ["Wilmington Airport", "Wilmington", "US", true], - "ILH": ["Illesheim Army Heliport", "Illesheim", "DE", false], - "ILI": ["Iliamna Airport", "Iliamna", "US", true], - "ILK": ["Atsinanana Airport", "Ilaka", "MG", false], - "ILL": ["Willmar Municipal Airport John L Rice Field (2006)", "Willmar", "US", false], - "ILM": ["Wilmington International Airport", "Wilmington", "US", true], - "ILN": ["Wilmington Airpark", "Wilmington", "US", false], - "ILO": ["Iloilo International Airport", "Cabatuan", "PH", true], - "ILP": ["Île des Pins Airport", "Île des Pins", "NC", true], - "ILQ": ["General Jorge Fernandez Maldon Airport", "Ilo", "PE", true], - "ILR": ["General Tunde Idiagbon International Airport", "Ilorin/Ogbomosho", "NG", true], - "ILS": ["Ilopango International Airport", "San Salvador", "SV", true], - "ILU": ["Kilaguni Airport", "Kilaguni", "KE", false], - "ILY": ["Islay Airport", "Isle of Islay, Argyll and Bute", "GB", true], - "ILZ": ["Žilina-Dolný Hričov Airport", "Dolný Hričov", "SK", false], - "IMA": ["Iamalele Airport", "Iamalele, Fergusson Island", "PG", false], - "IMB": ["Imbaimadai Airport", "Imbaimadai", "GY", false], - "IMD": ["Imonda Airport", "Imonda", "PG", false], - "IMF": ["Bir Tikendrajit International Airport", "Imphal", "IN", true], - "IMG": ["Inhaminga Airport", "Inhaminga", "MZ", false], - "IMK": ["Simikot Airport", "Simikot", "NP", true], - "IML": ["Imperial Municipal Airport", "Imperial", "US", false], - "IMM": ["Immokalee Regional Airport", "Immokalee", "US", false], - "IMN": ["Imane Airport", "Imane", "PG", false], - "IMO": ["Zemio Airport", "Zemio", "CF", false], - "IMP": ["Prefeito Renato Moreira Airport", "Imperatriz", "BR", true], - "IMQ": ["Maku National Airport", "Showt", "IR", false], - "IMT": ["Ford Airport", "Kingsford", "US", true], - "IMU": ["Akimuga Airstrip", "Akimuga, Mimika", "ID", false], - "INA": ["Inta Airport", "Inta", "RU", false], - "INB": ["Independence Airport", "Independence", "BZ", true], - "INC": ["Yinchuan Hedong International Airport", "Yinchuan", "CN", true], - "IND": ["Indianapolis International Airport", "Indianapolis", "US", true], - "INF": ["In Guezzam Airport", "In Guezzam", "DZ", false], - "INH": ["Inhambane Airport", "Inhambane", "MZ", true], - "INI": ["Niš Constantine the Great Airport", "Niš", "RS", true], - "INJ": ["Injune Airport", "Injune", "AU", false], - "INK": ["Winkler County Airport", "Wink", "US", false], - "INL": ["Falls International Airport", "International Falls", "US", true], - "INM": ["Innamincka Airport", "", "AU", false], - "INN": ["Innsbruck Airport", "Innsbruck", "AT", true], - "INO": ["Inongo Airport", "Inongo", "CD", true], - "INQ": ["Inisheer Aerodrome", "Inis Oírr", "IE", true], - "INS": ["Creech Air Force Base", "Indian Springs", "US", false], - "INT": ["Smith Reynolds Airport", "Winston Salem", "US", false], - "INU": ["Nauru International Airport", "Yaren", "NR", true], - "INV": ["Inverness Airport", "Inverness, Highland", "GB", true], - "INW": ["Winslow Lindbergh Regional Airport", "Winslow", "US", false], - "INX": ["Inanwatan Airport", "Inanwatan", "ID", false], - "INY": ["Inyati Airport", "Inyati", "ZA", false], - "INZ": ["In Salah Airport", "In Salah", "DZ", true], - "IOA": ["Ioannina King Pyrrhus National Airport", "Ioannina", "GR", true], - "IOK": ["Iokea Airport", "Iokea", "PG", false], - "IOM": ["Isle of Man Airport", "Castletown, Rushen", "IM", true], - "ION": ["Impfondo Airport", "Impfondo", "CG", false], - "IOP": ["Ioma Airport", "Ioma", "PG", false], - "IOQ": ["Isortoq Heliport", "Isortoq", "GL", true], - "IOR": ["Inishmore Aerodrome", "Inis Mór", "IE", true], - "IOS": ["Bahia - Jorge Amado Airport", "Ilhéus", "BR", true], - "IOT": ["Illorsuit Heliport", "Illorsuit", "GL", true], - "IOW": ["Iowa City Municipal Airport", "Iowa City", "US", false], - "IPA": ["Ipota Airport", "Ipota", "VU", false], - "IPC": ["Mataveri International Airport", "Isla De Pascua", "CL", true], - "IPE": ["Ipil Airport", "Ipil", "PH", false], - "IPG": ["Ipiranga Airport", "Santo Antônio do Içá", "BR", false], - "IPH": ["Sultan Azlan Shah Airport", "Ipoh", "MY", true], - "IPI": ["San Luis Airport", "Ipiales", "CO", true], - "IPL": ["Imperial County Airport", "Imperial", "US", true], - "IPN": ["Usiminas Airport", "Ipatinga", "BR", true], - "IPT": ["Williamsport Regional Airport", "Williamsport", "US", true], - "IPU": ["Ipiaú Airport", "Ipiaú", "BR", false], - "IPZ": ["San Isidro del General Airport", "Pérez Zeledón", "CR", false], - "IQA": ["Al Asad Air Base", "Hīt", "IQ", false], - "IQM": ["Qiemo Yudu Airport", "Qiemo", "CN", true], - "IQN": ["Qingyang Xifeng Airport", "Qingyang (Xifeng)", "CN", true], - "IQQ": ["Diego Aracena International Airport", "Iquique", "CL", true], - "IQT": ["Coronel FAP Francisco Secada Vignetta International Airport", "Iquitos", "PE", true], - "IRA": ["Ngorangora Airport", "Kirakira", "SB", true], - "IRB": ["Iraan Municipal Airport", "Iraan", "US", false], - "IRC": ["Circle City (New) Airport", "Circle", "US", true], - "IRD": ["Ishurdi Airport", "Ishurdi", "BD", false], - "IRE": ["Irecê Airport", "Irecê", "BR", false], - "IRG": ["Lockhart River Airport", "Lockhart River", "AU", true], - "IRI": ["Iringa Airport", "Nduli", "TZ", false], - "IRJ": ["Capitan V A Almonacid Airport", "La Rioja", "AR", true], - "IRK": ["Kirksville Regional Airport", "Kirksville", "US", true], - "IRM": ["Igrim Airport", "", "RU", false], - "IRN": ["Iriona Airport", "Iriona", "HN", false], - "IRO": ["Birao Airport", "Birao", "CF", false], - "IRP": ["Matari Airport", "Isiro", "CD", true], - "IRS": ["Kirsch Municipal Airport", "Sturgis", "US", false], - "IRU": ["Iranamadu Seaplane Base", "Iranamadu", "LK", false], - "IRZ": ["Tapuruquara Airport", "Santa Isabel do Rio Negro", "BR", true], - "ISA": ["Mount Isa Airport", "Mount Isa", "AU", true], - "ISB": ["Islamabad International Airport", "Attock", "PK", true], - "ISC": ["St. Mary's Airport", "St. Mary's, Isles of Scilly", "GB", true], - "ISD": ["Santa Bárbara Airport", "Iscuandé", "CO", false], - "ISE": ["Süleyman Demirel International Airport", "Isparta", "TR", true], - "ISG": ["New Ishigaki Airport", "Ishigaki", "JP", true], - "ISI": ["Isisford Airport", "", "AU", false], - "ISJ": ["Isla Mujeres National Airport", "Isla Mujeres", "MX", false], - "ISK": ["Nashik International Airport", "Nashik", "IN", true], - "ISL": ["İstanbul Atatürk Airport", "Istanbul(Bakırköy)", "TR", false], - "ISM": ["Kissimmee Gateway Airport", "Orlando", "US", false], - "ISO": ["Kinston Regional Jetport At Stallings Field", "Kinston", "US", false], - "ISP": ["Long Island MacArthur Airport", "Islip", "US", true], - "ISQ": ["Schoolcraft County Airport", "Manistique", "US", false], - "ISS": ["Wiscasset Airport", "Wiscasset", "US", false], - "IST": ["İstanbul Airport", "Istanbul", "TR", true], - "ISU": ["Jalal Talabani International Airport", "Sulaymaniyah", "IQ", true], - "ISW": ["Alexander Field South Wood County Airport", "Wisconsin Rapids", "US", false], - "ITA": ["Itacoatiara Airport", "Itacoatiara", "BR", false], - "ITB": ["Itaituba Airport", "Itaituba", "BR", true], - "ITE": ["Ituberá Airport", "Ituberá", "BR", false], - "ITH": ["Ithaca Tompkins Regional Airport", "Ithaca", "US", true], - "ITI": ["Fazenda Americana Airport", "Itambacuri", "BR", false], - "ITJ": ["Campo Comandantes Airport", "Itajaí", "BR", false], - "ITK": ["Itokama Airport", "Itokama", "PG", false], - "ITM": ["Osaka Itami International Airport", "Osaka", "JP", true], - "ITN": ["Tertuliano Guedes de Pinho Airport", "Itabuna", "BR", false], - "ITO": ["Hilo International Airport", "Hilo", "US", true], - "ITP": ["Itaperuna Airport", "Itaperuna", "BR", false], - "ITQ": ["Itaqui Airport", "Itaqui", "BR", false], - "ITR": ["Francisco Vilela do Amaral Airport", "Itumbiara", "BR", false], - "ITU": ["Iturup Airport", "Kurilsk", "RU", true], - "IUA": ["Canandaigua Airport", "Ontario County IDA", "US", false], - "IUE": ["Niue International Airport", "Alofi", "NU", true], - "IUI": ["Innarsuit Heliport", "Innarsuit", "GL", true], - "IUL": ["Ilu Airport", "Ilu", "ID", false], - "IUP": ["Apuí Airport", "Apuí", "BR", false], - "IVA": ["Ampampamena Airport", "", "MG", false], - "IVC": ["Invercargill Airport", "Invercargill", "NZ", true], - "IVG": ["Berane Airport", "Berane", "ME", false], - "IVI": ["Viveros Island Airport", "Isla Viveros", "PA", false], - "IVL": ["Ivalo Airport", "Ivalo", "FI", true], - "IVO": ["Chivolo Airport", "Chivolo", "CO", false], - "IVR": ["Inverell Airport", "Inverell", "AU", false], - "IVW": ["Inverway Airport", "Inverway", "AU", false], - "IWA": ["Ivanovo South Airport", "Ivanovo", "RU", true], - "IWD": ["Gogebic Iron County Airport", "Ironwood", "US", true], - "IWJ": ["Iwami Airport", "Masuda", "JP", true], - "IWK": ["Iwakuni Kintaikyo Airport", "Iwakuni", "JP", true], - "IWO": ["Ioto (Iwo Jima) Airbase", "Ogasawara", "JP", false], - "IWS": ["West Houston Airport", "Houston", "US", false], - "IXA": ["Agartala - Maharaja Bir Bikram Airport", "Agartala", "IN", true], - "IXB": ["Bagdogra Airport", "Siliguri", "IN", true], - "IXC": ["Shaheed Bhagat Singh International Airport", "Chandigarh", "IN", true], - "IXD": ["Prayagraj Airport", "Allahabad", "IN", true], - "IXE": ["Mangaluru International Airport", "Mangaluru", "IN", true], - "IXG": ["Belagavi Airport", "Belgaum", "IN", true], - "IXH": ["Kailashahar Airport", "Kailashahar", "IN", false], - "IXI": ["Lilabari North Lakhimpur Airport", "Lilabari", "IN", true], - "IXJ": ["Jammu Airport", "Jammu", "IN", true], - "IXK": ["Keshod Airport", "Keshod", "IN", true], - "IXL": ["Leh Kushok Bakula Rimpochee Airport", "Leh", "IN", true], - "IXM": ["Madurai Airport", "Madurai", "IN", true], - "IXN": ["Khowai Airport", "Khowai", "IN", false], - "IXP": ["Pathankot Airport", "Pathankot", "IN", true], - "IXQ": ["Kamalpur Airport", "Manik Bhandar", "IN", false], - "IXR": ["Birsa Munda Airport", "Ranchi", "IN", true], - "IXS": ["Silchar Airport", "Silchar", "IN", true], - "IXT": ["Pasighat Airport", "Pasighat", "IN", false], - "IXU": ["Aurangabad Airport", "Aurangabad", "IN", true], - "IXV": ["Along Airport", "", "IN", false], - "IXW": ["Sonari Airport", "Jamshedpur", "IN", false], - "IXX": ["Bidar Airport / Bidar Air Force Station", "Bidar", "IN", false], - "IXY": ["Kandla Airport", "Kandla", "IN", true], - "IXZ": ["Veer Savarkar International Airport / INS Utkrosh", "Port Blair", "IN", true], - "IYK": ["Inyokern Airport", "Inyokern", "US", false], - "IZA": ["Presidente Itamar Franco Airport", "Juiz de Fora", "BR", true], - "IZE": ["Itzehoe Hungriger Wolf Airfield", "Hohenlockstedt", "DE", false], - "IZM": ["Selçuk Efes Airport", "Selçuk", "TR", false], - "IZO": ["Izumo Enmusubi Airport", "Izumo", "JP", true], - "IZT": ["General Antonio Cárdenas Rodríguez National Airport / Ixtepec Air Base", "Ixtepec", "MX", true], - "JAA": ["Jalalabad Airport", "Jalalabad", "AF", false], - "JAB": ["Jabiru Airport", "", "AU", false], - "JAC": ["Jackson Hole Airport", "Jackson", "US", true], - "JAD": ["Perth Jandakot Airport", "Perth", "AU", false], - "JAE": ["Shumba Airport", "Jaén", "PE", true], - "JAF": ["Jaffna International Airport", "Jaffna", "LK", true], - "JAG": ["Shahbaz Air Base", "Jacobabad", "PK", false], - "JAH": ["Zi Les Paluds Heliport", "Aubagne", "FR", false], - "JAI": ["Jaipur International Airport", "Jaipur", "IN", true], - "JAK": ["Jacmel Airport", "Jacmel", "HT", false], - "JAL": ["El Lencero Airport", "Emiliano Zapata", "MX", false], - "JAM": ["Bezmer Air Base", "Bezmer", "BG", false], - "JAN": ["Jackson-Medgar Wiley Evers International Airport", "Jackson", "US", true], - "JAP": ["Chacarita Airport", "Puntarenas", "CR", false], - "JAQ": ["Jacquinot Bay Airport", "Jacquinot Bay", "PG", false], - "JAR": ["Jahrom Airport", "Jahrom", "IR", false], - "JAS": ["Jasper County Airport Bell Field", "Jasper", "US", false], - "JAT": ["Jabot Airport", "Ailinglapalap Atoll", "MH", false], - "JAU": ["Francisco Carle Airport", "Jauja", "PE", true], - "JAV": ["Ilulissat Airport", "Ilulissat", "GL", true], - "JAW": ["Araripina Airport", "Araripina", "BR", false], - "JAX": ["Jacksonville International Airport", "Jacksonville", "US", true], - "JBB": ["Notohadinegoro Airport", "Jember", "ID", true], - "JBK": ["Qitai Jiangbulake Airport", "Qitai", "CN", true], - "JBQ": ["La Isabela International Airport", "La Isabela", "DO", true], - "JBR": ["Jonesboro Municipal Airport", "Jonesboro", "US", true], - "JBS": ["São Borja Airport", "São Borja", "BR", false], - "JBT": ["Bethel Seaplane Base", "Bethel", "US", false], - "JCB": ["Santa Terezinha Airport", "Joaçaba", "BR", false], - "JCH": ["Qasigiannguit Heliport", "Qasigiannguit", "GL", true], - "JCI": ["New Century AirCenter Airport", "New Century", "US", false], - "JCJ": ["Chuja Port Heliport", "Chuja, Hach’uja-do (Chujado Islands)", "KR", false], - "JCK": ["Julia Creek Airport", "", "AU", true], - "JCL": ["České Budějovice South Bohemian Airport", "České Budějovice", "CZ", true], - "JCM": ["Jacobina Airport", "Jacobina", "BR", false], - "JCO": ["Comino Heliport", "Comino", "MT", false], - "JCR": ["Jacareacanga Airport", "Jacareacanga", "BR", false], - "JCS": ["Crateús Airport", "Crateús", "BR", false], - "JCT": ["Kimble County Airport", "Junction", "US", false], - "JCU": ["Ceuta Heliport", "Ceuta", "ES", true], - "JCY": ["LBJ Ranch Airport", "Stonewall", "US", false], - "JDA": ["Grant County Regional Airport / Ogilvie Field", "John Day", "US", false], - "JDE": ["Jiande Qiandaohu General Airport", "Hangzhou", "CN", false], - "JDF": ["Francisco de Assis Airport", "Juiz de Fora", "BR", true], - "JDG": ["Jeongseok Airport", "Jeju Island", "KR", false], - "JDH": ["Jodhpur Airport", "Jodhpur", "IN", true], - "JDN": ["Jordan Airport", "Jordan", "US", false], - "JDO": ["Orlando Bezerra de Menezes Airport", "Juazeiro do Norte", "BR", true], - "JDR": ["Prefeito Octávio de Almeida Neves Airport", "São João Del Rei", "BR", false], - "JDZ": ["Jingdezhen Luojia Airport", "Jingdezhen", "CN", true], - "JED": ["King Abdulaziz International Airport", "Jeddah", "SA", true], - "JEE": ["Jérémie Airport", "Carrefour Sanon", "HT", true], - "JEF": ["Jefferson City Memorial Airport", "Jefferson City", "US", false], - "JEG": ["Aasiaat Airport", "Aasiaat", "GL", true], - "JEJ": ["Jeh Airport", "Ailinglapalap Atoll", "MH", true], - "JEK": ["Jeki Airport", "Jeki", "ZM", false], - "JEQ": ["Jequié Airport", "Jequié", "BR", false], - "JER": ["Jersey Airport", "St. Peter", "JE", true], - "JFK": ["John F. Kennedy International Airport", "New York", "US", true], - "JFM": ["Fremantle Heliport", "Fremantle", "AU", false], - "JFN": ["Northeast Ohio Regional Airport", "Ashtabula", "US", false], - "JFR": ["Paamiut Airport", "Paamiut", "GL", true], - "JGA": ["Jamnagar Airport", "Jamnagar", "IN", true], - "JGB": ["Jagdalpur Airport", "Jagdalpur", "IN", true], - "JGD": ["Daxing'anling Elunchun Airport", "Jiagedaqi", "CN", true], - "JGN": ["Jiayuguan International Airport", "Jiayuguan", "CN", true], - "JGO": ["Qeqertarsuaq Heliport", "Qeqertarsuaq", "GL", true], - "JGR": ["Kangilinnguit Heliport", "Kangilinnguit", "GL", true], - "JGS": ["Jinggangshan Airport", "Ji'an", "CN", true], - "JHB": ["Senai International Airport", "Johor Bahru", "MY", true], - "JHF": ["São Paulo Catarina Executive Airport", "São Roque", "BR", false], - "JHG": ["Xishuangbanna Gasa International Airport", "Jinghong (Gasa)", "CN", true], - "JHL": ["Fort MacKay/Albian Aerodrome", "Albian Village", "CA", false], - "JHM": ["Kapalua Airport", "Lahaina", "US", true], - "JHN": ["Jenderal Besar Abdul Haris Nasution Airport", "Mandailing", "ID", false], - "JHS": ["Sisimiut Airport", "Sisimiut", "GL", true], - "JHW": ["Chautauqua County-Jamestown Airport", "Jamestown", "US", false], - "JIA": ["Juína Airport", "Juína", "BR", false], - "JIB": ["Djibouti-Ambouli Airport", "Djibouti City", "DJ", true], - "JIC": ["Jinchang Jinchuan Airport", "Jinchang", "CN", true], - "JID": ["Recreation and Conference Center Heliport", "City of Industry", "US", false], - "JIJ": ["Gerad Wilwal International Airport", "Jijiga", "ET", true], - "JIK": ["Ikaria Airport", "Ikaria Island", "GR", true], - "JIL": ["Jilin Ertaizi Airport", "Jilin", "CN", false], - "JIM": ["Jimma Airport", "Jimma", "ET", true], - "JIN": ["Jinja Airport", "Jinja", "UG", false], - "JIO": ["Jos Orno Imsula Airport", "Tiakur", "ID", true], - "JIP": ["Jipijapa Airport", "Jipijapa", "EC", false], - "JIQ": ["Qianjiang Wulingshan Airport", "Qianjiang", "CN", true], - "JIR": ["Jiri Airport", "Jiri", "NP", false], - "JIU": ["Jiujiang Lushan Airport", "Jiujiang", "CN", true], - "JJD": ["Comandante Ariston Pessoa Airport", "Cruz", "BR", true], - "JJG": ["Humberto Ghizzo Bortoluzzi Regional Airport", "Jaguaruna", "BR", true], - "JJI": ["Juanjui Airport", "Juanjuí", "PE", false], - "JJM": ["Mulika Lodge Airport", "Meru-Kinna", "KE", true], - "JJN": ["Quanzhou Jinjiang International Airport", "Quanzhou", "CN", true], - "JJU": ["Qaqortoq Airport", "Qaqortoq", "GL", true], - "JKG": ["Jönköping Airport", "Jönköping", "SE", true], - "JKH": ["Chios Island National Airport", "Chios Island", "GR", true], - "JKL": ["Kalymnos Airport", "Kalymnos Island", "GR", true], - "JKR": ["Janakpur Airport", "Janakpur", "NP", true], - "JKV": ["Cherokee County Airport", "Jacksonville", "US", false], - "JLA": ["Quartz Creek Airport", "Cooper Landing", "US", false], - "JLG": ["Jalgaon Airport", "Jalgaon", "IN", true], - "JLN": ["Joplin Regional Airport", "Joplin", "US", true], - "JLR": ["Jabalpur Airport", "Jabalpur", "IN", true], - "JLS": ["Jales Airport", "Jales", "BR", false], - "JMA": ["Aeroporto Elias Breder Airport", "Manhuaçu", "BR", false], - "JMB": ["Jamba Airport", "Jamba", "AO", false], - "JMJ": ["Lancang Jingmai Airport", "Pu'er (Lancang)", "CN", true], - "JMK": ["Mykonos Island National Airport", "Mykonos", "GR", true], - "JMO": ["Jomsom Airport", "Jomsom", "NP", true], - "JMR": ["Maricá Airport", "Maricá", "BR", false], - "JMS": ["Jamestown Regional Airport", "Jamestown", "US", true], - "JMU": ["Jiamusi Songjiang International Airport", "Jiamusi", "CN", true], - "JNA": ["Januária Airport", "Januária", "BR", false], - "JNB": ["O.R. Tambo International Airport", "Johannesburg", "ZA", true], - "JNG": ["Jining Da'an Airport", "Jining", "CN", true], - "JNH": ["Jiaxing Nanhu Airport", "Xiuzhou, Hangzhou", "CN", true], - "JNJ": ["Ja'Aluni Airport", "Duqm", "OM", false], - "JNN": ["Nanortalik Heliport", "Nanortalik", "GL", true], - "JNS": ["Narsaq Heliport", "Narsaq", "GL", true], - "JNU": ["Juneau International Airport", "Juneau", "US", true], - "JNX": ["Naxos Island National Airport", "Naxos", "GR", true], - "JNZ": ["Jinzhou Bay Airport", "Jinzhou (Linghai)", "CN", true], - "JOE": ["Joensuu Airport", "Joensuu", "FI", true], - "JOG": ["Adisutjipto International Airport", "Yogyakarta", "ID", true], - "JOH": ["Port St Johns Airport", "Port St Johns", "ZA", false], - "JOI": ["Lauro Carneiro de Loyola Airport", "Joinville", "BR", true], - "JOJ": ["Doris Lake", "Hope Bay", "CA", false], - "JOK": ["Yoshkar-Ola Airport", "Yoshkar-Ola", "RU", false], - "JOL": ["Jolo Airport", "Jolo", "PH", true], - "JOM": ["Njombe Airport", "Njombe", "TZ", false], - "JOP": ["Josephstaal Airport", "Josephstaal", "PG", false], - "JOS": ["Yakubu Gowon Airport", "Jos", "NG", true], - "JOT": ["Joliet Regional Airport", "Joliet", "US", false], - "JPA": ["Presidente Castro Pinto International Airport", "João Pessoa", "BR", true], - "JPE": ["Nagib Demachki Airport", "Paragominas", "BR", true], - "JPN": ["Pentagon Army Heliport", "Washington DC", "US", false], - "JPO": ["Aeroporto Brigadeiro Firmino Ayres", "Patos", "BR", false], - "JPR": ["Ji-Paraná Airport", "Ji-Paraná", "BR", true], - "JPY": ["Paraty Airport", "Paraty", "BR", false], - "JQA": ["Qaarsut Airport", "Uummannaq", "GL", true], - "JQE": ["Jaqué Airport", "Jaqué", "PA", false], - "JRA": ["West 30th Street Heliport", "New York", "US", true], - "JRB": ["Downtown Skyport Heliport", "New York", "US", false], - "JRF": ["Kalaeloa Airport", "Kapolei", "US", false], - "JRG": ["Jharsuguda Airport", "", "IN", true], - "JRH": ["Jorhat Airport", "Jorhat", "IN", true], - "JRJ": ["Ganzhou Ruijin Airport", "Ganzhou", "CN", false], - "JRK": ["Arsuk Heliport", "Arsuk", "GL", false], - "JRN": ["Juruena Airport", "Juruena", "BR", false], - "JRO": ["Kilimanjaro International Airport", "Arusha", "TZ", true], - "JRT": ["Juruti Airport", "Juruti", "BR", false], - "JSA": ["Jaisalmer Airport", "", "IN", true], - "JSB": ["Walfrido Salmito de Almeida Airport", "São Benedito", "BR", false], - "JSH": ["Sitia Airport", "Crete Island", "GR", true], - "JSI": ["Skiathos Island National Airport", "Skiathos", "GR", true], - "JSJ": ["Jiansanjiang Shidi Airport", "Jiansanjiang", "CN", true], - "JSK": ["Jask Airport", "Bandar-e-Jask", "IR", true], - "JSM": ["Jose De San Martin Airport", "Chubut", "AR", false], - "JSO": ["Dr. Luciano de Arruda Coelho Regional Airport", "Sobral", "BR", false], - "JSR": ["Jessore Airport", "Jashore (Jessore)", "BD", true], - "JST": ["John Murtha Johnstown Cambria County Airport", "Johnstown", "US", true], - "JSU": ["Maniitsoq Airport", "Maniitsoq", "GL", true], - "JSY": ["Syros Airport", "Syros Island", "GR", true], - "JTA": ["Pedro Teixeira Castelo Airport", "Tauá", "BR", false], - "JTC": ["Bauru/Arealva–Moussa Nakhal Tobias State Airport", "Bauru", "BR", true], - "JTI": ["Jataí Airport", "Jataí", "BR", false], - "JTN": ["Itanhaém Airport", "Itanhaém", "BR", false], - "JTR": ["Santorini International Airport", "Santorini Island", "GR", true], - "JTY": ["Astypalaia Airport", "Astypalaia Island", "GR", true], - "JUA": ["Inácio Luís do Nascimento Airport", "Juara", "BR", false], - "JUB": ["Juba International Airport", "Juba", "SS", true], - "JUH": ["Chizhou Jiuhuashan Airport", "Chizhou", "CN", true], - "JUI": ["Juist Airport", "Juist", "DE", true], - "JUJ": ["Gobernador Horacio Guzman International Airport", "San Salvador de Jujuy", "AR", true], - "JUK": ["Ukkusissat Heliport", "Ukkusissat", "GL", true], - "JUL": ["Inca Manco Capac International Airport", "Juliaca", "PE", true], - "JUM": ["Jumla Airport", "Jumla", "NP", true], - "JUN": ["Jundah Airport", "Jundah", "AU", false], - "JUO": ["Juradó Airport", "Juradó", "CO", false], - "JUR": ["Jurien Bay Airport", "Jurien Bay", "AU", false], - "JUT": ["Jutigalpa airport", "Jutigalpa", "HN", false], - "JUU": ["Nuugaatsiaq Heliport", "Nuugaatsiaq", "GL", true], - "JUV": ["Upernavik Airport", "Upernavik", "GL", true], - "JUZ": ["Quzhou Airport", "Quzhou (Kezheng)", "CN", true], - "JVA": ["Ankavandra Airport", "Ankavandra", "MG", false], - "JVI": ["Central Jersey Regional Airport", "Manville", "US", false], - "JVL": ["Southern Wisconsin Regional Airport", "Janesville", "US", false], - "JWA": ["Jwaneng Airport", "Jwaneng", "BW", false], - "JWN": ["Zanjan Airport", "Zanjan", "IR", false], - "JWO": ["Jungwon Air Base/Chungju Airport", "Gimseang-ro", "KR", false], - "JXA": ["Jixi Xingkaihu Airport", "Jixi", "CN", true], - "JXN": ["Jackson County Airport/Reynolds Field", "Jackson", "US", false], - "JYR": ["Jiroft Airport", "Jiroft", "IR", false], - "JYV": ["Jyväskylä Airport", "Jyväskylän Maalaiskunta", "FI", true], - "JZH": ["Jiuzhai Huanglong Airport", "Ngawa (Songpan)", "CN", true], - "KAA": ["Kasama Airport", "Kasama", "ZM", true], - "KAB": ["Kariba Airport", "Kariba", "ZW", true], - "KAC": ["Qamishli International Airport", "Qamishli", "SY", true], - "KAD": ["Kaduna International Airport", "Kaduna", "NG", true], - "KAE": ["Kake Seaplane Base", "Kake", "US", true], - "KAF": ["Karato Airport", "Karato", "PG", false], - "KAG": ["Gangneung Airport (K-18)", "Gangneung", "KR", false], - "KAH": ["Melbourne Heliport, Batman Park", "Melbourne", "AU", false], - "KAI": ["Kaieteur Airport", "Kaieteur Falls", "GY", true], - "KAJ": ["Kajaani Airport", "Kajaani", "FI", true], - "KAK": ["Kar Airport", "Kar", "PG", false], - "KAL": ["Kaltag Airport", "Kaltag", "US", true], - "KAM": ["Kamaran Airport", "Kamaran", "YE", false], - "KAN": ["Mallam Aminu Kano International Airport", "Kano", "NG", true], - "KAO": ["Kuusamo Airport", "Kuusamo", "FI", true], - "KAP": ["Kapanga Airport", "Kapanga", "CD", false], - "KAQ": ["Kamulai Airport", "Kamulai Mission", "PG", false], - "KAR": ["Kamarang Airport", "Kamarang", "GY", false], - "KAS": ["Karasburg Airport", "Karasburg", "NA", false], - "KAT": ["Kaitaia Airport", "Awanui", "NZ", true], - "KAU": ["Kauhava Airfield", "Kauhava", "FI", false], - "KAV": ["Kavanayen Airport", "Kavanayén", "VE", false], - "KAW": ["Kawthoung Airport", "Kawthoung", "MM", true], - "KAX": ["Kalbarri Airport", "Kalbarri", "AU", true], - "KAY": ["Wakaya Island Airport", "Wakaya Island", "FJ", false], - "KAZ": ["Kuabang Airport", "Kao", "ID", false], - "KBA": ["Kabala Airport", "Kabala", "SL", false], - "KBB": ["Kirkimbie Station Airport", "Kirkimbie", "AU", false], - "KBC": ["Birch Creek Airport", "Birch Creek", "US", true], - "KBD": ["Kimberley Downs Airport", "Kimberley Downs", "AU", false], - "KBE": ["Bell Island Hot Springs Seaplane Base", "Bell Island", "US", false], - "KBF": ["Karubaga Airport", "Karubaga", "ID", false], - "KBH": ["Kahama Airstrip", "Kahama", "TZ", false], - "KBI": ["Kribi Airport", "Kribi", "CM", false], - "KBJ": ["Kings Canyon Airport", "Petermann", "AU", false], - "KBK": ["Kushinagar International Airport", "Kushinagar", "IN", false], - "KBL": ["Kabul International Airport", "Kabul", "AF", true], - "KBM": ["Kabwum", "", "PG", false], - "KBN": ["Tunta Airport", "Kabinda", "CD", false], - "KBO": ["Kabalo Airport", "Kabalo", "CD", false], - "KBP": ["Boryspil International Airport", "Boryspil", "UA", false], - "KBQ": ["Kasungu Airport", "Kasungu", "MW", false], - "KBR": ["Sultan Ismail Petra Airport", "Kota Baharu", "MY", true], - "KBS": ["Bo Airport", "Bo", "SL", false], - "KBT": ["Kaben Airport", "Kaben", "MH", false], - "KBU": ["Gusti Syamsir Alam Airport", "Stagen", "ID", true], - "KBV": ["Krabi International Airport", "Krabi", "TH", true], - "KBW": ["Chignik Bay Seaplane Base", "Chignik", "US", false], - "KBX": ["Kambuaya Airport", "Kambuaya-Papua Island", "ID", false], - "KBY": ["Streaky Bay Airport", "", "AU", false], - "KBZ": ["Kaikoura Airport", "", "NZ", false], - "KCA": ["Kuqa Qiuci Airport", "Kuqa", "CN", true], - "KCB": ["Tepoe Airstrip", "Kasikasima", "SR", false], - "KCC": ["Coffman Cove Seaplane Base", "Coffman Cove", "US", true], - "KCD": ["Kamur Airport", "Kamur", "ID", false], - "KCE": ["Collinsville Airport", "", "AU", false], - "KCF": ["Thar Airport", "Kandanwari", "PK", false], - "KCG": ["Chignik Airport", "Chignik", "US", true], - "KCH": ["Kuching International Airport", "Kuching", "MY", true], - "KCI": ["Koolan Central Airport", "Koolan Island", "AU", false], - "KCJ": ["Komaio Airport", "Komaio", "PG", false], - "KCK": ["Kirensk Airport", "Kirensk", "RU", false], - "KCL": ["Chignik Lagoon Airport", "Chignik Flats", "US", false], - "KCM": ["Kahramanmaraş Airport", "Kahramanmaraş", "TR", true], - "KCN": ["Chernofski Harbor Seaplane Base", "Chernofski Harbor", "US", false], - "KCO": ["Cengiz Topel Airport", "Kartepe", "TR", false], - "KCP": ["Kam'yanets'-Podil's'kyi Airfield", "Kamyan'ka", "UA", false], - "KCQ": ["Chignik Lake Airport", "Chignik Lake", "US", true], - "KCR": ["Colorado Creek Airport", "Colorado Creek", "US", false], - "KCS": ["Kings Creek Airport", "Petermann", "AU", false], - "KCT": ["Koggala Airport", "Galle", "LK", true], - "KCU": ["Masindi Airport", "Masindi", "UG", false], - "KCY": ["Krasnoyarsk Cheremshanka Airport", "Krasnoyarsk", "RU", true], - "KCZ": ["Kochi Ryoma Airport", "Nankoku", "JP", true], - "KDA": ["Kolda Airport", "Kolda", "SN", false], - "KDB": ["Kambalda Airport", "Kambalda West", "AU", false], - "KDC": ["Kandi Airport", "Kandi", "BJ", false], - "KDD": ["Khuzdar Airport", "Khuzdar", "PK", true], - "KDE": ["Koroba Airport", "Koroba", "PG", false], - "KDH": ["Ahmad Shah Baba International Airport", "Kandahar", "AF", true], - "KDI": ["Haluoleo Airport", "Kendari", "ID", true], - "KDJ": ["Ville Airport", "N'Djolé", "GA", false], - "KDK": ["Kodiak Municipal Airport", "Kodiak", "US", false], - "KDL": ["Kärdla Airport", "Kärdla", "EE", true], - "KDM": ["Kaadedhdhoo Airport", "Huvadhu Atoll", "MV", true], - "KDN": ["Ndende Airport", "Ndende", "GA", false], - "KDO": ["Kadhdhoo Airport", "Kadhdhoo", "MV", true], - "KDP": ["Kandep Airport", "Kandep", "PG", false], - "KDQ": ["Kamberatoro Airport", "Kamberatoro Mission", "PG", false], - "KDR": ["Kandrian Airport", "Kandrian", "PG", false], - "KDS": ["Kamaran Downs Airport", "Kamaran Downs", "AU", false], - "KDT": ["Kamphaeng Saen Airport", "Nakhon Pathom", "TH", false], - "KDU": ["Skardu International Airport", "Skardu", "PK", true], - "KDV": ["Vunisea Airport", "Vunisea", "FJ", true], - "KDW": ["Victoria Reservoir Seaplane Base", "Kandy", "LK", false], - "KDX": ["Kadugli Airport", "Kadugli", "SD", false], - "KDY": ["Typliy Klyuch Airport", "Khandyga", "RU", false], - "KDZ": ["Polgolla Reservoir Seaplane Base", "Kandy", "LK", true], - "KEA": ["Kerki Airport", "Kerki", "TM", false], - "KEB": ["Nanwalek Airport", "Nanwalek", "US", true], - "KEC": ["Kasenga Airport", "Kasenga", "CD", false], - "KED": ["Kaédi Airport", "Kaédi", "MR", false], - "KEE": ["Kelle Airport", "Kelle", "CG", false], - "KEF": ["Keflavik International Airport", "Reykjavík", "IS", true], - "KEG": ["Keglsugl Airport", "Denglagu Mission", "PG", false], - "KEH": ["Kenmore Air Harbor LLC Seaplane Base", "Kenmore", "US", true], - "KEI": ["Kepi Airport", "Kepi", "ID", false], - "KEJ": ["Alexei Leonov Kemerovo International Airport", "Kemerovo", "RU", true], - "KEK": ["Ekwok Airport", "Ekwok", "US", false], - "KEL": ["Kiel-Holtenau Airport", "Kiel", "DE", false], - "KEM": ["Kemi-Tornio Airport", "Kemi / Tornio", "FI", true], - "KEN": ["Kenema Airport", "Kenema", "SL", false], - "KEO": ["Odienne Airport", "Odienne", "CI", false], - "KEP": ["Nepalgunj Airport", "Nepalgunj", "NP", true], - "KEQ": ["Kebar Airport", "Kebar", "ID", false], - "KER": ["Ayatollah Hashemi Rafsanjani International Airport", "Kerman", "IR", true], - "KES": ["Kelsey Airport", "Kelsey", "CA", false], - "KET": ["Kengtung Airport", "Kengtung", "MM", true], - "KEU": ["Keekorok Airport", "Keekorok", "KE", false], - "KEV": ["Halli Airport", "Jämsä", "FI", false], - "KEW": ["Keewaywin Airport", "Keewaywin", "CA", true], - "KEX": ["Kanabea Airport", "Kanabea", "PG", false], - "KEY": ["Kericho Airport", "Kericho", "KE", false], - "KEZ": ["Kelani-Peliyagoda Seaplane Base", "Colombo", "LK", false], - "KFA": ["Kiffa Airport", "Kiffa", "MR", false], - "KFE": ["Fortescue - Dave Forrest Aerodrome", "Cloudbreak Village", "AU", false], - "KFG": ["Kalkgurung Airport", "", "AU", true], - "KFM": ["Kirby Lake Airport", "Kirby Lake", "CA", false], - "KFP": ["False Pass Airport", "False Pass", "US", true], - "KFS": ["Kastamonu Airport", "Kastamonu", "TR", true], - "KFZ": ["Kukës International Airport", "Kukës", "AL", false], - "KGA": ["Kananga Airport", "Kananga", "CD", true], - "KGB": ["Konge Airport", "Konge", "PG", false], - "KGC": ["Kingscote Airport", "", "AU", true], - "KGD": ["Khrabrovo Airport", "Kaliningrad", "RU", true], - "KGE": ["Kaghau Airport", "Kagau Island", "SB", true], - "KGF": ["Sary-Arka Airport", "Karaganda", "KZ", true], - "KGG": ["Kédougou Airport", "Kédougou", "SN", false], - "KGH": ["Yongai Airport", "Yongai", "PG", false], - "KGI": ["Kalgoorlie Boulder Airport", "Broadwood", "AU", true], - "KGJ": ["Karonga Airport", "Karonga", "MW", false], - "KGK": ["Koliganek Airport", "Koliganek", "US", true], - "KGL": ["Kigali International Airport", "Kigali", "RW", true], - "KGM": ["Kungim Airport", "Kungim", "PG", false], - "KGN": ["Kasongo-Lunda Airport", "Kasongo-Lunda", "CD", false], - "KGO": ["Kirovograd Airport", "Kirovograd", "UA", false], - "KGP": ["Kogalym International Airport", "Kogalym", "RU", true], - "KGQ": ["Kangersuatsiaq Heliport", "Kangersuatsiaq", "GL", true], - "KGR": ["Kulgera Airport", "Ghan", "AU", false], - "KGS": ["Kos International Airport \"Ippokratis\"", "Kos Island", "GR", true], - "KGT": ["Kangding Airport", "Garzê (Kangding)", "CN", true], - "KGU": ["Keningau Airport", "Keningau", "MY", false], - "KGW": ["Kagi Airport", "Kagi", "PG", false], - "KGX": ["Grayling Airport", "Grayling", "US", true], - "KGY": ["Kingaroy Airport", "", "AU", false], - "KGZ": ["Glacier Creek Airport", "Glacier Creek", "US", false], - "KHA": ["Khaneh Airport", "Khaneh", "IR", false], - "KHC": ["Kerch Airport", "Kerch", "UA", false], - "KHD": ["Khoram Abad Airport", "", "IR", true], - "KHE": ["Kherson International Airport", "Kherson", "UA", false], - "KHG": ["Kashgar Laining International Airport", "Kashgar", "CN", true], - "KHH": ["Kaohsiung International Airport", "Kaohsiung (Xiaogang)", "TW", true], - "KHI": ["Jinnah International Airport", "Karachi", "PK", true], - "KHJ": ["Kauhajoki Airfield", "Kauhajoki", "FI", false], - "KHK": ["Khark Airport", "Khark", "IR", true], - "KHM": ["Kanti Airport", "Kanti", "MM", true], - "KHN": ["Nanchang Changbei International Airport", "Nanchang", "CN", true], - "KHO": ["Khoka Moya Airport", "Khoka Moya", "ZA", false], - "KHQ": ["Kullorsuaq Heliport", "Kullorsuaq", "GL", false], - "KHR": ["Kharkhorin Airport", "", "MN", false], - "KHS": ["Khasab Airport", "Khasab", "OM", true], - "KHT": ["Khost International Airport", "Khost", "AF", true], - "KHU": ["Kakhnovka Airfield", "Kremenchuk", "UA", false], - "KHV": ["Khabarovsk Novy Airport", "Khabarovsk", "RU", true], - "KHW": ["Khwai River Lodge Airport", "Khwai River Lodge", "BW", false], - "KHX": ["Savannah Airstrip", "Kihihi", "UG", true], - "KHY": ["Khoy Airport", "Khoy", "IR", false], - "KHZ": ["Kauehi Airport", "Kauehi", "PF", true], - "KIB": ["Ivanof Bay Seaplane Base", "Ivanof Bay", "US", false], - "KIC": ["Mesa Del Rey Airport", "King City", "US", false], - "KID": ["Kristianstad Airport", "Kristianstad", "SE", false], - "KIE": ["Aropa Airport", "Kieta", "PG", true], - "KIF": ["Kingfisher Lake Airport", "Kingfisher Lake", "CA", true], - "KIG": ["Koingnaas Airport", "Koingnaas", "ZA", false], - "KIH": ["Kish International Airport", "Kish Island", "IR", true], - "KII": ["Kibuli Airstrip", "Kibuli", "PG", false], - "KIJ": ["Niigata Airport", "Niigata", "JP", true], - "KIK": ["Kirkuk International Airport", "Kirkuk", "IQ", true], - "KIL": ["Kilwa Airport", "Kilwa", "CD", false], - "KIM": ["Kimberley Airport", "Kimberley", "ZA", true], - "KIN": ["Norman Manley International Airport", "Kingston", "JM", true], - "KIO": ["Kili Airport", "Kili Island", "MH", true], - "KIP": ["Kickapoo Downtown Airport", "Wichita Falls", "US", false], - "KIQ": ["Kira Airport", "Kira", "PG", false], - "KIR": ["Kerry Airport", "Farranfore", "IE", true], - "KIS": ["Kisumu International Airport", "Kisumu", "KE", true], - "KIT": ["Kithira Airport", "Kithira Island", "GR", true], - "KIU": ["Kiunga Airport", "Kiunga", "KE", false], - "KIW": ["Southdowns Airport", "Kitwe", "ZM", false], - "KIX": ["Kansai International Airport", "Osaka", "JP", true], - "KIY": ["Kilwa Airport", "Kilwa Masoko", "TZ", false], - "KJA": ["Krasnoyarsk International Airport", "Krasnoyarsk", "RU", true], - "KJB": ["Kurnool Airport", "Orvakal", "IN", true], - "KJH": ["Kaili Huangping Airport", "Kaili (Huangping)", "CN", true], - "KJI": ["Burqin Kanas Airport", "Burqin", "CN", true], - "KJJ": ["Kajjansi Airfield", "Kampala", "UG", false], - "KJK": ["Kortrijk-Wevelgem International Airport", "Wevelgem", "BE", false], - "KJP": ["Kerama Airport", "Zamami", "JP", false], - "KJT": ["Kertajati International Airport", "Kertajati", "ID", true], - "KJX": ["Kuala Batu Airport", "Blangpidie", "ID", false], - "KKA": ["Koyuk Alfred Adams Airport", "Koyuk", "US", true], - "KKB": ["Kitoi Bay Seaplane Base", "Kitoi Bay", "US", true], - "KKC": ["Khon Kaen Airport", "Khon Kaen", "TH", true], - "KKD": ["Kokoda Airport", "Kokoda", "PG", false], - "KKE": ["Kerikeri Airport", "Kerikeri", "NZ", true], - "KKG": ["Konawaruk Airport", "Konawaruk", "GY", false], - "KKH": ["Kongiganak Airport", "Kongiganak", "US", true], - "KKI": ["Akiachak Airport", "Akiachak", "US", true], - "KKJ": ["Kitakyushu Airport", "Kitakyushu", "JP", true], - "KKK": ["Kalakaket Creek AS Airport", "Kalakaket Creek", "US", false], - "KKL": ["Karluk Lake Seaplane Base", "Karluk Lake", "US", false], - "KKM": ["Khok Kathiam Airport", "Lop Buri", "TH", false], - "KKN": ["Kirkenes Airport, Høybuktmoen", "Kirkenes", "NO", true], - "KKO": ["Kaikohe Airport", "Kaikohe", "NZ", false], - "KKP": ["Koolburra Airport", "Koolburra", "AU", false], - "KKQ": ["Krasnoselkup Airport", "Krasnoselkup", "RU", false], - "KKR": ["Kaukura Airport", "Raitahiti", "PF", true], - "KKS": ["Kashan Airport", "Kashan", "IR", true], - "KKT": ["Kentland Municipal Airport", "Kentland", "US", false], - "KKU": ["Ekuk Airport", "Ekuk", "US", false], - "KKW": ["Kikwit Airport", "Kikwit", "CD", true], - "KKX": ["Kikai Airport", "Kikai", "JP", true], - "KKY": ["Kilkenny Airport", "Kilkenny", "IE", false], - "KKZ": ["Koh Kong Airport", "Krong Khemara Phoumin", "KH", false], - "KLB": ["Kalabo Airport", "Kalabo", "ZM", false], - "KLC": ["Kaolack Airport", "Kaolack", "SN", false], - "KLD": ["Migalovo Air Base", "Tver", "RU", false], - "KLE": ["Kaélé Airport", "Kaélé", "CM", false], - "KLF": ["Grabtsevo Airport", "Kaluga", "RU", false], - "KLG": ["Kalskag Airport", "Kalskag", "US", true], - "KLH": ["Kolhapur Airport", "Kolhapur", "IN", true], - "KLI": ["Kotakoli Airbase", "Kotakoli", "CD", false], - "KLK": ["Kalokol Airport", "Kalokol", "KE", false], - "KLL": ["Levelock Airport", "Levelock", "US", false], - "KLM": ["Kalaleh Airport", "Kalaleh", "IR", false], - "KLN": ["Larsen Bay Airport", "Larsen Bay", "US", true], - "KLO": ["Kalibo International Airport", "Kalibo", "PH", true], - "KLP": ["Seruyan Kuala Pembuang Airport", "Seruyan", "ID", true], - "KLR": ["Kalmar Airport", "Kalmar", "SE", true], - "KLS": ["Southwest Washington Regional Airport", "Kelso", "US", false], - "KLU": ["Klagenfurt Airport", "Klagenfurt am Wörthersee", "AT", true], - "KLV": ["Karlovy Vary Airport", "Karlovy Vary", "CZ", true], - "KLW": ["Klawock Airport", "Klawock", "US", true], - "KLX": ["Kalamata Airport", "Kalamata", "GR", true], - "KLY": ["Kinkungwa Airport", "Kalima", "CD", false], - "KLZ": ["Kleinsee Airport", "Kleinsee", "ZA", false], - "KMA": ["Kerema Airport", "Kerema", "PG", true], - "KMB": ["Koinambe Airport", "Konambe", "PG", false], - "KMC": ["King Khaled Military City Airport", "King Khaled Military City", "SA", true], - "KME": ["Kamembe Airport", "Kamembe", "RW", true], - "KMF": ["Kamina Airport", "Hoieti", "PG", false], - "KMG": ["Kunming Changshui International Airport", "Kunming", "CN", true], - "KMH": ["Johan Pienaar Airport", "Kuruman", "ZA", false], - "KMI": ["Miyazaki Airport", "Miyazaki", "JP", true], - "KMJ": ["Kumamoto Airport", "Kumamoto", "JP", true], - "KMK": ["Makabana Airport", "Makabana", "CG", false], - "KML": ["Kamileroi Airport", "", "AU", false], - "KMM": ["Kimaam Airport", "Kimaam", "ID", false], - "KMN": ["Kamina City Airport", "Kamina", "CD", true], - "KMO": ["Manokotak Airport", "Manokotak", "US", true], - "KMP": ["Keetmanshoop Airport", "Keetmanshoop", "NA", false], - "KMQ": ["Komatsu Airport / JASDF Komatsu Air Base", "Kanazawa", "JP", true], - "KMR": ["Karimui Airport", "Karimui", "PG", false], - "KMS": ["Prempeh I International Airport", "Kumasi", "GH", true], - "KMU": ["Kismayo Airport", "Kismayo", "SO", false], - "KMV": ["Kalay Airport", "Kalemyo", "MM", false], - "KMW": ["Kostroma Sokerkino Airport", "Kostroma", "RU", true], - "KMX": ["King Khalid Air Base", "Khamis Mushait", "SA", false], - "KMY": ["Moser Bay Seaplane Base", "Moser Bay", "US", true], - "KMZ": ["Kaoma Airport", "Kaoma", "ZM", false], - "KNA": ["Viña del Mar Airport", "Viña del Mar", "CL", false], - "KNB": ["Kanab Municipal Airport", "Kanab", "US", false], - "KND": ["Kindu Airport", "Kindu", "CD", true], - "KNE": ["Kanainj Airport", "Kanainj", "PG", false], - "KNF": ["RAF Marham", "King's Lynn, Norfolk", "GB", false], - "KNG": ["Utarom Airport", "Kaimana", "ID", true], - "KNH": ["Kinmen Airport", "Shang-I", "TW", true], - "KNI": ["Katanning Airport", "", "AU", false], - "KNJ": ["Kindamba Airport", "Kindamba", "CG", false], - "KNK": ["Kokhanok Airport", "Kokhanok", "US", true], - "KNM": ["Kaniama Airport", "Kaniama", "CD", false], - "KNN": ["Kankan Airport", "Kankan", "GN", false], - "KNO": ["Kualanamu International Airport", "Beringin", "ID", true], - "KNP": ["Kapanda Airport", "Capanda", "AO", false], - "KNQ": ["Koné Airport", "Koné", "NC", true], - "KNR": ["Jam Airport", "Jam", "IR", false], - "KNS": ["King Island Airport", "", "AU", true], - "KNT": ["Kennett Memorial Airport", "Kennett", "US", false], - "KNU": ["Kanpur Airport", "Kanpur", "IN", true], - "KNW": ["New Stuyahok Airport", "New Stuyahok", "US", true], - "KNX": ["East Kimberley Regional (Kununurra) Airport", "Kununurra", "AU", true], - "KNY": ["Kodinsk Airport", "", "RU", false], - "KNZ": ["Kenieba Airport", "Kenieba", "ML", false], - "KOA": ["Ellison Onizuka Kona International Airport at Keāhole", "Kailua-Kona", "US", true], - "KOC": ["Koumac Airport", "Koumac", "NC", true], - "KOD": ["Kotabangun Airport", "Kotabangun-Borneo Island", "ID", false], - "KOE": ["El Tari Airport", "Kupang", "ID", true], - "KOF": ["Komatipoort Airport", "Komatipoort", "ZA", false], - "KOH": ["Koolatah Airport", "Maramie", "AU", false], - "KOI": ["Kirkwall Airport", "Kirkwall, Orkney Islands", "GB", true], - "KOJ": ["Kagoshima Airport", "Kagoshima", "JP", true], - "KOK": ["Kokkola-Pietarsaari Airport", "Kokkola / Kruunupyy", "FI", true], - "KOM": ["Komo-Manda Airport", "Komo", "PG", false], - "KOO": ["Kongolo Airport", "Kongolo", "CD", false], - "KOP": ["Nakhon Phanom Airport", "Nakhon Phanom", "TH", true], - "KOQ": ["Köthen Airport", "Köthen (Anhalt)", "DE", false], - "KOR": ["Kakoro(Koroko) Airstrip", "Kakoro", "PG", false], - "KOS": ["Sihanouk International Airport", "Preah Sihanouk", "KH", true], - "KOT": ["Kotlik Airport", "Kotlik", "US", true], - "KOU": ["Koulamoutou Mabimbi Airport", "Koulamoutou", "GA", false], - "KOV": ["Kokshetau International Airport", "Kokshetau", "KZ", true], - "KOW": ["Ganzhou Huangjin Airport", "Ganzhou", "CN", true], - "KOX": ["Kokonao Airport", "Kokonao", "ID", false], - "KOY": ["Olga Bay Seaplane Base", "Olga Bay", "US", true], - "KOZ": ["Ouzinkie Airport", "Ouzinkie", "US", true], - "KPA": ["Kopiago Airport", "Kopiago", "PG", false], - "KPB": ["Point Baker Seaplane Base", "Point Baker", "US", true], - "KPC": ["Port Clarence Coast Guard Station", "Port Clarence", "US", false], - "KPE": ["Yapsiei Airport", "", "PG", false], - "KPF": ["Kondobol Airport", "Kondobol", "PG", false], - "KPI": ["Kapit Airport", "Kapit", "MY", false], - "KPL": ["Kapal Airport", "Kapal", "PG", false], - "KPM": ["Kompiam Airport", "", "PG", false], - "KPN": ["Kipnuk Airport", "Kipnuk", "US", true], - "KPO": ["Pohang Airport (G-815/K-3)", "Pohang", "KR", true], - "KPP": ["Kalpowar Airport", "Kalpower", "AU", false], - "KPR": ["Port Williams Seaplane Base", "Port Williams", "US", true], - "KPS": ["Kempsey Airport", "", "AU", false], - "KPT": ["Jackpot Airport/Hayden Field", "Jackpot", "US", false], - "KPV": ["Perryville Airport", "Perryville", "US", true], - "KPW": ["Keperveem Airport", "Keperveem", "RU", true], - "KPY": ["Port Bailey Seaplane Base", "Port Bailey", "US", true], - "KQA": ["Akutan Airport", "Akutan", "US", true], - "KQH": ["Kishangarh Airport Ajmer", "Ajmer (Kishangarh)", "IN", true], - "KQL": ["Kol Airport", "Kol", "PG", false], - "KQR": ["Karara Airport", "Karara", "AU", true], - "KQT": ["Bokhtar International Airport", "Bokhtar", "TJ", true], - "KRA": ["Kerang Airport", "", "AU", false], - "KRB": ["Karumba Airport", "", "AU", true], - "KRC": ["Departi Parbo Airport", "Sungai Penuh", "ID", true], - "KRD": ["Kurundi Airport", "Kurundi Station", "AU", false], - "KRE": ["Kirundo Airport", "Kirundo", "BI", true], - "KRF": ["Kramfors-Sollefteå Höga Kusten Airport", "Nyland", "SE", true], - "KRG": ["Karasabai Airport", "Karasabai", "GY", false], - "KRI": ["Kikori Airport", "Kikori", "PG", true], - "KRJ": ["Karawari Airstrip", "Amboin", "PG", false], - "KRK": ["Kraków John Paul II International Airport", "Balice", "PL", true], - "KRL": ["Korla Licheng Airport", "Korla", "CN", true], - "KRM": ["Karanambo Airport", "Karanambo", "GY", false], - "KRN": ["Kiruna Airport", "Kiruna", "SE", true], - "KRO": ["Kurgan Airport", "Kurgan", "RU", true], - "KRP": ["Midtjyllands Airport / Air Base Karup", "Karup", "DK", true], - "KRQ": ["Kramatorsk Airport", "Kramatorsk", "UA", false], - "KRR": ["Krasnodar Pashkovsky International Airport", "Krasnodar", "RU", true], - "KRS": ["Kristiansand Airport", "Kristiansand(Kjevik)", "NO", true], - "KRT": ["Khartoum International Airport", "Khartoum", "SD", true], - "KRU": ["Kerau Airport", "Gunim", "PG", false], - "KRV": ["Kerio Valley Airport", "Kimwarer", "KE", false], - "KRW": ["Turkmenbaşy International Airport", "Turkmenbaşy", "TM", true], - "KRX": ["Kar Kar Airport", "Kar Kar Island", "PG", false], - "KRY": ["Karamay Airport", "Karamay", "CN", true], - "KRZ": ["Kiri Airport", "Kiri", "CD", false], - "KSA": ["Kosrae International Airport", "Okat", "FM", true], - "KSB": ["Kasonombe Airport", "Kasonombe", "PG", false], - "KSC": ["Košice International Airport", "Košice", "SK", true], - "KSD": ["Karlstad Airport", "Karlstad", "SE", true], - "KSE": ["Kasese Airport", "Kasese", "UG", false], - "KSF": ["Kassel Airport", "Calden", "DE", true], - "KSG": ["Kisengam Airport [CLOSED]", "Kisenden", "PG", false], - "KSH": ["Shahid Ashrafi Esfahani Airport", "Kermanshah", "IR", true], - "KSI": ["Kissidougou Airport", "Kissidougou", "GN", false], - "KSJ": ["Kasos Airport", "Kasos Island", "GR", true], - "KSK": ["Karlskoga Airport", "Karlskoga", "SE", false], - "KSL": ["Kassala Airport", "Kassala", "SD", true], - "KSM": ["St Mary's Airport", "St Mary's", "US", true], - "KSN": ["Kostanay International Airport", "Kostanay", "KZ", true], - "KSO": ["Kastoria National Airport Aristotle", "Argos Orestiko", "GR", true], - "KSP": ["Kosipe Airport", "Kosipe Mission", "PG", false], - "KSQ": ["Karshi Airport", "Karshi", "UZ", true], - "KSR": ["Selayar - Haji Aroeppala Airport", "Benteng", "ID", true], - "KSS": ["Sikasso (Dignangan) Airport", "Sikasso", "ML", false], - "KSU": ["Kristiansund Airport, Kvernberget", "Kvernberget", "NO", true], - "KSV": ["Springvale Airport", "", "AU", false], - "KSW": ["Kiryat Shmona Airport", "Kiryat Shmona", "IL", false], - "KSX": ["Yasuru Airport", "Yasuru", "PG", false], - "KSY": ["Kars Airport", "Kars", "TR", true], - "KSZ": ["Kotlas Airport", "Kotlas", "RU", true], - "KTA": ["Karratha Airport", "Karratha", "AU", true], - "KTB": ["Thorne Bay Seaplane Base", "Thorne Bay", "US", true], - "KTC": ["Katiola Airport", "Katiola", "CI", false], - "KTD": ["Kitadaito Airport", "Kitadaitōjima", "JP", true], - "KTE": ["Kerteh Airport", "Kerteh", "MY", false], - "KTF": ["Takaka Airport", "", "NZ", false], - "KTG": ["Rahadi Osman Airport", "Ketapang", "ID", true], - "KTH": ["Tikchik Lodge Seaplane Base", "Tikchik", "US", false], - "KTI": ["Techo International Airport", "Phnom Penh (Boeng Khyang)", "KH", true], - "KTJ": ["Kichwa Tembo Airport", "Kichwa Tembo", "KE", false], - "KTL": ["Kitale Airport", "Kitale", "KE", false], - "KTM": ["Tribhuvan International Airport", "Kathmandu", "NP", true], - "KTN": ["Ketchikan International Airport", "Ketchikan", "US", true], - "KTO": ["Kato Airport", "Kato", "GY", false], - "KTP": ["Tinson Pen Airport", "Tinson Pen", "JM", true], - "KTQ": ["Kitee Airport", "Kitee", "FI", false], - "KTR": ["Tindal Airport", "", "AU", false], - "KTS": ["Brevig Mission Airport", "Brevig Mission", "US", true], - "KTT": ["Kittilä International Airport", "Kittilä", "FI", true], - "KTU": ["Kota Airport", "Kota", "IN", false], - "KTV": ["Kamarata Airport", "Kamarata", "VE", false], - "KTW": ["Katowice Wojciech Korfanty International Airport", "Katowice", "PL", true], - "KTY": ["Katukurunda Air Force Base", "Kalutara", "LK", false], - "KUA": ["Kuantan Airport", "Kuantan", "MY", true], - "KUB": ["Anduki Airport", "Seria", "BN", false], - "KUC": ["Kuria Airport", "Kuria", "KI", false], - "KUD": ["Kudat Airport", "Kudat", "MY", true], - "KUE": ["Kukudu Airport", "Kolombangara Island", "SB", false], - "KUF": ["Kurumoch International Airport", "Samara", "RU", true], - "KUG": ["Kubin Island Airport", "Kubin Island", "AU", true], - "KUH": ["Kushiro Airport", "Kushiro", "JP", true], - "KUK": ["Kasigluk Airport", "Kasigluk", "US", true], - "KUL": ["Kuala Lumpur International Airport", "Sepang", "MY", true], - "KUM": ["Yakushima Airport", "Yakushima", "JP", true], - "KUN": ["Kaunas International Airport", "Kaunas", "LT", true], - "KUO": ["Kuopio Airport", "Kuopio / Siilinjärvi", "FI", true], - "KUP": ["Kupiano Airport", "Kupiano", "PG", false], - "KUQ": ["Kuri Airport", "Kuri", "PG", false], - "KUR": ["Razer Airport", "Skazar", "AF", false], - "KUS": ["Kulusuk Airport", "Kulusuk", "GL", true], - "KUT": ["David the Builder Kutaisi International Airport", "Kopitnari", "GE", true], - "KUU": ["Kullu Manali Airport", "Bhuntar", "IN", true], - "KUV": ["Gunsan Airport / Gunsan Air Base", "Gunsan", "KR", true], - "KUX": ["Kuyol Airport", "", "PG", false], - "KUY": ["Kamusi Airport", "Kamusi", "PG", false], - "KUZ": ["Kuummiut Heliport", "Kuummiut", "GL", true], - "KVA": ["Kavala Alexander the Great International Airport", "Kavala", "GR", true], - "KVB": ["Skövde Airport", "Skövde", "SE", false], - "KVC": ["King Cove Airport", "King Cove", "US", true], - "KVE": ["Kitava Airport", "Kitava Island", "PG", false], - "KVG": ["Kavieng Airport", "Kavieng", "PG", true], - "KVK": ["Kirovsk-Apatity Airport", "Apatity", "RU", true], - "KVL": ["Kivalina Airport", "Kivalina", "US", true], - "KVM": ["Markovo Airport", "Markovo", "RU", true], - "KVO": ["Morava Airport", "Kraljevo", "RS", true], - "KVR": ["Kavalerovo Airport", "Kavalerovo", "RU", false], - "KVU": ["Korolevu Seaplane Base", "Korolevu", "FJ", false], - "KVX": ["Pobedilovo Airport", "Kirov", "RU", true], - "KWA": ["Bucholz Army Air Field", "Kwajalein", "MH", true], - "KWB": ["Dewadaru Airport", "Karimunjawa", "ID", true], - "KWD": ["Kavadja Airport", "Kavadja", "CF", false], - "KWE": ["Guiyang Longdongbao International Airport", "Guiyang (Nanming)", "CN", true], - "KWF": ["Waterfall Seaplane Base", "Waterfall", "US", false], - "KWG": ["Kryvyi Rih International Airport", "Kryvyi Rih", "UA", false], - "KWH": ["Khwahan Airport", "Khwahan", "AF", false], - "KWI": ["Kuwait International Airport", "Kuwait City", "KW", true], - "KWJ": ["Gwangju Airport", "Gwangju", "KR", true], - "KWK": ["Kwigillingok Airport", "Kwigillingok", "US", true], - "KWL": ["Guilin Liangjiang International Airport", "Guilin (Lingui)", "CN", true], - "KWM": ["Kowanyama Airport", "Kowanyama", "AU", true], - "KWN": ["Quinhagak Airport", "Quinhagak", "US", true], - "KWO": ["Kawito Airport", "Kawito", "PG", false], - "KWP": ["West Point Village Seaplane Base", "West Point", "US", true], - "KWS": ["Kwailabesi Airport", "Kwailabesi", "SB", false], - "KWT": ["Kwethluk Airport", "Kwethluk", "US", true], - "KWY": ["Kiwayu Airport", "Kiwayu", "KE", false], - "KWZ": ["Kolwezi Airport", "Kolwezi", "CD", true], - "KXA": ["Kasaan Seaplane Base", "Kasaan", "US", true], - "KXB": ["Sangia Nibandera Airport", "Kolaka", "ID", true], - "KXD": ["Kondinskoye Airport", "Kondinskoye", "RU", false], - "KXE": ["P C Pelser Airport", "Klerksdorp", "ZA", false], - "KXF": ["Koro Island Airport", "Koro Island", "FJ", true], - "KXK": ["Komsomolsk-on-Amur Airport", "Komsomolsk-on-Amur", "RU", true], - "KXO": ["Kisoro Airport", "Kisoro", "UG", true], - "KXU": ["Katiu Airport", "Katiu", "PF", false], - "KYA": ["Konya Airport", "Konya", "TR", true], - "KYB": ["Ken's Bore airport", "Ken's Bore", "AU", false], - "KYD": ["Lanyu Airport", "Orchid Island", "TW", true], - "KYE": ["Rene Mouawad Air Base", "Tripoli", "LB", false], - "KYF": ["Yeelirrie Airport", "", "AU", false], - "KYI": ["Yalata Mission Airport", "Yalata Mission", "AU", false], - "KYK": ["Karluk Airport", "Karluk", "US", true], - "KYO": ["Tampa North Aero Park Airport", "Tampa", "US", false], - "KYP": ["Kyaukpyu Airport", "Kyaukpyu", "MM", true], - "KYS": ["Kayes Dag Dag Airport", "Kayes", "ML", true], - "KYT": ["Kyauktu Airport", "Kyauktu", "MM", false], - "KYU": ["Koyukuk Airport", "Koyukuk", "US", true], - "KYX": ["Yalumet Airport", "Yalumet", "PG", false], - "KYZ": ["Kyzyl Airport", "Kyzyl", "RU", true], - "KZB": ["Zachar Bay Seaplane Base", "Zachar Bay", "US", true], - "KZD": ["Krakor Airport", "Krakor", "KH", false], - "KZF": ["Kaintiba Airport", "Kaintiba", "PG", false], - "KZG": ["Kitzingen Airfield", "Kitzingen", "DE", false], - "KZI": ["Kozani National Airport Filippos", "Kozani", "GR", true], - "KZN": ["Kazan International Airport", "Kazan", "RU", true], - "KZO": ["Korkyt Ata International Airport", "Kyzylorda", "KZ", true], - "KZR": ["Zafer Airport", "Altıntaş", "TR", true], - "KZS": ["Kastelorizo Airport", "Kastelorizo Island", "GR", true], - "LAA": ["Southeast Colorado Regional Airport", "Lamar", "US", false], - "LAB": ["Lab Lab Airport", "Lab Lab Mission", "PG", false], - "LAC": ["Layang-Layang Airport", "Pulau Layang-Layang (Swallow Reef)", "MY", false], - "LAD": ["Quatro de Fevereiro International Airport", "Luanda", "AO", true], - "LAE": ["Nadzab Tomodachi International Airport", "Lae", "PG", true], - "LAF": ["Purdue University Airport", "West Lafayette", "US", true], - "LAH": ["Oesman Sadik Airport", "Labuha-Halmahera Island", "ID", false], - "LAI": ["Lannion Airport", "Lannion", "FR", false], - "LAJ": ["Lages Airport", "Lages", "BR", true], - "LAK": ["Aklavik/Freddie Carmichael Airport", "Aklavik", "CA", true], - "LAL": ["Lakeland Linder International Airport", "Lakeland", "US", true], - "LAM": ["Los Alamos Airport", "Los Alamos", "US", false], - "LAN": ["Capital Region International Airport", "Lansing", "US", true], - "LAO": ["Laoag International Airport", "Laoag City", "PH", true], - "LAP": ["Manuel Márquez de León International Airport", "La Paz", "MX", true], - "LAQ": ["Al Abraq International Airport", "Al Albraq", "LY", true], - "LAR": ["Laramie Regional Airport", "Laramie", "US", true], - "LAS": ["Harry Reid International Airport", "Las Vegas", "US", true], - "LAU": ["Manda Airport", "Lamu", "KE", true], - "LAW": ["Lawton Fort Sill Regional Airport", "Lawton", "US", true], - "LAX": ["Los Angeles International Airport", "Los Angeles", "US", true], - "LAY": ["Ladysmith Airport", "Ladysmith", "ZA", false], - "LAZ": ["Bom Jesus da Lapa Airport", "Bom Jesus da Lapa", "BR", false], - "LBA": ["Leeds Bradford Airport", "Leeds, West Yorkshire", "GB", true], - "LBB": ["Lubbock Preston Smith International Airport", "Lubbock", "US", true], - "LBC": ["Lübeck Blankensee Airport", "Lübeck", "DE", true], - "LBD": ["Khujand International Airport", "Khujand", "TJ", true], - "LBE": ["Arnold Palmer Regional Airport", "Latrobe", "US", true], - "LBF": ["North Platte Regional Airport Lee Bird Field", "North Platte", "US", true], - "LBG": ["Paris-Le Bourget International Airport", "Paris", "FR", false], - "LBH": ["Palm Beach Seaplane Base", "Sydney", "AU", true], - "LBI": ["Albi Le Sequestre airport", "Albi", "FR", false], - "LBJ": ["Komodo Airport", "Labuan Bajo, Manggarai Barat", "ID", true], - "LBK": ["Liboi Airport", "Liboi", "KE", false], - "LBL": ["Liberal Mid-America Regional Airport", "Liberal", "US", true], - "LBM": ["Luabo Airport", "Luabo", "MZ", false], - "LBN": ["Lake Baringo (Kampi Ya Samaki) Airport", "Akorian", "KE", false], - "LBO": ["Lusambo Airport", "Lusambo", "CD", false], - "LBP": ["Long Banga Airport", "Long Banga", "MY", true], - "LBQ": ["Lambarene Airport", "Lambarene", "GA", false], - "LBR": ["Lábrea Airport", "Lábrea", "BR", true], - "LBS": ["Labasa Airport", "Labasa", "FJ", true], - "LBT": ["Lumberton Regional Airport", "Lumberton", "US", false], - "LBU": ["Labuan Airport", "Labuan", "MY", true], - "LBV": ["Libreville Leon M'ba International Airport", "Libreville", "GA", true], - "LBW": ["Yuvai Semaring Airport", "Long Bawan", "ID", true], - "LBX": ["Lubang Airport", "Lubang", "PH", false], - "LBY": ["La Baule-Escoublac Airport", "La Baule-Escoublac", "FR", false], - "LBZ": ["Lucapa Airport", "Lucapa", "AO", false], - "LCA": ["Larnaca International Airport", "Larnaca", "CY", true], - "LCB": ["André Antônio Maggi Airport", "Pontes e Lacerda", "BR", false], - "LCC": ["Lecce Galatina Air Base / Galatina Fortunato Cesari Airport", "Galatina (LE)", "IT", false], - "LCD": ["Louis Trichardt Airport", "Louis Trichardt", "ZA", false], - "LCE": ["Golosón International Airport", "La Ceiba", "HN", true], - "LCF": ["Las Vegas Airport", "Rio Dulce", "GT", false], - "LCG": ["A Coruña Airport", "Culleredo", "ES", true], - "LCH": ["Lake Charles Regional Airport", "Lake Charles", "US", true], - "LCI": ["Laconia Municipal Airport", "Laconia / Gilford", "US", false], - "LCJ": ["Łódź Władysław Reymont Airport", "Łódź", "PL", true], - "LCK": ["Rickenbacker International Airport", "Columbus", "US", true], - "LCM": ["La Cumbre Airport", "La Cumbre", "AR", false], - "LCN": ["Balcanoona Airport", "", "AU", false], - "LCO": ["Lague Airport", "Lague", "CG", false], - "LCP": ["Loncopue Airport", "Loncopue", "AR", false], - "LCQ": ["Lake City Gateway Airport", "Lake City", "US", false], - "LCR": ["Virgilio Barco Vargas (La Chorrera) Airport", "La Chorrera", "CO", true], - "LCS": ["Longchuan Guangsong Airport", "Dehong (Longchuan)", "CN", false], - "LCT": ["Shijiazhuang Luancheng Airport", "Shijiazhuang", "CN", false], - "LCX": ["Liancheng Guanzhishan Airport", "Longyan (Liancheng)", "CN", true], - "LCY": ["London City Airport", "London", "GB", true], - "LDA": ["Malda Airport", "Malda", "IN", false], - "LDB": ["Governor José Richa Airport", "Londrina", "BR", true], - "LDC": ["Lindeman Island Airport", "Lindeman Island", "AU", false], - "LDE": ["Tarbes-Lourdes-Pyrénées Airport", "Tarbes/Lourdes/Pyrénées", "FR", true], - "LDG": ["Leshukonskoye Airport", "Leshukonskoye", "RU", true], - "LDH": ["Lord Howe Island Airport", "Lord Howe Island", "AU", true], - "LDI": ["Lindi Airport", "Lindi", "TZ", false], - "LDJ": ["Linden Airport", "Linden", "US", false], - "LDK": ["Lidköping-Hovby Airport", "Lidköping", "SE", false], - "LDM": ["Mason County Airport", "Ludington", "US", false], - "LDN": ["Lamidanda Airport", "Lamidanda", "NP", false], - "LDO": ["Ladouanie Airport", "Aurora", "SR", false], - "LDS": ["Yichun Lindu Airport", "Yichun", "CN", true], - "LDU": ["Lahad Datu Airport", "Lahad Datu", "MY", true], - "LDV": ["Landivisiau Air Base", "Landivisiau", "FR", false], - "LDW": ["Lansdowne Airport", "Lansdowne Station", "AU", false], - "LDX": ["Saint-Laurent-du-Maroni Airport", "Saint-Laurent-du-Maroni", "GF", true], - "LDY": ["City of Derry Airport", "Derry, Derry and Strabane", "GB", true], - "LDZ": ["Londolozi Airport", "Londolozi", "ZA", false], - "LEA": ["Learmonth Airport", "Exmouth", "AU", true], - "LEB": ["Lebanon Municipal Airport", "Lebanon", "US", true], - "LEC": ["Coronel Horácio de Mattos Airport", "Lençóis", "BR", true], - "LED": ["Pulkovo Airport", "St. Petersburg", "RU", true], - "LEE": ["Leesburg International Airport", "Leesburg", "US", false], - "LEF": ["Lebakeng Airport", "Lebakeng", "LS", false], - "LEH": ["Le Havre-Octeville Airport", "Le Havre", "FR", false], - "LEI": ["Almería Airport", "Almería", "ES", true], - "LEJ": ["Leipzig/Halle Airport", "Schkeuditz", "DE", true], - "LEK": ["Tata Airport", "Labé", "GN", false], - "LEL": ["Lake Evella Airport", "", "AU", true], - "LEM": ["Lemmon Municipal Airport", "Lemmon", "US", false], - "LEN": ["León Airport", "La Virgen del Camino", "ES", true], - "LEO": ["Lekoni Airport", "Lekoni", "GA", false], - "LEP": ["Leopoldina Airport", "Leopoldina", "BR", false], - "LEQ": ["Land's End Airport", "Land's End, Cornwall", "GB", true], - "LER": ["Leinster Airport", "", "AU", true], - "LES": ["Lesobeng Airport", "Lesobeng", "LS", false], - "LET": ["Alfredo Vásquez Cobo International Airport", "Leticia", "CO", true], - "LEU": ["Pirineus - la Seu d'Urgel Airport", "La Seu d'Urgell Pyrenees and Andorra", "ES", true], - "LEV": ["Levuka Airfield", "Bureta", "FJ", true], - "LEW": ["Auburn Lewiston Municipal Airport", "Auburn/Lewiston", "US", false], - "LEX": ["Blue Grass Airport", "Lexington", "US", true], - "LEY": ["Lelystad Airport", "Lelystad", "NL", false], - "LEZ": ["La Esperanza Airport", "La Esperanza", "HN", false], - "LFB": ["Lumbo Airport", "Lumbo", "MZ", false], - "LFH": ["Lanping Fenghua General Airport", "Lanping Bai", "CN", false], - "LFI": ["Langley Air Force Base", "Hampton", "US", false], - "LFK": ["Angelina County Airport", "Lufkin", "US", false], - "LFM": ["Lamerd Airport", "Lamerd", "IR", true], - "LFN": ["Triangle North Executive Airport", "Louisburg", "US", false], - "LFO": ["Kelafo East Airport", "Kelafo", "ET", false], - "LFP": ["Lakefield Airport", "Lakefield", "AU", false], - "LFQ": ["Linfen Yaodu Airport", "Linfen (Yaodu)", "CN", true], - "LFR": ["La Fria Airport", "", "VE", false], - "LFT": ["Lafayette Regional Airport", "Lafayette", "US", true], - "LFW": ["Lomé–Tokoin International Airport", "Lomé", "TG", true], - "LGA": ["LaGuardia Airport", "New York", "US", true], - "LGB": ["Long Beach International Airport", "Long Beach", "US", true], - "LGC": ["LaGrange Callaway Airport", "LaGrange", "US", false], - "LGD": ["La Grande/Union County Airport", "La Grande", "US", false], - "LGE": ["Mulan Airport", "Lake Gregory", "AU", false], - "LGF": ["Laguna Army Airfield", "Yuma Proving Ground(Yuma)", "US", false], - "LGG": ["Liège Airport", "Grâce-Hollogne", "BE", true], - "LGH": ["Leigh Creek Airport", "", "AU", false], - "LGI": ["Deadman's Cay Airport", "Deadman's Cay", "BS", true], - "LGK": ["Langkawi International Airport", "Langkawi", "MY", true], - "LGL": ["Long Lellang Airport", "Long Datih", "MY", true], - "LGN": ["Linga Linga Airport", "Linga Linga", "PG", false], - "LGO": ["Langeoog Airport", "Langeoog", "DE", false], - "LGQ": ["Nueva Loja Airport", "Lago Agrio", "EC", false], - "LGR": ["Cochrane Airport", "Cochrane", "CL", false], - "LGS": ["Comodoro D.R. Salomón Airport", "Malargue", "AR", false], - "LGT": ["La Gaviota Airport", "", "CO", false], - "LGU": ["Logan-Cache Airport", "Logan", "US", false], - "LGW": ["London Gatwick Airport", "London", "GB", true], - "LGX": ["Lugh Ganane Airport", "Luuq", "SO", false], - "LGZ": ["Shannan Longzi Airport", "Shannan", "CN", true], - "LHA": ["Lahr Airport", "Lahr/Schwarzwald", "DE", false], - "LHE": ["Allama Iqbal International Airport", "Lahore", "PK", true], - "LHG": ["Lightning Ridge Airport", "", "AU", true], - "LHI": ["Lereh Airport", "Lereh-Papua Island", "ID", false], - "LHK": ["Guangzhou MR Air Base / Guanghua Airport", "Xiangyang (Laohekou)", "CN", false], - "LHL": ["Lachin International Airport", "Lachin", "AZ", true], - "LHN": ["Linhares Municipal Airport", "Linhares", "BR", false], - "LHR": ["London Heathrow Airport", "London", "GB", true], - "LHS": ["Las Heras Airport", "Las Heras", "AR", true], - "LHU": ["Lianshulu Lodge Airstrip", "Muneambuanas", "NA", false], - "LHV": ["William T. Piper Memorial Airport", "Lock Haven", "US", false], - "LHW": ["Lanzhou Zhongchuan International Airport", "Lanzhou (Yongdeng)", "CN", true], - "LIA": ["Liangping Airport", "Liangping", "CN", false], - "LIB": ["Limbunya Airport", "Limbunya", "AU", false], - "LIC": ["Limon Municipal Airport", "Limon", "US", false], - "LIE": ["Libenge Airport", "Libenge", "CD", false], - "LIF": ["Lifou Airport", "Lifou", "NC", true], - "LIG": ["Limoges Airport", "Limoges/Bellegarde", "FR", true], - "LIH": ["Lihue Airport", "Lihue, Kauai", "US", true], - "LII": ["Mulia Airport", "Mulia-Papua Island", "ID", false], - "LIJ": ["Lishui Airport", "Lishui (Liandu)", "CN", false], - "LIK": ["Likiep Airport", "Likiep Island", "MH", true], - "LIL": ["Lille Airport", "Lesquin", "FR", true], - "LIM": ["Jorge Chávez International Airport", "Lima", "PE", true], - "LIN": ["Milano Linate Airport", "Segrate (MI)", "IT", true], - "LIO": ["Limón International Airport", "Limón", "CR", true], - "LIP": ["Lins Airport", "Lins", "BR", false], - "LIQ": ["Lisala Airport", "Lisala", "CD", false], - "LIR": ["Daniel Oduber Quirós International Airport", "Liberia", "CR", true], - "LIS": ["Lisbon Humberto Delgado Airport", "Lisbon", "PT", true], - "LIT": ["Bill & Hillary Clinton National Airport/Adams Field", "Little Rock", "US", true], - "LIV": ["Livengood Camp Airport", "Livengood", "US", false], - "LIW": ["Loikaw Airport", "Loikaw", "MM", true], - "LIX": ["Likoma Island Airport", "Likoma Island", "MW", false], - "LIY": ["MidCoast Regional Airport at Wright Army Airfield", "Hinesville", "US", false], - "LIZ": ["Loring International Airport", "Limestone", "US", false], - "LJA": ["Lodja Airport", "Lodja", "CD", false], - "LJG": ["Lijiang Sanyi International Airport", "Lijiang", "CN", true], - "LJN": ["Texas Gulf Coast Regional Airport", "Angleton", "US", false], - "LJU": ["Ljubljana Jože Pučnik Airport", "Zgornji Brnik", "SI", true], - "LKA": ["Larantuka Gewayentana Airport", "Tiwatobi", "ID", true], - "LKB": ["Lakeba Island Airport", "Lakeba Island", "FJ", true], - "LKC": ["Lekana Airport", "Lekana", "CG", false], - "LKD": ["Lakeland Airport", "Lakeland Downs", "AU", false], - "LKE": ["Kenmore Air Harbor Seaplane Base", "Seattle", "US", true], - "LKG": ["Lokichogio Airport", "Lokichogio", "KE", false], - "LKH": ["Long Akah Airport", "Long Akah", "MY", false], - "LKI": ["Lasikin Airport", "Lubang", "ID", true], - "LKK": ["Kulik Lake Airport", "Kulik Lake", "US", false], - "LKL": ["Lakselv Airport, Banak", "Lakselv", "NO", true], - "LKM": ["Bolaang Mongondow Airport", "Lolak", "ID", true], - "LKN": ["Leknes Airport", "Leknes", "NO", true], - "LKO": ["Chaudhary Charan Singh International Airport", "Lucknow", "IN", true], - "LKP": ["Lake Placid Airport", "Lake Placid", "US", false], - "LKR": ["Las Khorey Airport", "Las Khorey", "SO", false], - "LKV": ["Lake County Airport", "Lakeview", "US", false], - "LKW": ["Lekhwair Airport", "Lekhwair", "OM", false], - "LKY": ["Lake Manyara Airport", "Lake Manyara National Park", "TZ", false], - "LKZ": ["RAF Lakenheath", "Brandon, Suffolk", "GB", false], - "LLA": ["Luleå Airport", "Luleå", "SE", true], - "LLB": ["Libo Airport", "Qiannan (Libo)", "CN", true], - "LLC": ["Cagayan North International Airport", "Lal-lo", "PH", false], - "LLE": ["Malelane Airport", "Malelane", "ZA", false], - "LLF": ["Yongzhou Lingling Airport", "Yongzhou", "CN", true], - "LLG": ["Chillagoe Airport", "Chillagoe", "AU", false], - "LLH": ["La Lima - Reginaldo Hammer Airport", "La Lima", "HN", false], - "LLI": ["Lalibela Airport", "Girany Amba", "ET", false], - "LLJ": ["Silampari Airport", "Lubuk Linggau", "ID", false], - "LLK": ["Lankaran International Airport", "Lankaran", "AZ", true], - "LLL": ["Lissadell Airport", "Lissadell Station", "AU", false], - "LLM": ["Lomlom Airport", "Lomlom", "SB", false], - "LLN": ["Kelila Airport", "Kelila", "ID", false], - "LLO": ["Bua - Palopo Lagaligo Airport", "Palopo", "ID", true], - "LLS": ["Alferez Armando Rodriguez Airport", "Las Lomitas", "AR", false], - "LLT": ["Lobito Airport", "Lobito", "AO", false], - "LLU": ["Alluitsup Paa Heliport", "Alluitsup Paa", "GL", true], - "LLV": ["Lüliang Dawu Airport", "Lüliang", "CN", true], - "LLW": ["Kamuzu International Airport", "Lumbadzi", "MW", true], - "LLX": ["Caledonia County Airport", "Lyndonville", "US", false], - "LLY": ["South Jersey Regional Airport", "Lumberton", "US", false], - "LMA": ["Minchumina Airport", "Minchumina", "US", true], - "LMB": ["Salima Airport", "Salima", "MW", false], - "LMC": ["La Macarena Airport", "La Macarena", "CO", true], - "LMD": ["Los Menucos Airport", "Los Menucos", "AR", false], - "LME": ["Le Mans-Arnage Airport", "Le Mans, Sarthe", "FR", false], - "LMH": ["Limon Airport", "Limón", "HN", false], - "LMI": ["Lumi Airport", "Lumi", "PG", false], - "LML": ["Lae Island Airport", "Lae Island", "MH", false], - "LMM": ["Valle del Fuerte International Airport", "Los Mochis", "MX", true], - "LMN": ["Limbang Airport", "Limbang", "MY", true], - "LMO": ["RAF Lossiemouth", "Lossiemouth, Moray", "GB", false], - "LMP": ["Lampedusa Airport", "Lampedusa", "IT", true], - "LMQ": ["Marsa al Brega Airport", "Marsa al Brega", "LY", false], - "LMR": ["Lime Acres Finsch Mine Airport", "Lime Acres", "ZA", false], - "LMS": ["Louisville Winston County Airport", "Louisville", "US", false], - "LMT": ["Crater Lake-Klamath Regional Airport", "Klamath Falls", "US", false], - "LMU": ["Letung Airport", "Bukit Padi", "ID", false], - "LMV": ["Madivaru Airport", "Naifaru", "MV", false], - "LMX": ["Lopez de Micay Airport", "Micay", "CO", false], - "LMY": ["Lake Murray Airport", "Lake Murray", "PG", true], - "LMZ": ["Palma Airport", "Palma", "MZ", false], - "LNA": ["Palm Beach County Park Airport", "West Palm Beach", "US", false], - "LNB": ["Lamen Bay Airport", "Lamen Bay", "VU", true], - "LNC": ["Lengbati Airport", "", "PG", false], - "LND": ["Hunt Field", "Lander", "US", false], - "LNE": ["Lonorore Airport", "Lonorore", "VU", true], - "LNF": ["Munbil Airport", "", "PG", false], - "LNG": ["Lese Airport", "Lese", "PG", false], - "LNH": ["Lake Nash Airport", "Alpurrurulam", "AU", false], - "LNI": ["Point Lonely Short Range Radar Site Airfield", "Point Lonely", "US", false], - "LNJ": ["Lincang Boshang Airport", "Lincang", "CN", true], - "LNK": ["Lincoln Airport", "Lincoln", "US", true], - "LNL": ["Longnan Chengzhou Airport", "Longnan (Cheng)", "CN", true], - "LNM": ["Langimar Airport", "", "PG", false], - "LNN": ["Lake County Executive Airport", "Willoughby", "US", false], - "LNO": ["Leonora Airport", "Leonora", "AU", true], - "LNP": ["Lonesome Pine Airport", "Wise", "US", false], - "LNR": ["Tri-County Regional Airport", "Spring Green", "US", false], - "LNS": ["Lancaster Airport", "Lancaster", "US", true], - "LNU": ["Robert Atty Bessing", "Malinau", "ID", true], - "LNV": ["Londolovit Airport", "Londolovit", "PG", true], - "LNX": ["Smolensk North Airport", "Smolensk", "RU", false], - "LNY": ["Lanai Airport", "Lanai City", "US", true], - "LNZ": ["Linz-Hörsching Airport", "Linz", "AT", true], - "LOA": ["Lorraine Airport", "", "AU", false], - "LOB": ["San Rafael Airport", "Los Andes", "CL", false], - "LOC": ["Lock Airport", "Lock", "AU", false], - "LOD": ["Longana Airport", "Longana", "VU", true], - "LOE": ["Loei Airport", "", "TH", true], - "LOH": ["Ciudad de Catamayo Airport", "La Toma (Catamayo)", "EC", true], - "LOI": ["Helmuth Baungarten Airport", "Lontras", "BR", false], - "LOK": ["Lodwar Airport", "Lodwar", "KE", false], - "LOL": ["Derby Field", "Lovelock", "US", false], - "LOM": ["Francisco Primo de Verdad y Ramos Airport", "Lagos de Moreno", "MX", false], - "LOO": ["Laghouat - Molay Ahmed Medeghri Airport", "Laghouat", "DZ", true], - "LOP": ["Lombok International Airport", "Mataram (Pujut, Lombok Tengah)", "ID", true], - "LOR": ["Lowe AHP (Fort Rucker) Heliport", "Fort Rucker Ozark", "US", false], - "LOS": ["Murtala Muhammed International Airport", "Lagos", "NG", true], - "LOT": ["Lewis University Airport", "Chicago/Romeoville", "US", false], - "LOU": ["Bowman Field", "Louisville", "US", false], - "LOV": ["Monclova International Airport", "Monclova", "MX", false], - "LOW": ["Louisa County Airport / Freeman Field", "Louisa", "US", false], - "LOY": ["Loiyangalani Airport", "Loiyangalani", "KE", false], - "LOZ": ["London-Corbin Airport/Magee Field", "London", "US", false], - "LPA": ["Gran Canaria Airport", "Gran Canaria Island", "ES", true], - "LPB": ["El Alto International Airport", "La Paz / El Alto", "BO", true], - "LPC": ["Lompoc Airport", "Lompoc", "US", false], - "LPD": ["La Pedrera Airport", "La Pedrera", "CO", true], - "LPE": ["La Primavera Airport", "La Primavera", "CO", false], - "LPF": ["Liupanshui Yuezhao Airport", "Liupanshui (Zhongshan)", "CN", true], - "LPG": ["La Plata Airport", "La Plata", "AR", false], - "LPI": ["Linköping City Airport", "Linköping", "SE", true], - "LPJ": ["Armando Schwarck Airport", "Guayabal", "VE", false], - "LPK": ["Lipetsk Airport", "Lipetsk", "RU", false], - "LPL": ["Liverpool John Lennon Airport", "Liverpool", "GB", true], - "LPM": ["Lamap Airport", "Lamap", "VU", true], - "LPN": ["Leron Plains Airport", "Leron Plains", "PG", false], - "LPO": ["La Porte Municipal Airport", "La Porte", "US", false], - "LPP": ["Lappeenranta Airport", "Lappeenranta", "FI", true], - "LPQ": ["Luang Phabang International Airport", "Luang Phabang", "LA", true], - "LPS": ["Lopez Island Airport", "Lopez", "US", true], - "LPT": ["Lampang Airport", "", "TH", true], - "LPU": ["Long Apung Airport", "Long Apung-Borneo Island", "ID", true], - "LPX": ["Liepāja International Airport", "Liepāja", "LV", false], - "LPY": ["Le Puy-Loudes Airfield", "Chaspuzac, Haute-Loire", "FR", true], - "LQK": ["Pickens County Airport", "Pickens", "US", false], - "LQM": ["Caucaya Airport", "Puerto Leguízamo", "CO", true], - "LQN": ["Qala-i-Naw Airport", "Qala-i-Naw", "AF", false], - "LRA": ["Larissa Air Base", "Larissa", "GR", false], - "LRB": ["Leribe Airport", "Leribe", "LS", false], - "LRD": ["Laredo International Airport", "Laredo", "US", true], - "LRE": ["Longreach Airport", "Longreach", "AU", true], - "LRF": ["Little Rock Air Force Base", "Jacksonville", "US", false], - "LRG": ["Loralai Airport", "Loralai", "PK", false], - "LRH": ["La Rochelle Île de Ré Airport", "La Rochelle", "FR", true], - "LRJ": ["Le Mars Municipal Airport", "Le Mars", "US", false], - "LRL": ["Niamtougou International Airport", "Niamtougou", "TG", false], - "LRM": ["Casa De Campo International Airport", "La Romana", "DO", true], - "LRQ": ["Laurie River Airport", "Laurie River", "CA", false], - "LRR": ["Lar Airport", "Lar", "IR", true], - "LRS": ["Leros Airport", "Leros Island", "GR", true], - "LRT": ["Lorient South Brittany (Bretagne Sud) Airport", "Lorient/Lann/Bihoué", "FR", false], - "LRU": ["Las Cruces International Airport", "Las Cruces", "US", true], - "LRV": ["Los Roques Airport", "Gran Roque Island", "VE", true], - "LSA": ["Losuia Airport", "Losuia", "PG", true], - "LSB": ["Lordsburg Municipal Airport", "Lordsburg", "US", false], - "LSC": ["La Florida Airport", "La Serena-Coquimbo", "CL", true], - "LSE": ["La Crosse Regional Airport", "La Crosse", "US", true], - "LSF": ["Lawson Army Air Field", "Fort Benning", "US", false], - "LSG": ["Leshan Airport", "Leshan (Wutongqiao)", "CN", true], - "LSH": ["Lashio Airport", "Lashio", "MM", true], - "LSI": ["Sumburgh Airport", "Lerwick, Shetland", "GB", true], - "LSJ": ["Long Island Airport", "Long Island", "PG", false], - "LSK": ["Lusk Municipal Airport", "Lusk", "US", false], - "LSL": ["Los Chiles Airport", "Los Chiles", "CR", false], - "LSM": ["Long Semado Airport", "Long Semado", "MY", false], - "LSN": ["Los Banos Municipal Airport", "Los Banos", "US", false], - "LSO": ["Les Sables-d'Olonne Talmont Airport", "Les Sables-d'Olonne", "FR", false], - "LSP": ["Josefa Camejo International Airport", "Paraguaná", "VE", true], - "LSQ": ["María Dolores Airport", "Los Angeles", "CL", false], - "LSR": ["Alas Leuser Airport", "Kutacane", "ID", true], - "LSS": ["Terre-de-Haut Airport", "Les Saintes", "GP", false], - "LST": ["Launceston Airport", "Launceston (Western Junction)", "AU", true], - "LSU": ["Long Sukang Airport", "Long Sukang", "MY", false], - "LSV": ["Nellis Air Force Base", "Las Vegas", "US", false], - "LSW": ["Malikus Saleh Airport", "Lhok Seumawe-Sumatra Island", "ID", true], - "LSX": ["Lhok Sukon Airport", "Lhok Sukon-Sumatra Island", "ID", false], - "LSY": ["Lismore Airport", "Lismore", "AU", true], - "LSZ": ["Lošinj Airport", "Mali Lošinj", "HR", false], - "LTA": ["Tzaneen Airport", "Tzaneen", "ZA", false], - "LTC": ["Lai Airport", "Lai", "TD", false], - "LTD": ["Ghadames Airport", "Ghadames", "LY", true], - "LTF": ["Leitre Airport", "Leitre", "PG", false], - "LTG": ["Langtang Airport", "Langtang", "NP", false], - "LTH": ["Long Thanh International Airport (Under Construction)", "Ho Chi Minh City (Long Thanh)", "VN", false], - "LTI": ["Altai Airport", "Altai", "MN", true], - "LTK": ["Latakia International Airport", "Latakia", "SY", true], - "LTL": ["Lastourville Airport", "Lastourville", "GA", false], - "LTM": ["Lethem Airport", "Lethem", "GY", true], - "LTN": ["London Luton Airport", "Luton, Luton", "GB", true], - "LTO": ["Loreto International Airport", "Loreto", "MX", true], - "LTP": ["Lyndhurst Airport", "Lyndhurst", "AU", false], - "LTQ": ["Le Touquet-Côte d'Opale Airport", "Le Touquet-Paris-Plage", "FR", false], - "LTS": ["Altus Air Force Base", "Altus", "US", false], - "LTT": ["La Môle Airport", "Saint-Tropez", "FR", true], - "LTU": ["Murod Kond Airport", "Latur", "IN", true], - "LTV": ["Lotus Vale Airport", "Lotus Vale", "AU", false], - "LTW": ["St. Mary's County Regional Airport", "California", "US", false], - "LTX": ["Cotopaxi International Airport", "Latacunga", "EC", true], - "LUA": ["Tenzing-Hillary Airport", "Lukla", "NP", true], - "LUB": ["Lumid Pau Airport", "Lumid Pau", "GY", false], - "LUC": ["Laucala Island Airport", "Laucala Island", "FJ", false], - "LUD": ["Luderitz Airport", "Luderitz", "NA", true], - "LUE": ["Lučenec Airfield", "Lučenec", "SK", false], - "LUF": ["Luke Air Force Base", "Glendale", "US", false], - "LUG": ["Lugano Airport", "Agno", "CH", true], - "LUH": ["Ludhiana Airport", "", "IN", false], - "LUI": ["Carta Airport", "La Unión", "HN", false], - "LUK": ["Cincinnati Municipal Airport Lunken Field", "Cincinnati", "US", true], - "LUL": ["Hesler Noble Field", "Laurel", "US", false], - "LUM": ["Dehong Mangshi International Airport", "Dehong (Mangshi)", "CN", true], - "LUN": ["Kenneth Kaunda International Airport", "Lusaka", "ZM", true], - "LUO": ["Luena Airport", "Luena", "AO", false], - "LUP": ["Kalaupapa Airport", "Kalaupapa", "US", true], - "LUQ": ["Brigadier Mayor D Cesar Raul Ojeda Airport", "San Luis", "AR", true], - "LUR": ["Cape Lisburne LRRS Airport", "Cape Lisburne", "US", true], - "LUT": ["New Laura Airport", "Laura Station", "AU", false], - "LUU": ["Laura Airport", "", "AU", false], - "LUV": ["Karel Sadsuitubun Airport", "Langgur", "ID", true], - "LUW": ["Syukuran Aminuddin Amir Airport", "Luwok", "ID", false], - "LUX": ["Luxembourg-Findel International Airport", "Luxembourg", "LU", true], - "LUZ": ["Lublin Airport", "Lublin", "PL", true], - "LVA": ["Laval-Entrammes Airport", "Laval, Mayenne", "FR", false], - "LVD": ["Lime Village Airport", "Lime Village", "US", false], - "LVI": ["Harry Mwanga Nkumbula International Airport", "Livingstone", "ZM", true], - "LVK": ["Livermore Municipal Airport", "Livermore", "US", false], - "LVL": ["Brunswick Municipal Airport", "Lawrenceville", "US", false], - "LVM": ["Mission Field", "Livingston", "US", false], - "LVO": ["Laverton Airport", "Laverton", "AU", true], - "LVP": ["Lavan Airport", "Lavan Airport", "IR", false], - "LVR": ["Municipal Bom Futuro Airport", "Lucas do Rio Verde", "BR", false], - "LVS": ["Las Vegas Municipal Airport", "Las Vegas", "US", false], - "LWA": ["Lebak Rural Airport", "Lebak", "PH", false], - "LWB": ["Greenbrier Valley Airport", "Lewisburg", "US", true], - "LWC": ["Lawrence Municipal Airport", "Lawrence", "US", false], - "LWE": ["Wonopito Airport", "Lewoleba", "ID", false], - "LWH": ["Lawn Hill Airport", "", "AU", false], - "LWI": ["Lowai Airport", "Lowai", "PG", false], - "LWK": ["Lerwick / Tingwall Airport", "Lerwick, Shetland Islands", "GB", true], - "LWL": ["Wells Municipal Airport/Harriet Field", "Wells", "US", false], - "LWM": ["Lawrence Municipal Airport", "Lawrence", "US", false], - "LWN": ["Shirak International Airport", "Gyumri", "AM", true], - "LWO": ["Lviv International Airport", "Lviv", "UA", false], - "LWR": ["Leeuwarden Air Base", "Leeuwarden", "NL", false], - "LWS": ["Lewiston Nez Perce County Airport", "Lewiston", "US", true], - "LWT": ["Lewistown Municipal Airport", "Lewistown", "US", false], - "LWV": ["Lawrenceville Vincennes International Airport", "Lawrenceville", "US", false], - "LWY": ["Lawas Airport", "Lawas", "MY", true], - "LXA": ["Lhasa Gonggar International Airport", "Shannan (Gonggar)", "CN", true], - "LXG": ["Luang Namtha Airport", "Luang Namtha", "LA", true], - "LXN": ["Jim Kelly Field", "Lexington", "US", false], - "LXR": ["Luxor International Airport", "Luxor", "EG", true], - "LXS": ["Limnos Airport", "Limnos Island", "GR", true], - "LXU": ["Lukulu Airport", "Lukulu", "ZM", false], - "LXV": ["Lake County Airport", "Leadville", "US", false], - "LYA": ["Luoyang Beijiao Airport", "Luoyang (Laocheng)", "CN", true], - "LYB": ["Edward Bodden Little Cayman Airfield", "Blossom Village", "KY", false], - "LYC": ["Lycksele Airport", "Lycksele", "SE", true], - "LYG": ["Lianyungang Huaguoshan International Airport", "Lianyungang", "CN", true], - "LYH": ["Lynchburg Regional Airport - Preston Glenn Field", "Lynchburg", "US", true], - "LYI": ["Linyi Qiyang Airport", "Linyi (Hedong)", "CN", true], - "LYK": ["Lunyuk Airport", "Lunyuk", "ID", false], - "LYN": ["Lyon Bron Airport", "Chassieu, Lyon", "FR", false], - "LYO": ["Lyons-Rice County Municipal Airport", "Lyons", "US", false], - "LYP": ["Faisalabad International Airport", "Faisalabad", "PK", true], - "LYR": ["Svalbard Airport, Longyear", "Longyearbyen", "NO", true], - "LYS": ["Lyon Saint-Exupéry Airport", "Colombier-Saugnieu, Rhône", "FR", true], - "LYT": ["Lady Elliot Island Airstrip", "Lady Elliot Island", "AU", false], - "LYU": ["Ely Municipal Airport", "Ely", "US", false], - "LYX": ["Lydd London Ashford Airport", "Romney Marsh, Kent", "GB", false], - "LZA": ["Luiza Airport", "Luiza", "CD", false], - "LZC": ["Lázaro Cárdenas Airport", "Lázaro Cárdenas", "MX", false], - "LZG": ["Langzhong Gucheng Airport", "Nanchong (Langzhong)", "CN", true], - "LZH": ["Liuzhou Bailian Airport / Bailian Air Base", "Liuzhou (Liujiang)", "CN", true], - "LZI": ["Luozi Airport", "Luozi", "CD", false], - "LZM": ["Luzamba Airport", "Luzamba", "AO", false], - "LZN": ["Matsu Nangan Airport", "Matsu (Nangan)", "TW", true], - "LZO": ["Luzhou Yunlong Airport", "Luzhou (Yunlong)", "CN", true], - "LZR": ["Lizard Island Airport", "", "AU", false], - "LZU": ["Gwinnett County Briscoe Field", "Lawrenceville", "US", false], - "LZY": ["Nyingchi Mainling Airport", "Nyingchi (Mainling)", "CN", true], - "MAA": ["Chennai International Airport", "Chennai", "IN", true], - "MAB": ["João Correa da Rocha Airport", "Marabá", "BR", true], - "MAC": ["Macon Downtown Airport", "Macon", "US", false], - "MAD": ["Adolfo Suárez Madrid–Barajas Airport", "Madrid", "ES", true], - "MAE": ["Madera Municipal Airport", "Madera", "US", false], - "MAF": ["Midland International Air and Space Port", "Midland", "US", true], - "MAG": ["Madang Airport", "Madang", "PG", true], - "MAH": ["Menorca Airport", "Mahón (Maó)", "ES", true], - "MAJ": ["Marshall Islands International Airport", "Majuro Atoll", "MH", true], - "MAK": ["Malakal International Airport", "Malakal", "SS", true], - "MAL": ["Mangole Airport, Falabisahaya", "Mangole Island", "ID", false], - "MAM": ["General Servando Canales International Airport", "Matamoros", "MX", true], - "MAN": ["Manchester Airport", "Manchester, Greater Manchester", "GB", true], - "MAO": ["Eduardo Gomes International Airport", "Manaus", "BR", true], - "MAP": ["Mamai Airport", "Mamai", "PG", false], - "MAQ": ["Mae Sot Airport", "", "TH", true], - "MAR": ["La Chinita International Airport", "Maracaibo", "VE", true], - "MAS": ["Momote Airport", "Manus Island", "PG", true], - "MAT": ["Tshimpi Airport", "Matadi", "CD", false], - "MAU": ["Maupiti Airport", "", "PF", true], - "MAV": ["Maloelap Island Airport", "Maloelap Island", "MH", false], - "MAW": ["Malden Regional Airport", "Malden", "US", false], - "MAX": ["Ouro Sogui Airport", "Ouro Sogui", "SN", false], - "MAY": ["Clarence A. Bain Airport", "Mangrove Cay", "BS", false], - "MAZ": ["Eugenio Maria De Hostos Airport", "Mayaguez", "PR", true], - "MBA": ["Moi International Airport", "Mombasa", "KE", true], - "MBB": ["Marble Bar Airport", "", "AU", false], - "MBC": ["M'Bigou Airport", "M'Bigou", "GA", false], - "MBD": ["Mmabatho International Airport", "Mafeking", "ZA", true], - "MBE": ["Monbetsu Airport", "Monbetsu", "JP", true], - "MBF": ["Porepunkah Airport", "", "AU", false], - "MBG": ["Mobridge Municipal Airport", "Mobridge", "US", false], - "MBH": ["Maryborough Airport", "Maryborough", "AU", false], - "MBI": ["Songwe Airport", "Mbeya", "TZ", true], - "MBJ": ["Sangster International Airport", "Montego Bay", "JM", true], - "MBK": ["Orlando Villas Boas Regional Airport", "Matupá", "BR", false], - "MBL": ["Manistee County Blacker Airport", "Manistee", "US", true], - "MBM": ["Mkambati Airport", "Mkambati", "ZA", false], - "MBN": ["Mount Barnett Airport", "Wunaamin Miliwundi Ranges", "AU", false], - "MBO": ["Mamburao Airport", "Mamburao", "PH", false], - "MBP": ["Moyobamba Airport", "Moyobamba", "PE", false], - "MBQ": ["Mbarara Airport", "Mbarara", "UG", false], - "MBS": ["MBS International Airport", "Freeland", "US", true], - "MBT": ["Moises R. Espinosa Airport", "Masbate", "PH", true], - "MBU": ["Babanakira Airport", "Mbambanakira", "SB", false], - "MBV": ["Masa Airport", "Masa", "PG", false], - "MBW": ["Melbourne Moorabbin Airport", "Melbourne", "AU", true], - "MBX": ["Maribor Edvard Rusjan Airport", "Maribor", "SI", true], - "MBY": ["Omar N Bradley Airport", "Moberly", "US", false], - "MBZ": ["Maués Airport", "Maués", "BR", false], - "MCA": ["Macenta Airport", "Macenta", "GN", false], - "MCB": ["McComb-Pike County Airport / John E Lewis Field", "McComb", "US", false], - "MCC": ["McClellan Airfield", "Sacramento", "US", false], - "MCD": ["Mackinac Island Airport", "Mackinac Island", "US", false], - "MCE": ["Merced Regional Macready Field", "Merced", "US", true], - "MCF": ["MacDill Air Force Base", "Tampa", "US", false], - "MCG": ["McGrath Airport", "McGrath", "US", true], - "MCI": ["Kansas City International Airport", "Kansas City", "US", true], - "MCJ": ["Jorge Isaac Airport", "La Mina-Maicao", "CO", false], - "MCK": ["McCook Ben Nelson Regional Airport", "McCook", "US", true], - "MCL": ["Denali National Park Airport", "Denali Park", "US", false], - "MCM": ["Monaco Heliport", "Fontvieille", "MC", true], - "MCN": ["Middle Georgia Regional Airport", "Macon", "US", true], - "MCO": ["Orlando International Airport", "Orlando", "US", true], - "MCP": ["Macapá - Alberto Alcolumbre International Airport", "Macapá", "BR", true], - "MCS": ["Monte Caseros Airport", "Monte Caseros", "AR", false], - "MCT": ["Muscat International Airport", "Muscat/Seeb", "OM", true], - "MCU": ["Montluçon-Guéret Airport", "Lépaud, Creuse", "FR", false], - "MCV": ["McArthur River Mine Airport", "McArthur River Mine", "AU", true], - "MCW": ["Mason City Municipal Airport", "Mason City", "US", true], - "MCX": ["Makhachkala Uytash International Airport", "Makhachkala", "RU", true], - "MCY": ["Sunshine Coast Airport", "Maroochydore", "AU", true], - "MCZ": ["Zumbi dos Palmares International Airport", "Maceió", "BR", true], - "MDA": ["Martindale Army Heliport", "San Antonio", "US", false], - "MDB": ["Melinda Airport", "Hope Creek", "BZ", false], - "MDC": ["Sam Ratulangi International Airport", "Manado", "ID", true], - "MDD": ["Midland Airpark", "Midland", "US", false], - "MDE": ["Jose Maria Córdova International Airport", "Medellín", "CO", true], - "MDF": ["Taylor County Airport", "Medford", "US", false], - "MDG": ["Mudanjiang Hailang International Airport", "Mudanjiang", "CN", true], - "MDH": ["Southern Illinois Airport", "Murphysboro", "US", false], - "MDI": ["Makurdi Airport", "Makurdi", "NG", true], - "MDJ": ["Madras Municipal Airport", "Madras", "US", false], - "MDK": ["Mbandaka Airport", "Mbandaka", "CD", true], - "MDL": ["Mandalay International Airport", "Mandalay", "MM", true], - "MDM": ["Munduku Airport", "Munduku", "PG", false], - "MDN": ["Madison Regional Airport", "Madison", "US", false], - "MDO": ["Middleton Island Airport", "Middleton Island", "US", false], - "MDP": ["Mindiptana Airport", "Mindiptana-Papua Island", "ID", false], - "MDQ": ["Ástor Piazzola International Airport", "Mar del Plata", "AR", true], - "MDR": ["Musiara Airstrip", "Mara Rianta", "KE", false], - "MDS": ["Middle Caicos Airport", "Middle Caicos", "TC", false], - "MDT": ["Harrisburg International Airport", "Harrisburg", "US", true], - "MDU": ["Mendi Airport", "Mendi", "PG", true], - "MDV": ["Médouneu Airport", "Médouneu, Gabon", "GQ", false], - "MDW": ["Chicago Midway International Airport", "Chicago", "US", true], - "MDX": ["Mercedes Airport", "Mercedes", "AR", false], - "MDY": ["Henderson Field", "Sand Island", "UM", false], - "MDZ": ["Governor Francisco Gabrielli International Airport", "Mendoza", "AR", true], - "MEA": ["Macaé Benedito Lacerda Airport", "Macaé", "BR", false], - "MEB": ["Melbourne Essendon Airport", "Essendon Fields", "AU", true], - "MEC": ["Eloy Alfaro International Airport", "Manta", "EC", true], - "MED": ["Prince Mohammad Bin Abdulaziz Airport", "Medina", "SA", true], - "MEE": ["Maré Airport", "Maré", "NC", true], - "MEF": ["Melfi Airport", "Melfi", "TD", false], - "MEG": ["Malanje Airport", "Malanje", "AO", true], - "MEH": ["Mehamn Airport", "Mehamn", "NO", true], - "MEI": ["Key Field / Meridian Regional Airport", "Meridian", "US", true], - "MEJ": ["Port Meadville Airport", "Meadville", "US", false], - "MEK": ["Bassatine Airport", "Meknes", "MA", false], - "MEL": ["Melbourne Airport", "Melbourne", "AU", true], - "MEM": ["Frederick W. Smith International Airport", "Memphis", "US", true], - "MEN": ["Mende-Brenoux Airfield", "Mende/Brénoux", "FR", false], - "MEO": ["Dare County Regional Airport", "Manteo", "US", false], - "MEP": ["Mersing Airport", "Mersing", "MY", false], - "MEQ": ["Cut Nyak Dhien Airport", "Kuala Pesisir", "ID", true], - "MER": ["Castle Airport", "Merced", "US", false], - "MES": ["Soewondo Air Force Base", "Medan", "ID", false], - "MET": ["Moreton Airport", "Moreton", "AU", false], - "MEU": ["Monte Dourado - Serra do Areão Airport", "Almeirim", "BR", false], - "MEV": ["Minden-Tahoe Airport", "Minden", "US", false], - "MEW": ["Mweka Airport", "Mweka", "CD", false], - "MEX": ["Mexico City Benito Juárez International Airport", "Mexico City", "MX", true], - "MEY": ["Meghauli Airport", "Meghauli", "NP", false], - "MEZ": ["Musina(Messina) Airport", "Musina", "ZA", false], - "MFA": ["Mafia Airport", "Kilindoni", "TZ", true], - "MFB": ["Monfort Airport", "Monfort", "CO", false], - "MFC": ["Mafeteng Airport", "Mafeteng", "LS", false], - "MFD": ["Mansfield Lahm Regional Airport", "Mansfield", "US", false], - "MFE": ["McAllen Miller International Airport", "McAllen", "US", true], - "MFF": ["Moanda Airport", "Moanda", "GA", false], - "MFG": ["Muzaffarabad Airport", "Muzaffarabad", "PK", true], - "MFH": ["Mesquite Airport", "Mesquite", "US", false], - "MFI": ["Marshfield Municipal Airport", "Marshfield", "US", false], - "MFJ": ["Moala Airport", "Moala", "FJ", true], - "MFK": ["Matsu Beigan Airport", "Matsu (Beigan)", "TW", true], - "MFL": ["Mount Full Stop Airport", "Wando Vale", "AU", false], - "MFM": ["Macau International Airport", "Nossa Senhora do Carmo", "MO", true], - "MFN": ["Milford Sound Airport", "", "NZ", false], - "MFO": ["Manguna Airport", "Manguna", "PG", false], - "MFP": ["Manners Creek Airport", "", "AU", false], - "MFQ": ["Maradi Airport", "Maradi", "NE", false], - "MFR": ["Rogue Valley International-Medford Airport", "Medford", "US", true], - "MFS": ["Miraflores Airport", "Miraflores", "CO", false], - "MFU": ["Mfuwe International Airport", "Mfuwe", "ZM", true], - "MFV": ["Accomack County Airport", "Melfa", "US", false], - "MFX": ["Méribel Altiport", "Les Allues", "FR", false], - "MFZ": ["Meselia Airport", "Demgulu", "PG", false], - "MGA": ["Augusto C. Sandino (Managua) International Airport", "Managua", "NI", true], - "MGB": ["Mount Gambier Airport", "Mount Gambier", "AU", true], - "MGC": ["Michigan City Municipal Airport", "Michigan City", "US", true], - "MGD": ["Magdalena Airport", "Magdalena", "BO", false], - "MGE": ["Dobbins Air Reserve Base", "Marietta", "US", false], - "MGF": ["Regional de Maringá - Sílvio Name Júnior Airport", "Maringá", "BR", true], - "MGH": ["Margate Airport", "Margate", "ZA", true], - "MGI": ["National Wildlife Refuge Airport", "Matagorda Island", "US", false], - "MGJ": ["Orange County Airport", "Montgomery", "US", false], - "MGK": ["Mong Tong Airport", "Mong Tong", "MM", false], - "MGL": ["Mönchengladbach Airport", "Mönchengladbach", "DE", false], - "MGM": ["Montgomery Regional (Dannelly Field) Airport", "Montgomery", "US", true], - "MGN": ["Baracoa Airport", "Magangué", "CO", false], - "MGP": ["Manga Airport", "Manga Mission", "PG", false], - "MGQ": ["Aden Adde International Airport", "Mogadishu", "SO", true], - "MGR": ["Moultrie Municipal Airport", "Moultrie", "US", false], - "MGS": ["Mangaia Island Airport", "Mangaia Island", "CK", false], - "MGT": ["Milingimbi Airport", "Milingimbi Island", "AU", true], - "MGU": ["Manaung Airport", "Manaung", "MM", false], - "MGV": ["Margaret River (Station) Airport", "", "AU", false], - "MGW": ["Morgantown Municipal Airport Walter L. (Bill) Hart Field", "Morgantown", "US", true], - "MGX": ["Moabi Airport", "Moabi", "GA", false], - "MGY": ["Dayton-Wright Brothers Airport", "Dayton", "US", false], - "MGZ": ["Myeik Airport", "Mkeik", "MM", true], - "MHA": ["Mahdia Airport", "Mahdia", "GY", false], - "MHB": ["Mechanics Bay Heliport", "Auckland City", "NZ", false], - "MHC": ["Mocopulli Airport", "Dalcahue", "CL", true], - "MHD": ["Mashhad International Airport", "Mashhad", "IR", true], - "MHE": ["Mitchell Municipal Airport", "Mitchell", "US", false], - "MHF": ["Morichal Airport", "Morichal", "CO", false], - "MHG": ["Mannheim-City Airport", "Mannheim", "DE", true], - "MHH": ["Leonard M. Thompson International Airport", "Marsh Harbour", "BS", true], - "MHI": ["Moucha Airport", "Moucha Island", "DJ", false], - "MHK": ["Manhattan Regional Airport", "Manhattan", "US", true], - "MHL": ["Marshall Memorial Municipal Airport", "Marshall", "US", false], - "MHM": ["Manaoba Airport", "Manaoba", "SB", true], - "MHN": ["Hooker County Airport", "Mullen", "US", false], - "MHO": ["Mount House Airport", "Wunaamin Miliwundi Ranges", "AU", false], - "MHQ": ["Mariehamn Airport", "Mariehamn", "FI", true], - "MHR": ["Sacramento Mather Airport", "Sacramento", "US", false], - "MHS": ["Dunsmuir Muni-Mott Airport", "Dunsmuir", "US", false], - "MHT": ["Manchester-Boston Regional Airport", "Manchester", "US", true], - "MHU": ["Mount Hotham Airport", "Mount Hotham", "AU", true], - "MHV": ["Mojave Air & Space Port", "Mojave", "US", false], - "MHW": ["Monteagudo Airport", "El Bañado", "BO", false], - "MHX": ["Manihiki Island Airport", "Manihiki Island", "CK", true], - "MHY": ["Morehead Airport", "Morehead", "PG", false], - "MHZ": ["RAF Mildenhall", "Bury Saint Edmunds, Suffolk", "GB", false], - "MIA": ["Miami International Airport", "Miami", "US", true], - "MIB": ["Minot Air Force Base", "Minot", "US", false], - "MIC": ["Crystal Airport", "Minneapolis", "US", false], - "MID": ["Manuel Crescencio Rejón International Airport", "Mérida", "MX", true], - "MIE": ["Delaware County Johnson Field", "Muncie", "US", false], - "MIF": ["Roy Hurd Memorial Airport", "Monahans", "US", false], - "MIG": ["Mianyang Nanjiao Airport", "Mianyang (Fucheng)", "CN", true], - "MIH": ["Mitchell Plateau Airport", "Mitchell Plateau", "AU", false], - "MII": ["Frank Miloye Milenkowichi–Marília State Airport", "Marília", "BR", true], - "MIJ": ["Mili Island Airport", "Mili Island", "MH", true], - "MIK": ["Mikkeli Airport", "Mikkeli", "FI", false], - "MIM": ["Merimbula Airport", "Merimbula", "AU", true], - "MIN": ["Minnipa Airport", "", "AU", false], - "MIO": ["Miami Municipal Airport", "Miami", "US", false], - "MIP": ["Mitzpe Ramon Airfield", "Mitzpe Ramon", "IL", false], - "MIQ": ["Millard Airport", "Omaha", "US", false], - "MIR": ["Monastir Habib Bourguiba International Airport", "Monastir", "TN", true], - "MIS": ["Misima Island Airport", "Misima Island", "PG", true], - "MIT": ["Shafter Airport - Minter Field", "Shafter", "US", false], - "MIU": ["Maiduguri International Airport", "Maiduguri", "NG", true], - "MIV": ["Millville Municipal Airport", "Millville", "US", false], - "MIW": ["Marshalltown Municipal Airport", "Marshalltown", "US", false], - "MIX": ["Miriti Parana Airport", "Mirití-Paraná", "CO", false], - "MIY": ["Mittebah Airport", "", "AU", false], - "MIZ": ["Mainoru Airstrip", "Mainoru", "AU", false], - "MJA": ["Manja Airport", "Manja", "MG", false], - "MJB": ["Mejit Atoll Airport", "Mejit Atoll", "MH", false], - "MJC": ["Man Airport", "", "CI", false], - "MJD": ["Moenjodaro Airport", "Moenjodaro", "PK", false], - "MJE": ["Majkin Airport", "Majkin", "MH", true], - "MJF": ["Mosjøen Airport, Kjærstad", "Mosjøen", "NO", true], - "MJG": ["Mayajigua Airport", "Mayajigua", "CU", false], - "MJI": ["Mitiga International Airport", "Tripoli", "LY", true], - "MJJ": ["Moki Airport", "Moki", "PG", false], - "MJK": ["Shark Bay Airport", "Denham", "AU", true], - "MJL": ["Mouilla Ville Airport", "Mouila", "GA", false], - "MJM": ["Mbuji Mayi Airport", "Mbuji Mayi", "CD", true], - "MJN": ["Amborovy Airport", "Mahajanga", "MG", true], - "MJO": ["Mount Etjo Airport", "Mount Etjo Safari Lodge", "NA", false], - "MJP": ["Manjimup Airport", "", "AU", false], - "MJQ": ["Jackson Municipal Airport", "Jackson", "US", false], - "MJR": ["Miramar Airport", "Miramar", "AR", false], - "MJS": ["Maganja da Costa Airport", "Maganja", "MZ", false], - "MJT": ["Mytilene International Airport", "Mytilene", "GR", true], - "MJU": ["Tampa Padang Airport", "Mamuju", "ID", false], - "MJW": ["Mahenye Airport", "Gonarezhou National Park", "ZW", false], - "MJX": ["Ocean County Airport", "Toms River", "US", false], - "MJY": ["Motygino Airport", "Motygino", "RU", true], - "MJZ": ["Mirny Airport", "Mirny", "RU", true], - "MKB": ["Mekambo Airport", "Mekambo", "GA", false], - "MKC": ["Charles B. Wheeler Downtown Airport", "Kansas City", "US", false], - "MKE": ["General Mitchell International Airport", "Milwaukee", "US", true], - "MKG": ["Muskegon County Airport", "Muskegon", "US", true], - "MKH": ["Mokhotlong Airport", "Mokhotlong", "LS", false], - "MKI": ["M'Boki Airport", "Mboki", "CF", false], - "MKJ": ["Makoua Airport", "Makoua", "CG", false], - "MKK": ["Molokai Airport", "Kaunakakai", "US", true], - "MKL": ["McKellar-Sipes Regional Airport", "Jackson", "US", true], - "MKM": ["Mukah Airport", "Mukah", "MY", true], - "MKN": ["Malekolon Airport", "Babase Island", "PG", false], - "MKO": ["Muskogee-Davis Regional Airport", "Muskogee", "US", false], - "MKP": ["Makemo Airport", "Makemo", "PF", true], - "MKQ": ["Mopah International Airport", "Merauke", "ID", true], - "MKR": ["Meekatharra Airport", "", "AU", true], - "MKS": ["Mekane Selam Airport", "Mekane Selam", "ET", false], - "MKT": ["Mankato Regional Airport", "Mankato", "US", false], - "MKU": ["Makokou Airport", "Makokou", "GA", true], - "MKW": ["Rendani Airport", "Manokwari", "ID", true], - "MKY": ["Mackay Airport", "Mackay", "AU", true], - "MKZ": ["Malacca International Airport", "Malacca", "MY", true], - "MLA": ["Malta International Airport", "Valletta", "MT", true], - "MLB": ["Melbourne Orlando International Airport", "Melbourne", "US", true], - "MLC": ["Mc Alester Regional Airport", "Mc Alester", "US", false], - "MLD": ["Malad City Airport", "Malad City", "US", false], - "MLE": ["Velana International Airport", "Malé", "MV", true], - "MLF": ["Milford Municipal-Ben and Judy Briscoe Field", "Milford", "US", false], - "MLG": ["Abdul Rachman Saleh Airport", "Malang", "ID", true], - "MLI": ["Quad City International Airport", "Moline", "US", true], - "MLJ": ["Baldwin County Regional Airport", "Milledgeville", "US", false], - "MLK": ["Malta Airport", "Malta", "US", false], - "MLL": ["Marshall Don Hunter Sr Airport", "Marshall", "US", true], - "MLM": ["General Francisco J. Mujica International Airport", "Morelia", "MX", true], - "MLN": ["Melilla Airport", "Melilla", "ES", true], - "MLO": ["Milos Airport", "Milos Island", "GR", true], - "MLP": ["Malabang Airport", "Malabang", "PH", false], - "MLQ": ["Malalaua Airport", "Malalaua", "PG", false], - "MLR": ["Millicent Airport", "", "AU", false], - "MLS": ["Miles City Airport - Frank Wiley Field", "Miles City", "US", false], - "MLT": ["Millinocket Municipal Airport", "Millinocket", "US", false], - "MLU": ["Monroe Regional Airport", "Monroe", "US", true], - "MLV": ["Merluna Airport", "Merluna", "AU", false], - "MLW": ["Spriggs Payne Airport", "Monrovia", "LR", true], - "MLX": ["Malatya Erhaç Airport", "Malatya", "TR", true], - "MLY": ["Manley Hot Springs Airport", "Manley Hot Springs", "US", true], - "MLZ": ["Cerro Largo International Airport", "Melo", "UY", false], - "MMB": ["Memanbetsu Airport", "Ōzora", "JP", true], - "MMC": ["Ciudad Mante Los Huastecas National Airport", "Ciudad Mante", "MX", false], - "MMD": ["Minamidaito Airport", "Minamidaito", "JP", true], - "MME": ["Teesside International Airport", "Darlington, Durham", "GB", true], - "MMF": ["Mamfe Airport", "Mamfe", "CM", false], - "MMG": ["Mount Magnet Airport", "", "AU", true], - "MMH": ["Mammoth Yosemite Airport", "Mammoth Lakes", "US", true], - "MMI": ["McMinn County Airport", "Athens", "US", false], - "MMJ": ["Shinshu-Matsumoto Airport", "Matsumoto", "JP", true], - "MMK": ["Emperor Nicholas II Murmansk Airport", "Murmansk", "RU", true], - "MML": ["Southwest Minnesota Regional Airport - Marshall/Ryan Field", "Marshall", "US", false], - "MMM": ["Middlemount Airport", "", "AU", false], - "MMN": ["Minute Man Air Field", "Stow", "US", false], - "MMO": ["Maio Airport", "Vila do Maio", "CV", true], - "MMP": ["San Bernardo Airport", "Santa Cruz de Mompóx", "CO", false], - "MMQ": ["Mbala Airport", "Mbala", "ZM", false], - "MMS": ["Selfs Airport", "Marks", "US", false], - "MMT": ["Mc Entire Joint National Guard Base", "Eastover", "US", false], - "MMU": ["Morristown Municipal Airport", "Morristown", "US", false], - "MMV": ["Mal Airport", "Mal Island", "PG", false], - "MMX": ["Malmö Sturup Airport", "Malmö", "SE", true], - "MMY": ["Miyako Airport", "Miyakojima", "JP", true], - "MMZ": ["Maymana Zahiraddin Faryabi Airport", "Maymana", "AF", false], - "MNA": ["Melangguane Airport", "Karakelong Island", "ID", false], - "MNB": ["Muanda Airport", "Muanda", "CD", false], - "MNC": ["Nacala International Airport", "Nacala", "MZ", true], - "MND": ["Medina Airport", "Medina", "CO", false], - "MNE": ["Mungeranie Airport", "Mungeranie", "AU", false], - "MNF": ["Mana Island Airport", "Mana Island", "FJ", true], - "MNG": ["Maningrida Airport", "Maningrida", "AU", true], - "MNH": ["Mussanah Airport", "Al Masna'ah", "OM", false], - "MNI": ["John A. Osborne Airport", "Gerald's Park", "MS", true], - "MNJ": ["Mananjary Airport", "Mananjary", "MG", true], - "MNK": ["Maiana Airport", "Maiana", "KI", false], - "MNL": ["Ninoy Aquino International Airport", "Manila (Pasay)", "PH", true], - "MNM": ["Menominee–Marinette Twin County Airport", "Menominee", "US", false], - "MNN": ["Marion Municipal Airport", "Marion", "US", false], - "MNO": ["Manono Airport", "Manono", "CD", false], - "MNP": ["Maron Island Airport", "Hermit Islands", "PG", false], - "MNQ": ["Monto Airport", "", "AU", false], - "MNR": ["Mongu Airport", "Mongu", "ZM", false], - "MNS": ["Mansa Airport", "Mansa", "ZM", false], - "MNT": ["Minto Al Wright Airport", "Minto", "US", true], - "MNU": ["Mawlamyine Airport", "Mawlamyine", "MM", true], - "MNV": ["Mount Valley Airport", "", "AU", false], - "MNW": ["Macdonald Downs Airport", "", "AU", false], - "MNX": ["Manicoré Airport", "Manicoré", "BR", true], - "MNY": ["Mono Airport", "Stirling Island", "SB", true], - "MNZ": ["Washington Manassas Harry P. Davis Field", "Manassas", "US", false], - "MOA": ["Orestes Acosta Airport", "Moa", "CU", false], - "MOB": ["Mobile Regional Airport", "Mobile", "US", true], - "MOC": ["Mário Ribeiro Airport", "Montes Claros", "BR", true], - "MOD": ["Modesto City Co-Harry Sham Field", "Modesto", "US", false], - "MOE": ["Momeik Airport", "", "MM", false], - "MOF": ["Frans Xavier Seda Airport", "Waioti", "ID", true], - "MOG": ["Mong Hsat Airport", "Mong Hsat", "MM", true], - "MOH": ["Maleo Airport", "Morowali", "ID", true], - "MOI": ["Mitiaro Island Airport", "Mitiaro Island", "CK", true], - "MOJ": ["Moengo Airstrip", "Moengo", "SR", true], - "MOK": ["Muynak Airport", "Muynak", "UZ", false], - "MOL": ["Molde Airport, Årø", "Årø", "NO", true], - "MOM": ["Letfotar Airport", "Moudjeria", "MR", false], - "MON": ["Mount Cook Airport", "", "NZ", false], - "MOO": ["Moomba Airport", "Moomba", "AU", false], - "MOP": ["Mount Pleasant Municipal Airport", "Mount Pleasant", "US", false], - "MOQ": ["Morondava Airport", "Morondava", "MG", true], - "MOR": ["Morristown Regional Airport", "Morristown", "US", false], - "MOS": ["Moses Point Airport", "Elim", "US", false], - "MOT": ["Minot International Airport", "Minot", "US", true], - "MOU": ["Mountain Village Airport", "Mountain Village", "US", true], - "MOV": ["Moranbah Airport", "Moranbah", "AU", true], - "MOX": ["Morris Municipal Airport Charlie Schmidt Field", "Morris", "US", false], - "MOY": ["Monterrey Airport", "Monterrey", "CO", false], - "MOZ": ["Moorea Temae Airport", "Moorea-Maiao", "PF", true], - "MPA": ["Katima Mulilo Airport", "Mpacha", "NA", true], - "MPB": ["Miami Seaplane Base", "Miami", "US", false], - "MPC": ["Muko Muko Airport", "Muko Muko", "ID", true], - "MPD": ["Sindhri Tharparkar Airport", "Sindhri", "PK", false], - "MPF": ["Mapoda Airport", "Mapoda", "PG", false], - "MPG": ["Makini Airport", "Makini", "PG", false], - "MPH": ["Godofredo P. Ramos Airport", "Caticlan", "PH", true], - "MPI": ["Mamitupu Airport", "Mamitupu", "PA", false], - "MPJ": ["Petit Jean Park Airport", "Morrilton", "US", false], - "MPL": ["Montpellier-Méditerranée Airport", "Montpellier/Méditerranée", "FR", true], - "MPM": ["Maputo Airport", "Maputo", "MZ", true], - "MPN": ["Mount Pleasant Airport / RAF Mount Pleasant", "Mount Pleasant", "FK", true], - "MPO": ["Pocono Mountains Municipal Airport", "Mount Pocono", "US", false], - "MPP": ["Mulatupo Airport", "Mulatupo", "PA", false], - "MPR": ["Mc Pherson Airport", "Mc Pherson", "US", false], - "MPS": ["Mount Pleasant Regional Airport", "Mount Pleasant", "US", false], - "MPT": ["Maliana Airport", "Maliana", "TL", false], - "MPU": ["Mapua(Mabua) Airport", "Tatau Island", "PG", false], - "MPV": ["Edward F Knapp State Airport", "Barre/Montpelier", "US", false], - "MPW": ["Mariupol International Airport", "Mariupol", "UA", false], - "MPX": ["Miyanmin Airport", "Miyanmin", "PG", false], - "MPY": ["Maripasoula Airport", "Maripasoula", "GF", true], - "MPZ": ["Mount Pleasant Municipal Airport", "Mount Pleasant", "US", false], - "MQA": ["Mandora Airport", "Eighty Mile Beach", "AU", false], - "MQB": ["Macomb Municipal Airport", "Macomb", "US", false], - "MQC": ["Miquelon Airport", "Miquelon", "PM", true], - "MQD": ["Maquinchao Airport", "Maquinchao", "AR", false], - "MQE": ["Marqua Airport", "Marqua", "AU", false], - "MQF": ["Magnitogorsk International Airport", "Magnitogorsk", "RU", true], - "MQG": ["Midgard Airport", "Midgard", "NA", false], - "MQH": ["Minaçu Airport", "Minaçu", "BR", false], - "MQJ": ["Moma Airport", "Khonuu", "RU", true], - "MQK": ["San Matías Airport", "San Matías", "BO", false], - "MQL": ["Mildura Airport", "Mildura", "AU", true], - "MQM": ["Mardin Airport", "Mardin", "TR", true], - "MQN": ["Mo i Rana Airport, Røssvoll", "Mo i Rana", "NO", true], - "MQO": ["Malam Airport", "Malam", "PG", false], - "MQP": ["Kruger Mpumalanga International Airport", "Mbombela", "ZA", true], - "MQQ": ["Moundou Airport", "Moundou", "TD", false], - "MQR": ["Mosquera Airport", "Mosquera", "CO", false], - "MQS": ["Mustique Airport", "Lovell", "VC", true], - "MQT": ["Marquette Sawyer Regional Airport", "Gwinn", "US", true], - "MQU": ["Mariquita Airport", "Mariquita", "CO", false], - "MQV": ["Mostaganem Airport", "Sayada", "DZ", false], - "MQW": ["Telfair Wheeler Airport", "Mc Rae", "US", false], - "MQX": ["Mekele Alula Aba Nega Airport", "Mekele", "ET", true], - "MQY": ["Smyrna Airport", "Smyrna", "US", false], - "MQZ": ["Margaret River Airport", "", "AU", false], - "MRA": ["Misrata International Airport", "Misrata", "LY", true], - "MRB": ["Eastern WV Regional Airport/Shepherd Field", "Martinsburg", "US", false], - "MRC": ["Maury County Airport", "Columbia/Mount Pleasant", "US", false], - "MRD": ["Alberto Carnevalli Airport", "Mérida", "VE", false], - "MRE": ["Mara Serena Lodge Airstrip", "Serena", "KE", true], - "MRF": ["Marfa Municipal Airport", "Marfa", "US", false], - "MRG": ["Mareeba Airport", "Mareeba", "AU", false], - "MRH": ["May River Airstrip", "May River", "PG", false], - "MRI": ["Merrill Field", "Anchorage", "US", true], - "MRK": ["Marco Island Executive Airport", "Marco Island", "US", false], - "MRM": ["Manari Airport", "Manari", "PG", false], - "MRN": ["Foothills Regional Airport", "Morganton", "US", false], - "MRO": ["Hood Airport", "Masterton", "NZ", false], - "MRP": ["Marla Airport", "Marla", "AU", false], - "MRQ": ["Marinduque Airport", "Gasan", "PH", false], - "MRR": ["Jose Maria Velasco Ibarra Airport", "Macará", "EC", false], - "MRS": ["Marseille Provence Airport", "Marignane, Bouches-du-Rhône", "FR", true], - "MRT": ["Moroak Airport", "Moroak", "AU", false], - "MRU": ["Sir Seewoosagur Ramgoolam International Airport", "Plaine Magnien", "MU", true], - "MRV": ["Mineralnye Vody Airport", "Mineralnyye Vody", "RU", true], - "MRW": ["Lolland Falster Maribo Airport", "Rødby", "DK", false], - "MRX": ["Mahshahr Airport", "Mahshahr", "IR", true], - "MRY": ["Monterey Regional Airport", "Monterey", "US", true], - "MRZ": ["Moree Airport", "Moree", "AU", true], - "MSA": ["Muskrat Dam Airport", "Muskrat Dam", "CA", true], - "MSB": ["Marigot Seaplane Base", "Saint Martin", "MF", false], - "MSC": ["Falcon Field", "Mesa", "US", false], - "MSF": ["Mount Swan Airport", "", "AU", false], - "MSG": ["Matsaile Airport", "Matsaile", "LS", false], - "MSH": ["RAFO Masirah", "Masirah", "OM", false], - "MSI": ["Masalembo Airport", "Masalembo Island", "ID", false], - "MSJ": ["Misawa Airport / Misawa Air Base", "Misawa", "JP", true], - "MSK": ["Morelia Airport", "Puerto Gaitán", "CO", false], - "MSL": ["Northwest Alabama Regional Airport", "Muscle Shoals", "US", true], - "MSM": ["Masi Manimba Airport", "Masi Manimba", "CD", false], - "MSN": ["Dane County Regional Truax Field", "Madison", "US", true], - "MSO": ["Missoula Montana Airport", "Missoula", "US", true], - "MSP": ["Minneapolis–Saint Paul International Airport / Wold–Chamberlain Field", "Minneapolis", "US", true], - "MSQ": ["Minsk National Airport", "Minsk", "BY", true], - "MSR": ["Muş Airport", "Muş", "TR", true], - "MSS": ["Massena International Airport Richards Field", "Massena", "US", true], - "MST": ["Maastricht Aachen Airport", "Maastricht", "NL", true], - "MSU": ["Moshoeshoe I International Airport", "Maseru(Mazenod)", "LS", true], - "MSV": ["Sullivan County International Airport", "Monticello", "US", false], - "MSW": ["Massawa International Airport", "Massawa", "ER", false], - "MSX": ["Mossendjo Airport", "Mossendjo", "CG", false], - "MSY": ["Louis Armstrong New Orleans International Airport", "New Orleans", "US", true], - "MSZ": ["Welwitschia Mirabilis International Airport", "Moçâmedes", "AO", true], - "MTA": ["Matamata Glider Airport", "", "NZ", false], - "MTB": ["Montelibano Airport", "Montelíbano", "CO", false], - "MTC": ["Selfridge Air National Guard Base Airport", "Mount Clemens", "US", false], - "MTD": ["Mount Sanford Station Airport", "", "AU", false], - "MTE": ["Monte Alegre Airport", "Monte Alegre", "BR", false], - "MTF": ["Mizan Teferi Airport", "Mizan Teferi", "ET", true], - "MTG": ["Vila Bela da Santíssima Trindade Airport", "Vila Bela Da Santíssima Trindade", "BR", false], - "MTH": ["Florida Keys Marathon International Airport", "Marathon", "US", false], - "MTI": ["Mosteiros Airport", "Vila do Mosteiros", "CV", false], - "MTJ": ["Montrose Regional Airport", "Montrose", "US", true], - "MTK": ["Makin Island Airport", "Makin Island", "KI", false], - "MTL": ["Maitland Airport", "", "AU", false], - "MTM": ["Metlakatla Seaplane Base", "Metlakatla", "US", true], - "MTN": ["Martin State Airport", "Baltimore", "US", false], - "MTO": ["Coles County Memorial Airport", "Mattoon", "US", false], - "MTP": ["Montauk Airport", "Montauk", "US", true], - "MTQ": ["Mitchell Airport", "", "AU", false], - "MTR": ["Los Garzones Airport", "Montería", "CO", true], - "MTS": ["Matsapha International Airport", "Manzini", "SZ", false], - "MTT": ["Minatitlán/Coatzacoalcos International Airport", "Cosoleacaque", "MX", true], - "MTU": ["Montepuez Airport", "Montepuez", "MZ", false], - "MTV": ["Mota Lava Airport", "Ablow", "VU", false], - "MTW": ["Manitowoc County Airport", "Manitowoc", "US", false], - "MTX": ["Metro Field", "Fairbanks", "US", false], - "MTY": ["Monterrey International Airport", "Monterrey", "MX", true], - "MTZ": ["Bar Yehuda Airfield", "Masada", "IL", false], - "MUA": ["Munda Airport", "Munda", "SB", true], - "MUB": ["Maun International Airport", "Maun", "BW", true], - "MUC": ["Munich Airport", "Munich", "DE", true], - "MUD": ["Mueda Airport", "Mueda", "MZ", false], - "MUE": ["Waimea Kohala Airport", "Waimea (Kamuela)", "US", true], - "MUG": ["Mulege Airport", "Mulege", "MX", false], - "MUH": ["Mersa Matruh International Airport", "Marsa Matruh", "EG", true], - "MUI": ["Muir Army Air Field (Fort Indiantown Gap) Airport", "Fort Indiantown Gap(Annville)", "US", false], - "MUJ": ["Mui River Airport", "Omo National Park", "ET", false], - "MUK": ["Mauke Airport", "Mauke Island", "CK", true], - "MUL": ["Spence Airport", "Moultrie", "US", false], - "MUM": ["Muli Airport", "Muli", "MV", false], - "MUN": ["José Tadeo Monagas International Airport", "Maturín", "VE", true], - "MUO": ["Mountain Home Air Force Base", "Mountain Home", "US", false], - "MUP": ["Mulga Park Airport", "", "AU", false], - "MUQ": ["Muccan Station Airport", "Muccan Station", "AU", false], - "MUR": ["Marudi Airport", "Marudi", "MY", true], - "MUS": ["JMSDF Minami Torishima Air Base", "Ogasawara", "JP", false], - "MUT": ["Muscatine Municipal Airport", "Muscatine", "US", false], - "MUU": ["Barra Grande Airport", "Maraú", "BR", false], - "MUW": ["Ghriss Airport", "Ghriss", "DZ", false], - "MUX": ["Multan International Airport", "Multan", "PK", true], - "MUY": ["Mouyondzi Airport", "Mouyondzi", "CG", false], - "MUZ": ["Musoma Airport", "Musoma", "TZ", true], - "MVA": ["Mývatn Airport", "Myvatn", "IS", false], - "MVB": ["M'Vengue El Hadj Omar Bongo Ondimba International Airport", "Franceville", "GA", true], - "MVC": ["Monroe County Aeroplex Airport", "Monroeville", "US", false], - "MVD": ["Carrasco General Cesáreo L. Berisso International Airport", "Ciudad de la Costa", "UY", true], - "MVE": ["Montevideo Chippewa County Airport", "Montevideo", "US", false], - "MVF": ["Dix-Sept Rosado Airport", "Mossoró", "BR", true], - "MVK": ["Mulka Airport", "Mulka", "AU", false], - "MVL": ["Morrisville-Stowe State Airport", "Morrisville", "US", false], - "MVM": ["Kayenta Airport", "Kayenta", "US", false], - "MVN": ["Mount Vernon Outland Airport", "Mount Vernon", "US", false], - "MVO": ["Mongo Airport", "Mongo", "TD", false], - "MVP": ["Fabio Alberto Leon Bentley Airport", "Mitú", "CO", true], - "MVQ": ["Mogilev Airport", "Mogilev", "BY", true], - "MVR": ["Salak Airport", "Maroua", "CM", true], - "MVS": ["Mucuri Airport", "Mucuri", "BR", false], - "MVT": ["Mataiva Airport", "", "PF", true], - "MVU": ["Musgrave Airport", "Musgrave", "AU", false], - "MVV": ["Megève Altiport", "Megève", "FR", false], - "MVW": ["Skagit Regional Airport", "Burlington", "US", false], - "MVX": ["Minvoul Airport", "Minvoul", "GA", false], - "MVY": ["Martha's Vineyard Airport", "Martha's Vineyard", "US", true], - "MVZ": ["Masvingo International Airport", "Masvingo", "ZW", false], - "MWA": ["Veterans Airport of Southern Illinois", "Marion", "US", true], - "MWB": ["Morawa Airport", "", "AU", false], - "MWC": ["Lawrence J Timmerman Airport", "Milwaukee", "US", false], - "MWD": ["Mianwali Air Base", "Mianwali", "PK", false], - "MWE": ["Merowe Airport", "Merowe", "SD", false], - "MWF": ["Maewo-Naone Airport", "Maewo Island", "VU", false], - "MWG": ["Marawaka Airport", "Marawaka", "PG", false], - "MWH": ["Grant County International Airport", "Moses Lake", "US", false], - "MWI": ["Maramuni Airport", "Maramuni", "PG", false], - "MWJ": ["Matthews Ridge Airport", "Matthews Ridge", "GY", false], - "MWK": ["Matak Airport", "Palmatak", "ID", false], - "MWL": ["Mineral Wells Regional Airport", "Mineral Wells", "US", true], - "MWM": ["Windom Municipal Airport", "Windom", "US", false], - "MWN": ["Mwadui Airport", "Mwadui", "TZ", false], - "MWO": ["Middletown Regional Airport", "Middletown", "US", false], - "MWQ": ["Magway Airport", "Magway", "MM", true], - "MWR": ["Motswari Airport", "Motswari Private Game Reserve", "ZA", false], - "MWS": ["Indonesia Morowali Industrial Park (IMIP) Airport", "Morowali", "ID", false], - "MWT": ["Moolawatana Airport", "Moolawatana Station", "AU", false], - "MWU": ["Mussau Airport", "Mussau Island", "PG", false], - "MWV": ["Mondulkiri Airport", "Sen Monorom", "KH", false], - "MWX": ["Muan International Airport", "Muan (Piseo-ri)", "KR", true], - "MWY": ["Miranda Downs Airport", "Miranda Downs", "AU", false], - "MWZ": ["Mwanza International Airport", "Mwanza", "TZ", true], - "MXA": ["Manila Municipal Airport", "Manila", "US", false], - "MXB": ["Andi Jemma Airport", "Masamba", "ID", false], - "MXC": ["Monticello Airport", "Monticello", "US", false], - "MXD": ["Marion Downs Airport", "Marion Downs", "AU", false], - "MXE": ["Laurinburg Maxton Airport", "Maxton", "US", false], - "MXF": ["Maxwell Air Force Base", "Montgomery", "US", false], - "MXH": ["Moro Airport", "Moro", "PG", true], - "MXI": ["Mati National Airport", "Mati", "PH", false], - "MXJ": ["Minna Airport", "Minna", "NG", false], - "MXK": ["Mindik Airport", "Mindik", "PG", false], - "MXL": ["General Rodolfo Sánchez Taboada International Airport", "Mexicali", "MX", true], - "MXM": ["Morombe Airport", "Morombe", "MG", false], - "MXN": ["Morlaix-Ploujean Airport", "Morlaix/Ploujean", "FR", false], - "MXO": ["Monticello Regional Airport", "Monticello", "US", false], - "MXP": ["Milan Malpensa International Airport", "Ferno (VA)", "IT", true], - "MXQ": ["Lorenzo Airport", "Cairu", "BR", false], - "MXR": ["Myrhorod Air Base", "Myrhorod", "UA", false], - "MXS": ["Maota Airport", "Maota", "WS", false], - "MXT": ["Maintirano Airport", "Maintirano", "MG", false], - "MXU": ["Mullewa Airport", "", "AU", false], - "MXV": ["Mörön Airport", "Mörön", "MN", true], - "MXW": ["Mandalgobi Airport", "Mandalgobi", "MN", true], - "MXX": ["Mora Airport", "Mora", "SE", true], - "MXY": ["Mc Carthy Airport", "Mccarthy", "US", false], - "MXZ": ["Meizhou Meixian Changgangji International Airport", "Meizhou (Meixian)", "CN", true], - "MYA": ["Moruya Airport", "Moruya", "AU", true], - "MYB": ["Mayumba Airport", "Mayumba", "GA", false], - "MYC": ["Escuela Mariscal Sucre Airport", "Maracay", "VE", false], - "MYD": ["Malindi International Airport", "Malindi", "KE", true], - "MYE": ["Miyakejima Airport", "Miyakejima", "JP", true], - "MYF": ["Montgomery-Gibbs Executive Airport", "San Diego", "US", false], - "MYG": ["Mayaguana Airport", "Abraham Bay Settlement", "BS", true], - "MYH": ["Marble Canyon Airport", "Marble Canyon", "US", false], - "MYI": ["Murray Island Airport", "Murray Island", "AU", true], - "MYJ": ["Matsuyama Airport", "Matsuyama", "JP", true], - "MYK": ["May Creek Airport", "May Creek", "US", true], - "MYL": ["McCall Municipal Airport", "McCall", "US", true], - "MYM": ["Monkey Mountain Airport", "Monkey Mountain", "GY", false], - "MYN": ["Marib Airport", "Marib", "YE", false], - "MYO": ["Myroodan Station Airport", "", "AU", false], - "MYP": ["Mary International Airport", "Mary", "TM", true], - "MYQ": ["Mysore Airport", "Mysore", "IN", true], - "MYR": ["Myrtle Beach International Airport", "Myrtle Beach", "US", true], - "MYS": ["Moyale Airport", "Moyale", "ET", false], - "MYT": ["Myitkyina Airport", "Myitkyina", "MM", true], - "MYU": ["Mekoryuk Airport", "Mekoryuk", "US", true], - "MYV": ["Yuba County Airport", "Marysville", "US", false], - "MYW": ["Mtwara Airport", "Mtwara", "TZ", true], - "MYX": ["Menyamya Airport", "Menyamya", "PG", false], - "MYY": ["Miri Airport", "Miri", "MY", true], - "MYZ": ["Monkey Bay Airport", "Monkey Bay", "MW", false], - "MZA": ["Mayor PNP Nancy Flores Paucar Airport", "Mazamari", "PE", false], - "MZB": ["Mocímboa da Praia Airport", "Mocímboa da Praia", "MZ", false], - "MZC": ["Mitzic Airport", "Mitzic", "GA", false], - "MZD": ["Méndez Airport", "Santiago de Méndez", "EC", false], - "MZE": ["Manatee Airport", "Spanish Lookout", "BZ", false], - "MZG": ["Penghu Magong Airport", "Huxi", "TW", true], - "MZH": ["Amasya Merzifon Airport", "Amasya", "TR", true], - "MZI": ["Mopti Airport", "Sévaré", "ML", true], - "MZJ": ["Pinal Airpark", "Marana", "US", false], - "MZK": ["Marakei Airport", "Marakei", "KI", false], - "MZL": ["La Nubia Airport", "Manizales", "CO", true], - "MZN": ["Minj Airport", "Minj", "PG", false], - "MZO": ["Sierra Maestra International Airport", "Manzanillo", "CU", true], - "MZP": ["Motueka Airport", "", "NZ", false], - "MZQ": ["Mkuze Airport", "Mkuze", "ZA", true], - "MZR": ["Mazar-i-Sharif International Airport", "Mazar-i-Sharif", "AF", true], - "MZS": ["Moradabad Airport", "Moradabad", "IN", true], - "MZT": ["General Rafael Buelna International Airport", "Mazatlàn", "MX", true], - "MZU": ["Muzaffarpur Airport", "Muzaffarpur", "IN", false], - "MZV": ["Mulu Airport", "Mulu", "MY", true], - "MZW": ["Mecheria Airport", "Mecheria", "DZ", true], - "MZY": ["Mossel Bay Airport", "Mossel Bay", "ZA", false], - "MZZ": ["Marion Municipal Airport", "Marion", "US", false], - "NAA": ["Narrabri Airport", "Narrabri", "AU", true], - "NAC": ["Naracoorte Airport", "", "AU", false], - "NAD": ["Macanal Airport", "Macanal", "CO", false], - "NAE": ["Natitingou Airport", "Natitingou", "BJ", false], - "NAF": ["Banaina Airport", "Banaina-Borneo Island", "ID", false], - "NAG": ["Dr. Babasaheb Ambedkar International Airport", "Nagpur", "IN", true], - "NAH": ["Naha Airport", "Tabukan Utara, Sangihe Islands", "ID", true], - "NAI": ["Annai Airport", "Annai", "GY", false], - "NAJ": ["Nakhchivan International Airport", "Nakhchivan", "AZ", true], - "NAK": ["Nakhon Ratchasima Airport", "Chaloem Phra Kiat", "TH", false], - "NAL": ["Nalchik Airport", "Nalchik", "RU", true], - "NAM": ["Namniwel Airport", "Namniwel", "ID", true], - "NAN": ["Nadi International Airport", "Nadi", "FJ", true], - "NAO": ["Nanchong Gaoping Airport", "Nanchong (Gaoping)", "CN", true], - "NAP": ["Naples International Airport", "Napoli", "IT", true], - "NAQ": ["Qaanaaq Airport", "Qaanaaq", "GL", true], - "NAR": ["Puerto Nare Airport", "Armenia", "CO", false], - "NAS": ["Lynden Pindling International Airport", "Nassau", "BS", true], - "NAT": ["Rio Grande do Norte/São Gonçalo do Amarante–Governador Aluízio Alves International Airport", "Natal", "BR", true], - "NAU": ["Napuka Island Airport", "Napuka Island", "PF", true], - "NAV": ["Nevşehir Kapadokya Airport", "Nevşehir", "TR", true], - "NAW": ["Narathiwat Airport", "", "TH", true], - "NAY": ["Beijing Nanyuan Airport", "Beijing", "CN", false], - "NAZ": ["Nana Airport", "Star Harbor", "SB", false], - "NBA": ["Nambaiyufa Airport", "Nambaiyufa", "PG", false], - "NBB": ["Barranco Minas Airport", "Barranco Minas", "CO", false], - "NBC": ["Begishevo Airport", "Nizhnekamsk", "RU", true], - "NBE": ["Enfidha - Hammamet International Airport", "Enfidha", "TN", true], - "NBG": ["New Orleans NAS JRB/Alvin Callender Field", "New Orleans", "US", false], - "NBH": ["Nambucca Heads Airport", "Nambucca Heads", "AU", false], - "NBJ": ["Dr. Antonio Agostinho Neto International Airport", "Luanda (Ícolo e Bengo)", "AO", true], - "NBL": ["Wannukandi Airport", "San Blas", "PA", false], - "NBN": ["Annobón Airport", "San Antonio de Palé", "GQ", true], - "NBO": ["Jomo Kenyatta International Airport", "Nairobi", "KE", true], - "NBS": ["Changbaishan Airport", "Baishan", "CN", true], - "NBW": ["Leeward Point Field", "Guantanamo Bay Naval Station", "CU", false], - "NBX": ["Douw Aturure Airport", "Nabire", "ID", false], - "NCA": ["North Caicos Airport", "North Caicos", "TC", true], - "NCE": ["Nice-Côte d'Azur Airport", "Nice, Alpes-Maritimes", "FR", true], - "NCG": ["Aeropuerto Municipal de Nuevo Casas Grandes", "Nuevo Casas Grandes", "MX", false], - "NCH": ["Nachingwea Airport", "Nachingwea", "TZ", false], - "NCI": ["Necocli Airport", "Necocli", "CO", false], - "NCJ": ["Sunchales Aeroclub Airport", "Sunchales", "AR", false], - "NCL": ["Newcastle International Airport", "Newcastle upon Tyne, Tyne and Wear", "GB", true], - "NCN": ["Chenega Bay Airport", "Chenega", "US", true], - "NCO": ["Quonset State Airport", "North Kingstown", "US", false], - "NCR": ["San Carlos Airport", "San Carlos", "NI", false], - "NCS": ["Newcastle Airport", "Newcastle", "ZA", false], - "NCT": ["Guanacaste Airport", "Nicoya/Guanacate", "CR", false], - "NCU": ["Nukus International Airport", "Nukus", "UZ", true], - "NCY": ["Annecy Meythet airport", "Annecy", "FR", true], - "NDA": ["Bandanaira Airport", "Bandanaira", "ID", false], - "NDB": ["Nouadhibou International Airport", "Nouadhibou", "MR", true], - "NDC": ["Nanded Airport", "Nanded", "IN", true], - "NDD": ["Sumbe Airport", "Sumbe", "AO", false], - "NDE": ["Mandera Airport", "Mandera", "KE", false], - "NDF": ["N'dalatando Airport", "N'dalatando", "AO", false], - "NDG": ["Qiqihar Sanjiazi Airport", "Qiqihar", "CN", true], - "NDI": ["Namudi Airport", "Namudi", "PG", false], - "NDJ": ["N'Djamena International Airport", "N'Djamena", "TD", true], - "NDK": ["Namorik Airport", "Namorik Atoll", "MH", false], - "NDL": ["N'Délé Airport", "N'Délé", "CF", false], - "NDM": ["Mendi Airport", "Mendi", "ET", false], - "NDN": ["Nadunumu Airport", "Nadunumu", "PG", false], - "NDR": ["Nador Al Aaroui International Airport", "Al Aaroui", "MA", true], - "NDS": ["Sandstone Airport", "Sandstone", "AU", false], - "NDU": ["Rundu Airport", "Rundu", "NA", true], - "NDY": ["Sanday Airport", "Sanday", "GB", true], - "NEC": ["Necochea Airport", "Necochea", "AR", true], - "NEG": ["Negril Airport", "Negril", "JM", false], - "NEI": ["Terney Airport", "Terney", "RU", false], - "NEJ": ["Nejjo Airport", "Nejo", "ET", false], - "NEK": ["Nekemte Airport", "Nekemte", "ET", false], - "NEL": ["Lakehurst Maxfield Field Airport", "Lakehurst", "US", false], - "NEN": ["Whitehouse Naval Outlying Field", "Jacksonville", "US", false], - "NER": ["Chulman Airport", "Neryungri", "RU", true], - "NEU": ["Sam Neua Airport", "", "LA", false], - "NEV": ["Vance W. Amory International Airport", "Charlestown", "KN", true], - "NEW": ["Lakefront Airport", "New Orleans", "US", false], - "NFG": ["Nefteyugansk Airport", "Nefteyugansk", "RU", false], - "NFL": ["Fallon Naval Air Station", "Fallon", "US", false], - "NFO": ["Mata'aho Airport", "Angaha, Niuafo'ou Island", "TO", false], - "NFR": ["Nafurah 1 Airport", "Nafurah 1", "LY", false], - "NGA": ["Young Airport", "", "AU", false], - "NGB": ["Ningbo Lishe International Airport", "Ningbo", "CN", true], - "NGD": ["Captain Auguste George Airport", "Anegada", "VG", false], - "NGE": ["N'Gaoundéré Airport", "N'Gaoundéré", "CM", true], - "NGF": ["Kaneohe Bay MCAS (Marion E. Carl Field) Airport", "Kaneohe", "US", false], - "NGI": ["Ngau Airport", "Ngau", "FJ", true], - "NGK": ["Nogliki Airport", "Nogliki", "RU", true], - "NGL": ["Ngala Airport", "Ngala", "ZA", false], - "NGO": ["Chubu Centrair International Airport", "Tokoname", "JP", true], - "NGP": ["Naval Air Station Corpus Christi Truax Field", "Corpus Christi", "US", false], - "NGQ": ["Ngari Gunsa Airport", "Shiquanhe", "CN", true], - "NGS": ["Nagasaki Airport", "Nagasaki", "JP", true], - "NGU": ["Norfolk Naval Station (Chambers Field)", "Norfolk", "US", false], - "NGW": ["Cabaniss Field Naval Outlying Landing Field", "Corpus Christi", "US", false], - "NGX": ["Manang Airport", "Ngawal", "NP", false], - "NHD": ["Al Minhad Air Base", "Dubai", "AE", false], - "NHF": ["New Halfa Airport", "New Halfa", "SD", false], - "NHK": ["Patuxent River Naval Air Station (Trapnell Field)", "Patuxent River", "US", false], - "NHT": ["RAF Northolt", "Northolt, Greater London", "GB", false], - "NHV": ["Nuku Hiva Airport", "Nuku Hiva", "PF", true], - "NHX": ["Naval Outlying Field Barin", "Foley", "US", false], - "NHZ": ["Brunswick Executive Airport", "Brunswick", "US", false], - "NIA": ["Nimba Airport", "Nimba", "LR", false], - "NIB": ["Nikolai Airport", "Nikolai", "US", true], - "NIF": ["Camp Nifty Airport", "", "AU", false], - "NIG": ["Nikunau Airport", "Nikunau", "KI", false], - "NIK": ["Niokolo-Koba Airport", "Niokolo-Koba National Park", "SN", false], - "NIM": ["Diori Hamani International Airport", "Niamey", "NE", true], - "NIN": ["Ninilchik Airport", "Ninilchik", "US", false], - "NIO": ["Nioki Airport", "Nioki", "CD", false], - "NIP": ["Jacksonville Naval Air Station (Towers Field)", "Jacksonville", "US", false], - "NIQ": ["Niaqornat Heliport", "Niaqornat", "GL", true], - "NIR": ["Chase Field Industrial Airport", "Beeville", "US", false], - "NIS": ["Simberi Airport", "Simberi Island", "PG", false], - "NIT": ["Niort - Marais Poitevin Airport", "Niort/Souché", "FR", false], - "NIU": ["Naiu Airport", "Naiu Atoll", "PF", true], - "NIX": ["Nioro du Sahel Airport", "Nioro du Sahel", "ML", false], - "NJA": ["JMSDF Atsugi Air Base / Naval Air Facility Atsugi", "Ayase / Yamato", "JP", false], - "NJC": ["Nizhnevartovsk Airport", "Nizhnevartovsk", "RU", true], - "NJF": ["Al Najaf International Airport", "Najaf", "IQ", true], - "NJJ": ["Nenjiang Melgen Airport", "Heihe", "CN", false], - "NJK": ["El Centro NAF Airport (Vraciu Field)", "El Centro", "US", false], - "NKA": ["Nkan Airstrip", "Ntoum", "GA", false], - "NKB": ["Noonkanbah Airport", "", "AU", false], - "NKC": ["Nouakchott–Oumtounsy International Airport", "Nouakchott", "MR", true], - "NKD": ["Sinak Airport", "Sinak", "ID", false], - "NKG": ["Nanjing Lukou International Airport", "Nanjing", "CN", true], - "NKI": ["Naukati Bay Seaplane Base", "Tuxekan Island", "US", true], - "NKL": ["N'Kolo-Fuma Airport", "N'Kolo-Fuma", "CD", false], - "NKM": ["Nagoya Airport / JASDF Komaki Air Base", "Nagoya", "JP", true], - "NKN": ["Nankina Airport", "Gwarawon", "PG", false], - "NKO": ["Ankokoambo Airport", "Ankokoambo", "MG", false], - "NKP": ["Nukutepipi Airport", "Nukutepipi", "PF", false], - "NKT": ["Şırnak Şerafettin Elçi Airport", "Şırnak", "TR", true], - "NKU": ["Nkaus Airport", "Nkaus", "LS", false], - "NKW": ["Naval Support Facility Diego Garcia", "Diego Garcia", "IO", false], - "NKX": ["Miramar Marine Corps Air Station - Mitscher Field", "San Diego", "US", false], - "NKY": ["Yokangassi Airport", "Nkayi", "CG", false], - "NLA": ["Simon Mwansa Kapwepwe International Airport", "Ndola", "ZM", true], - "NLC": ["Lemoore Naval Air Station (Reeves Field) Airport", "Lemoore", "US", false], - "NLD": ["Quetzalcóatl International Airport", "Nuevo Laredo", "MX", true], - "NLE": ["Jerry Tyler Memorial Airport", "Niles", "US", false], - "NLF": ["Darnley Island Airport", "Darnley Island", "AU", true], - "NLG": ["Nelson Lagoon Airport", "Nelson Lagoon", "US", true], - "NLH": ["Ninglang Luguhu Airport", "Ninglang", "CN", true], - "NLI": ["Nikolayevsk-na-Amure Airport", "Nikolayevsk-na-Amure Airport", "RU", true], - "NLK": ["Norfolk Island International Airport", "Burnt Pine", "NF", true], - "NLL": ["Nullagine Airport", "", "AU", false], - "NLN": ["Kneeland Airport", "Kneeland", "US", false], - "NLO": ["Ndolo Airport", "N'dolo", "CD", false], - "NLP": ["Nelspruit Airport", "Nelspruit", "ZA", false], - "NLS": ["Nicholson Airport", "", "AU", false], - "NLT": ["Xinyuan Nalati Airport", "Xinyuan", "CN", true], - "NLU": ["Felipe Ángeles International Airport", "Mexico City", "MX", true], - "NLV": ["Mykolaiv International Airport [CLOSED]", "Mykolaiv", "UA", false], - "NMA": ["Namangan International Airport", "Namangan", "UZ", true], - "NMB": ["Daman Airport", "Daman", "IN", false], - "NMC": ["Normans Cay Airport", "Normans Cay", "BS", false], - "NME": ["Nightmute Airport", "Nightmute", "US", true], - "NMF": ["Maafaru International Airport", "Noonu Atoll", "MV", true], - "NMG": ["San Miguel Airport", "Isla del Rey", "PA", false], - "NMI": ["Navi Mumbai International Airport", "Navi Mumbai", "IN", true], - "NML": ["Fort McMurray / Mildred Lake Airport", "Fort McMurray", "CA", false], - "NMN": ["Nomane Airport", "Namane", "PG", false], - "NMP": ["New Moon Airport", "Basalt", "AU", false], - "NMR": ["Nappa Merrie Airport", "", "AU", false], - "NMS": ["Namsang Airport", "Namsang", "MM", false], - "NMT": ["Namtu Airport", "Namtu", "MM", false], - "NNA": ["Kenitra Air Base", "Kenitra", "MA", false], - "NNB": ["Santa Ana Airport", "Santa Ana Island", "SB", true], - "NND": ["Nangade Airport", "Nangade", "MZ", false], - "NNG": ["Nanning Wuxu International Airport", "Nanning (Jiangnan)", "CN", true], - "NNI": ["Namutoni Airport", "Namutoni", "NA", false], - "NNK": ["Naknek Airport", "Naknek", "US", false], - "NNL": ["Nondalton Airport", "Nondalton", "US", false], - "NNM": ["Naryan Mar Airport", "Naryan Mar", "RU", true], - "NNR": ["Connemara Regional Airport", "Inverin", "IE", true], - "NNT": ["Nan Airport", "", "TH", true], - "NNU": ["Nanuque Airport", "Nanuque", "BR", false], - "NNX": ["Nunukan Airport", "Nunukan-Nunukan Island", "ID", false], - "NNY": ["Nanyang Jiangying Airport", "Nanyang (Wancheng)", "CN", true], - "NOA": ["Naval Air Station Nowra - HMAS Albatross", "Nowra Hill", "AU", false], - "NOB": ["Nosara Airport", "Nicoya", "CR", true], - "NOC": ["Ireland West Airport Knock", "Charlestown", "IE", true], - "NOD": ["Norden-Norddeich Airport", "Norddeich", "DE", true], - "NOG": ["Nogales International Airport", "Nogales", "MX", false], - "NOI": ["Krymsk Air Base", "Krymsk", "RU", false], - "NOJ": ["Noyabrsk Airport", "Noyabrsk", "RU", true], - "NOK": ["Xavantina Airport", "Nova Xavantina", "BR", false], - "NOM": ["Nomad River Airport", "Nomad River", "PG", false], - "NON": ["Nonouti Airport", "Nonouti", "KI", false], - "NOO": ["Naoro Airport", "Naoro Vilage", "PG", false], - "NOP": ["Sinop Airport", "Sinop", "TR", true], - "NOR": ["Norðfjörður Airport", "Norðfjörður", "IS", false], - "NOS": ["Nosy Be International Airport", "Nosy Be", "MG", true], - "NOT": ["Marin County Airport - Gnoss Field", "Novato", "US", false], - "NOU": ["La Tontouta International Airport", "Nouméa (La Tontouta)", "NC", true], - "NOV": ["Albano Machado Airport", "Huambo", "AO", true], - "NOZ": ["Spichenkovo Airport", "Novokuznetsk", "RU", true], - "NPA": ["Naval Air Station Pensacola Forrest Sherman Field", "Pensacola", "US", false], - "NPE": ["Hawke's Bay Airport", "Napier", "NZ", true], - "NPG": ["Nipa Airport", "Nipa", "PG", false], - "NPH": ["J. Randy McKnight Nephi Municipal Airport", "Nephi", "US", false], - "NPL": ["New Plymouth Airport", "New Plymouth", "NZ", true], - "NPO": ["Nanga Pinoh Airport", "Nanga Pinoh-Borneo Island", "ID", true], - "NPP": ["Napperby Airport", "Napperby", "AU", false], - "NPR": ["Novo Progresso Airport", "Novo Progresso", "BR", false], - "NPT": ["Newport State Airport", "Newport", "US", true], - "NPU": ["San Pedro Airport", "San Pedro de Urabá", "CO", false], - "NPY": ["Mpanda Airport", "Mpanda", "TZ", false], - "NQA": ["Millington-Memphis Airport", "Millington", "US", false], - "NQI": ["Kingsville Naval Air Station", "Kingsville", "US", false], - "NQL": ["Niquelândia Airport", "Niquelândia", "BR", false], - "NQN": ["Presidente Perón International Airport", "Neuquén", "AR", true], - "NQT": ["Nottingham City Airport", "Nottingham, Nottinghamshire", "GB", false], - "NQU": ["Reyes Murillo Airport", "Nuquí", "CO", true], - "NQX": ["Naval Air Station Key West/Boca Chica Field", "Key West", "US", false], - "NQY": ["Cornwall Airport Newquay", "Newquay", "GB", true], - "NQZ": ["Nursultan Nazarbayev International Airport", "Astana", "KZ", true], - "NRA": ["Narrandera Airport", "Narrandera", "AU", true], - "NRB": ["Naval Station Mayport / Admiral David L McDonald Field", "Jacksonville", "US", false], - "NRD": ["Norderney Airport", "Norderney", "DE", true], - "NRE": ["Namrole Airport", "Namrole", "ID", false], - "NRG": ["Narrogin Airport", "Narrogin", "AU", false], - "NRI": ["Grand Lake Regional Airport", "Afton", "US", false], - "NRK": ["Norrköping Airport", "Norrköping", "SE", true], - "NRL": ["North Ronaldsay Airport", "North Ronaldsay", "GB", true], - "NRM": ["Nara Airport", "Keibane", "ML", false], - "NRN": ["Weeze (Niederrhein) Airport", "Weeze", "DE", true], - "NRR": ["José Aponte de la Torre Airport", "Ceiba", "PR", true], - "NRS": ["Naval Outlying Field Imperial Beach (Ream Field)", "Imperial Beach", "US", false], - "NRT": ["Narita International Airport", "Narita", "JP", true], - "NRY": ["Newry Airport", "Newry", "AU", false], - "NSB": ["Bimini North Seaplane Base", "Bimini", "BS", true], - "NSE": ["Whiting Field Naval Air Station - North", "Milton", "US", false], - "NSH": ["Nowshahr Airport", "Nowshahr", "IR", true], - "NSI": ["Yaoundé Nsimalen International Airport", "Yaoundé", "CM", true], - "NSK": ["Alykel International Airport", "Norilsk", "RU", true], - "NSL": ["Slayton Municipal Airport", "Slayton", "US", false], - "NSM": ["Norseman Aerodrome", "Norseman", "AU", false], - "NSN": ["Nelson Airport", "Nelson", "NZ", true], - "NSO": ["Scone Airport", "", "AU", false], - "NSQ": ["Nuussuaq Heliport", "Nuussuaq", "GL", false], - "NSR": ["Serra da Capivara Airport", "São Raimundo Nonato", "BR", false], - "NST": ["Nakhon Si Thammarat Airport", "Nakhon Si Thammarat", "TH", true], - "NSV": ["Noosa Airport", "", "AU", false], - "NTB": ["Notodden Airport", "Notodden", "NO", false], - "NTC": ["Paradise Island Airport", "Santa Carolina", "MZ", false], - "NTD": ["Point Mugu Naval Air Station (Naval Base Ventura Co)", "Point Mugu", "US", false], - "NTE": ["Nantes Atlantique Airport", "Nantes", "FR", true], - "NTG": ["Nantong Xingdong International Airport", "Nantong (Tongzhou)", "CN", true], - "NTI": ["Stenkol Airport", "Bintuni", "ID", false], - "NTJ": ["Manti-Ephraim Airport", "Manti", "US", false], - "NTL": ["Newcastle Airport", "Williamtown", "AU", true], - "NTN": ["Normanton Airport", "Normanton", "AU", true], - "NTQ": ["Noto Satoyama Airport", "Wajima", "JP", true], - "NTR": ["Del Norte International Airport", "Monterrey", "MX", false], - "NTT": ["Kuini Lavenia Airport", "Niuatoputapu", "TO", true], - "NTU": ["Oceana Naval Air Station", "Virginia Beach", "US", false], - "NTX": ["Ranai Airport", "Ranai-Natuna Besar Island", "ID", true], - "NTY": ["Pilanesberg International Airport", "Pilanesberg", "ZA", false], - "NUA": ["Nuwara Eliya (Lake Gregory) Waterdrome", "Nuwara Eliya", "LK", false], - "NUB": ["Numbulwar Airport", "", "AU", false], - "NUD": ["En Nahud Airport", "En Nahud", "SD", false], - "NUE": ["Nuremberg Airport", "Nuremberg", "DE", true], - "NUF": ["Castlereagh Lake Seaplane Base", "Hatton", "LK", false], - "NUG": ["Nuguria Airstrip", "Nuguria Island", "PG", false], - "NUH": ["Nunchia Airport", "Nunchia", "CO", false], - "NUI": ["Nuiqsut Airport", "Nuiqsut", "US", true], - "NUJ": ["Nojeh Air Base", "Amirabad", "IR", false], - "NUK": ["Nukutavake Airport", "Nukutavake", "PF", false], - "NUL": ["Nulato Airport", "Nulato", "US", true], - "NUM": ["Neom Bay Airport", "Sharma", "SA", true], - "NUP": ["Nunapitchuk Airport", "Nunapitchuk", "US", true], - "NUQ": ["Moffett Federal Airfield", "Mountain View", "US", false], - "NUR": ["Nullabor Motel Airport", "", "AU", false], - "NUS": ["Norsup Airport", "Norsup", "VU", true], - "NUU": ["Nakuru Lanet Airport", "Nakuru", "KE", false], - "NUW": ["Whidbey Island Naval Air Station (Ault Field)", "Oak Harbor", "US", false], - "NUX": ["Novy Urengoy Airport", "Novy Urengoy", "RU", true], - "NVA": ["Benito Salas Airport", "Neiva", "CO", true], - "NVD": ["Nevada Municipal Airport", "Nevada", "US", false], - "NVI": ["Navoi International Airport", "Navoi", "UZ", true], - "NVM": ["Brigadeiro Eduardo Gomes Airport", "Nova Mutum", "BR", false], - "NVN": ["Nervino Airport", "Beckwourth", "US", false], - "NVP": ["Novo Aripuanã Airport", "Novo Aripuanã", "BR", false], - "NVS": ["Nevers-Fourchambault Airport", "Marzy, Nièvre", "FR", false], - "NVT": ["Ministro Victor Konder International Airport", "Navegantes", "BR", true], - "NVY": ["Neyveli Airport", "Neyveli", "IN", false], - "NWA": ["Mohéli Bandar Es Eslam Airport", "Fomboni", "KM", false], - "NWH": ["Parlin Field", "Newport", "US", false], - "NWI": ["Norwich Airport", "Norwich, Norfolk", "GB", true], - "NWT": ["Nowata Airport", "Nowata", "PG", false], - "NYA": ["Nyagan Airport", "Nyagan", "RU", true], - "NYE": ["Nyeri Airport", "Nyeri", "KE", false], - "NYG": ["Quantico Marine Corps Airfield / Turner Field", "Quantico", "US", false], - "NYI": ["Sunyani Airport", "Sunyani", "GH", true], - "NYK": ["Nanyuki Civil Airport", "Gathiuru", "KE", true], - "NYM": ["Nadym Airport", "Nadym", "RU", true], - "NYN": ["Nyngan Airport", "", "AU", false], - "NYO": ["Stockholm Skavsta Airport", "Nyköping", "SE", true], - "NYR": ["Nyurba Airport", "Nyurba", "RU", false], - "NYS": ["New York Skyports Inc Seaplane Base", "New York", "US", true], - "NYT": ["Nay Pyi Taw International Airport", "Naypyitaw", "MM", true], - "NYU": ["Bagan Airport", "Nyaung U", "MM", true], - "NYW": ["Monywar Airport", "Monywar", "MM", false], - "NZA": ["Nzagi Airport", "Nzagi", "AO", false], - "NZC": ["Maria Reiche Neuman Airport", "Nazca", "PE", true], - "NZE": ["Nzérékoré Airport", "Nzérékoré", "GN", false], - "NZG": ["Nizhneangarsk International Airport", "Nizhneangarsk", "RU", true], - "NZH": ["Manzhouli Xijiao Airport", "Manzhouli", "CN", true], - "NZL": ["Zhalantun Genghis Khan Airport", "Zhalantun", "CN", true], - "NZY": ["North Island Naval Air Station-Halsey Field", "San Diego", "US", false], - "OAA": ["Shank Air Base", "Baraki Barak", "AF", false], - "OAG": ["Orange Airport", "Orange", "AU", false], - "OAH": ["Shindand Air Base", "Shindand", "AF", false], - "OAI": ["Bagram Airfield", "Bagram", "AF", false], - "OAJ": ["Albert J Ellis Airport", "Richlands", "US", true], - "OAK": ["Oakland San Francisco Bay Airport", "Oakland", "US", true], - "OAL": ["Cacoal Airport", "Cacoal", "BR", true], - "OAM": ["Oamaru Airport", "", "NZ", false], - "OAN": ["El Arrayán Airport", "Olanchito", "HN", false], - "OAR": ["Marina Municipal Airport", "Marina", "US", false], - "OAS": ["Sharana Airstrip", "Sharana", "AF", false], - "OAX": ["Xoxocotlán International Airport", "Oaxaca", "MX", true], - "OAZ": ["Camp Shorabak Airfield", "Lashkar Gah", "AF", false], - "OBA": ["Oban Airport", "Oban", "AU", false], - "OBC": ["Obock Airport", "Obock", "DJ", false], - "OBD": ["Obano Airport", "Obano", "ID", false], - "OBE": ["Okeechobee County Airport", "Okeechobee", "US", false], - "OBF": ["Oberpfaffenhofen Airport", "Weßling", "DE", false], - "OBI": ["Óbidos Municipal Airport", "Óbidos", "BR", false], - "OBL": ["Oostmalle Airfield", "Zoersel", "BE", false], - "OBM": ["Morobe Airport", "Morobe", "PG", false], - "OBN": ["Oban Airport", "North Connel", "GB", true], - "OBO": ["Tokachi-Obihiro Airport", "Obihiro", "JP", true], - "OBS": ["Aubenas-South Ardèche Airport", "Lanas, Ardèche", "FR", false], - "OBU": ["Kobuk Airport", "Kobuk", "US", true], - "OBX": ["Obo Airport", "Obo", "PG", true], - "OBY": ["Ittoqqortoormiit Heliport", "Ittoqqortoormiit", "GL", true], - "OCA": ["Ocean Reef Club Airport", "Key Largo", "US", false], - "OCC": ["Francisco De Orellana Airport", "Coca", "EC", true], - "OCE": ["Ocean City Municipal Airport", "Ocean City", "US", true], - "OCF": ["Ocala International Airport - Jim Taylor Field", "Ocala", "US", false], - "OCH": ["A L Mangham Jr. Regional Airport", "Nacogdoches", "US", false], - "OCJ": ["Ian Fleming International Airport", "Boscobel", "JM", true], - "OCM": ["Boolgeeda", "", "AU", false], - "OCN": ["Oceanside Municipal Airport", "Oceanside", "US", false], - "OCS": ["Corisco International Airport", "Corisco Island", "GQ", true], - "OCV": ["Aguas Claras Airport", "Ocaña", "CO", false], - "OCW": ["Warren Field", "Washington", "US", false], - "ODA": ["Ouadda Airport", "Ouadda", "CF", false], - "ODB": ["Córdoba Airport", "Córdoba", "ES", true], - "ODC": ["Oakdale Airport", "Oakdale", "US", false], - "ODD": ["Oodnadatta Airport", "", "AU", false], - "ODE": ["Odense Hans Christian Andersen Airport", "Odense", "DK", true], - "ODH": ["RAF Odiham", "Hook, Hampshire", "GB", false], - "ODJ": ["Ouanda Djallé Airport", "Ouanda Djallé", "CF", false], - "ODL": ["Cordillo Downs Airport", "Cordillo Downs", "AU", false], - "ODM": ["Garrett County Airport", "Accident", "US", false], - "ODN": ["Long Seridan Airport", "Long Seridan", "MY", true], - "ODO": ["Bodaybo Airport", "Bodaybo", "RU", true], - "ODR": ["Ord River Airport", "Ord River", "AU", false], - "ODS": ["Odesa International Airport", "Odesa", "UA", false], - "ODT": ["Odessa Schlemeyer Field", "Odessa", "US", false], - "ODW": ["AJ Eisenberg Airport", "Oak Harbor", "US", false], - "ODY": ["Oudomsay Airport", "Oudomsay", "LA", true], - "OEA": ["O'Neal Airport", "Lawrenceville", "US", false], - "OEC": ["Oecusse Route of the Sandalwood International Airport", "Oecussi-Ambeno", "TL", true], - "OEM": ["Vincent Fayks Airport", "Paloemeu", "SR", false], - "OEO": ["L O Simenstad Municipal Airport", "Osceola", "US", false], - "OER": ["Örnsköldsvik Airport", "Örnsköldsvik", "SE", true], - "OES": ["Antoine de Saint Exupéry Airport", "San Antonio Oeste", "AR", true], - "OFF": ["Offutt Air Force Base", "Omaha", "US", false], - "OFI": ["Ouango Fitini Airport", "Ouango Fitini", "CI", false], - "OFK": ["Karl Stefan Memorial Airport", "Norfolk", "US", false], - "OFU": ["Ofu Airport", "Ofu", "AS", true], - "OGA": ["Ogallala Municipal Airport Searle Field", "Ogallala", "US", false], - "OGB": ["Orangeburg Municipal Airport", "Orangeburg", "US", false], - "OGD": ["Ogden Hinckley Airport", "Ogden", "US", true], - "OGE": ["Ogeranang Airport", "", "PG", false], - "OGG": ["Kahului International Airport", "Kahului", "US", true], - "OGL": ["Eugene F. Correia International Airport", "Ogle", "GY", true], - "OGM": ["Ogobsucum Airport", "Ustupu", "PA", false], - "OGN": ["Yonaguni Airport", "Yonaguni", "JP", true], - "OGO": ["Abengourou Airport", "Abengourou", "CI", false], - "OGR": ["Bongor Airport", "Bongor", "TD", false], - "OGS": ["Ogdensburg International Airport", "Ogdensburg", "US", true], - "OGU": ["Ordu–Giresun Airport", "Ordu", "TR", true], - "OGX": ["Ain Beida Airport", "Ouargla", "DZ", true], - "OGZ": ["Vladikavkaz Beslan International Airport", "Beslan", "RU", true], - "OHA": ["RNZAF Base Ohakea", "", "NZ", false], - "OHB": ["Ambohibary Airport", "Moramanga", "MG", false], - "OHD": ["Ohrid St. Paul the Apostle Airport", "Ohrid", "MK", true], - "OHE": ["Mohe Gulian Airport", "Mohe", "CN", true], - "OHH": ["Okha Airport", "Okha", "RU", false], - "OHI": ["Oshakati Airport", "Oshakati", "NA", false], - "OHO": ["Okhotsk Airport", "Okhotsk", "RU", true], - "OHR": ["Wyk auf Föhr Airport", "Wyk auf Föhr", "DE", false], - "OHS": ["Suhar International Airport", "Suhar", "OM", true], - "OHT": ["Kohat Airport", "Kohat", "PK", false], - "OIA": ["Ourilândia do Norte Airport", "Ourilândia do Norte", "BR", false], - "OIC": ["Lt Warren Eaton Airport", "Norwich", "US", false], - "OIM": ["Oshima Airport", "Izu Oshima", "JP", true], - "OIR": ["Okushiri Airport", "Okushiri Island", "JP", true], - "OIT": ["Oita Airport", "Oita", "JP", true], - "OJC": ["Johnson County Executive Airport", "Olathe", "US", false], - "OJU": ["Tanjung Api Airport", "Tojo Una-Una", "ID", true], - "OKA": ["Naha International Airport", "Naha", "JP", true], - "OKB": ["Orchid Beach Airport", "Orchid Beach", "AU", false], - "OKC": ["OKC Will Rogers World Airport", "Oklahoma City", "US", true], - "OKD": ["Sapporo Okadama Airport", "Sapporo", "JP", true], - "OKE": ["Okinoerabu Airport", "Wadomari", "JP", true], - "OKF": ["Okaukuejo Airport", "Okaukuejo", "NA", false], - "OKG": ["Okoyo Airport", "Okoyo", "CG", false], - "OKI": ["Oki Global Geopark Airport", "Okinoshima", "JP", true], - "OKJ": ["Okayama Momotaro Airport", "Okayama", "JP", true], - "OKK": ["Kokomo Municipal Airport", "Kokomo", "US", false], - "OKL": ["Oksibil Airport", "Oksibil", "ID", true], - "OKM": ["Okmulgee Regional Airport", "Okmulgee", "US", false], - "OKN": ["Okondja Airport", "Okondja", "GA", false], - "OKO": ["Yokota Air Base", "Fussa", "JP", false], - "OKP": ["Oksapmin Airport", "Oksapmin", "PG", false], - "OKQ": ["Okaba Airport", "Okaba", "ID", false], - "OKR": ["Yorke Island Airport", "Yorke Island", "AU", true], - "OKS": ["Garden County Airport/King Rhiley Field", "Oshkosh", "US", false], - "OKT": ["Oktyabrskiy Airport", "Kzyl-Yar", "RU", false], - "OKU": ["Mokuti Lodge Airport", "Mokuti Lodge", "NA", false], - "OKV": ["Okao Airport", "Okao", "PG", false], - "OKY": ["Oakey Army Aviation Centre", "", "AU", true], - "OLA": ["Ørland Airport", "Ørland", "NO", true], - "OLB": ["Olbia Costa Smeralda Airport", "Olbia (SS)", "IT", true], - "OLC": ["Senadora Eunice Micheles Airport", "São Paulo De Olivença", "BR", false], - "OLD": ["Dewitt Field - Old Town Municipal Airport", "Old Town", "US", false], - "OLE": ["Cattaraugus County-Olean Airport", "Olean", "US", false], - "OLF": ["L M Clayton Airport", "Wolf Point", "US", true], - "OLG": ["Olare Orok Airfield", "Mara", "KE", false], - "OLH": ["Old Harbor Airport", "Old Harbor", "US", true], - "OLI": ["Rif Airport", "Rif", "IS", false], - "OLJ": ["North West Santo Airport", "Olpoi", "VU", true], - "OLK": ["Aeródromo de Fuerte", "Fuerte Olimpo", "PY", false], - "OLL": ["Oyo Ollombo Airport", "Oyo", "CG", false], - "OLM": ["Olympia Regional Airport", "Olympia", "US", true], - "OLN": ["Lago Musters Airport", "Sarmiento", "AR", false], - "OLO": ["Olomouc Airport", "Olomouc", "CZ", false], - "OLP": ["Olympic Dam Airport", "Olympic Dam", "AU", true], - "OLQ": ["Olsobip Airport", "Olsobip", "PG", false], - "OLR": ["Salerno Airport", "Khost", "AF", false], - "OLS": ["Nogales International Airport", "Nogales", "US", false], - "OLU": ["Columbus Municipal Airport", "Columbus", "US", false], - "OLV": ["Olive Branch Airport", "Olive Branch", "US", false], - "OLX": ["Mara-Olkiombo Airport", "Serena", "KE", false], - "OLY": ["Olney Noble Airport", "Olney-Noble", "US", false], - "OLZ": ["Olyokminsk Airport", "Olyokminsk", "RU", true], - "OMA": ["Eppley Airfield", "Omaha", "US", true], - "OMB": ["Omboue Hospital Airport", "Omboue", "GA", false], - "OMC": ["Ormoc Airport", "Ormoc City", "PH", false], - "OMD": ["Oranjemund Airport", "Oranjemund", "NA", true], - "OME": ["Nome Airport", "Nome", "US", true], - "OMF": ["King Hussein Air College", "Mafraq", "JO", false], - "OMG": ["Omega Airport", "Omega", "NA", false], - "OMH": ["Urmia Airport", "Urmia", "IR", true], - "OMI": ["Omidiyeh Airport", "Omidiyeh", "IR", false], - "OMJ": ["JMSDF Omura Air Base", "Nagasaki", "JP", false], - "OMK": ["Omak Airport", "Omak", "US", false], - "OMM": ["Marmul Airport", "Marmul", "OM", false], - "OMN": ["Zomin Airport", "Zomin", "UZ", true], - "OMO": ["Mostar International Airport", "Mostar", "BA", true], - "OMR": ["Oradea International Airport", "Oradea", "RO", true], - "OMS": ["Omsk Central Airport", "Omsk", "RU", true], - "OMY": ["Preah Vinhear Airport", "Tbeng Meanchey", "KH", false], - "ONA": ["Winona Municipal Airport Max Conrad Field", "Winona", "US", false], - "ONB": ["Ononge Airport", "Onange Mission", "PG", false], - "OND": ["Ondangwa Airport", "Ondangwa", "NA", true], - "ONG": ["Mornington Island Airport", "", "AU", true], - "ONH": ["Albert S Nader Regional Airport", "Oneonta", "US", false], - "ONI": ["Moanamani Airport", "Moanamani", "ID", false], - "ONJ": ["Odate Noshiro Airport", "Kitaakita", "JP", true], - "ONK": ["Olenyok Airport", "Olenyok", "RU", false], - "ONL": ["O'Neill Municipal Airport–John L Baker Field", "O'Neill", "US", false], - "ONM": ["Socorro Municipal Airport", "Socorro", "US", false], - "ONO": ["Ontario Municipal Airport", "Ontario", "US", false], - "ONP": ["Newport Municipal Airport", "Newport", "US", false], - "ONQ": ["Zonguldak Çaycuma Airport", "Zonguldak", "TR", true], - "ONR": ["Monkira Airport", "", "AU", false], - "ONS": ["Onslow Airport", "", "AU", false], - "ONT": ["Ontario International Airport", "Ontario", "US", true], - "ONU": ["Ono-i-Lau Airport", "Ono-i-Lau", "FJ", false], - "ONX": ["Enrique Adolfo Jimenez Airport", "Colón", "PA", true], - "ONY": ["Olney Municipal Airport", "Olney", "US", false], - "OOA": ["Oskaloosa Municipal Airport", "Cedar", "US", false], - "OOD": ["Koodaideri Mine Airport", "Koodaideri Mine", "AU", false], - "OOK": ["Toksook Bay Airport", "Toksook Bay", "US", true], - "OOL": ["Gold Coast Airport", "Gold Coast", "AU", true], - "OOM": ["Cooma Snowy Mountains Airport", "Cooma", "AU", true], - "OOR": ["Mooraberree Airport", "", "AU", false], - "OOT": ["Onotoa Airport", "Onotoa", "KI", false], - "OPA": ["Kópasker Airport", "Kópasker", "IS", false], - "OPB": ["Open Bay Airport", "Maitanakunai", "PG", false], - "OPF": ["Miami-Opa Locka Executive Airport", "Miami", "US", true], - "OPI": ["Oenpelli Airport", "", "AU", false], - "OPL": ["St Landry Parish Ahart Field", "Opelousas", "US", false], - "OPO": ["Francisco de Sá Carneiro Airport", "Porto", "PT", true], - "OPP": ["Salinópolis Airport", "Salinópolis", "BR", true], - "OPS": ["Presidente João Batista Figueiredo Airport", "Sinop", "BR", true], - "OPU": ["Balimo Airport", "Balimo", "PG", true], - "OPW": ["Opuwa Airport", "Opuwa", "NA", false], - "OQN": ["Kokand Airport", "Kokand", "UZ", false], - "ORA": ["Orán Airport", "Orán", "AR", false], - "ORB": ["Örebro Airport", "Örebro", "SE", true], - "ORC": ["Orocue Airport", "Orocue", "CO", false], - "ORD": ["Chicago O'Hare International Airport", "Chicago", "US", true], - "ORE": ["Orléans–Saint-Denis-de-l'Hôtel Airport", "Saint-Denis-de-l'Hôtel, Loiret", "FR", false], - "ORF": ["Norfolk International Airport", "Norfolk", "US", true], - "ORG": ["Zorg en Hoop Airport", "Paramaribo", "SR", true], - "ORH": ["Worcester Regional Airport", "Worcester", "US", true], - "ORI": ["Port Lions Airport", "Port Lions", "US", true], - "ORJ": ["Orinduik Airport", "Orinduik", "GY", false], - "ORK": ["Cork International Airport", "Cork", "IE", true], - "ORL": ["Orlando Executive Airport", "Orlando", "US", false], - "ORM": ["Sywell Aerodrome", "Northampton, Northamptonshire", "GB", false], - "ORN": ["Oran Es-Sénia (Ahmed Ben Bella) International Airport", "Es-Sénia", "DZ", true], - "ORO": ["Yoro Airport", "Yoro", "HN", false], - "ORP": ["Orapa Airport", "Orapa", "BW", false], - "ORR": ["Yorketown Airport", "", "AU", false], - "ORT": ["Northway Airport", "Northway", "US", true], - "ORU": ["Juan Mendoza International Airport", "Oruro", "BO", true], - "ORV": ["Robert (Bob) Curtis Memorial Airport", "Noorvik", "US", true], - "ORW": ["Ormara Airport", "Ormara Raik", "PK", false], - "ORX": ["Oriximiná Airport", "Oriximiná", "BR", false], - "ORY": ["Paris-Orly Airport", "Paris (Orly, Val-de-Marne)", "FR", true], - "ORZ": ["H.E Alfredo Martinez (Tower Hill) Airstrip", "Orange Walk", "BZ", true], - "OSB": ["Grand Glaize Osage Beach Airport", "Osage Beach", "US", false], - "OSC": ["Oscoda Wurtsmith Airport", "Oscoda", "US", false], - "OSD": ["Åre Östersund Airport", "Östersund", "SE", true], - "OSE": ["Omora Airport", "Omora", "PG", false], - "OSG": ["Ossima Airport", "Ossima", "PG", false], - "OSH": ["Wittman Regional Airport", "Oshkosh", "US", false], - "OSI": ["Osijek Airport", "Osijek(Klisa)", "HR", true], - "OSJ": ["Ol Seki Naboisho Airstrip", "Mara Simba", "KE", false], - "OSK": ["Oskarshamn Airport", "Oskarshamn", "SE", false], - "OSL": ["Oslo-Gardermoen International Airport", "Oslo (Gardermoen)", "NO", true], - "OSM": ["Mosul International Airport", "Mosul", "IQ", false], - "OSN": ["Osan Air Base", "Pyeongtaek", "KR", false], - "OSO": ["Osborne Mine Airport", "", "AU", false], - "OSR": ["Leoš Janáček Airport Ostrava", "Mošnov", "CZ", true], - "OSS": ["Osh International Airport", "Osh", "KG", true], - "OST": ["Ostend-Bruges International Airport", "Oostende", "BE", true], - "OSU": ["The Ohio State University Airport - Don Scott Field", "Columbus", "US", false], - "OSW": ["Orsk Airport", "Orsk", "RU", true], - "OSX": ["Kosciusko Attala County Airport", "Kosciusko", "US", false], - "OSY": ["Namsos Airport", "Namsos", "NO", true], - "OTC": ["Bol-Berim Airport", "Bol", "TD", false], - "OTD": ["Raul Arias Espinoza Airport", "Contadora Island", "PA", true], - "OTG": ["Worthington Municipal Airport", "Worthington", "US", false], - "OTH": ["Southwest Oregon Regional Airport", "North Bend", "US", true], - "OTI": ["Pitu Airport", "Gotalalamo-Morotai Island", "ID", false], - "OTJ": ["Otjiwarongo Airport", "Otjiwarongo", "NA", false], - "OTK": ["Tillamook Airport", "Tillamook", "US", false], - "OTL": ["Boutilimit Airport", "Boutilimit", "MR", false], - "OTM": ["Ottumwa Regional Airport", "Ottumwa", "US", false], - "OTN": ["Ed-Air Airport", "Oaktown", "US", false], - "OTO": ["Otog Banner Wulan Airport", "Wulan", "CN", false], - "OTP": ["Bucharest Henri Coandă International Airport", "Otopeni", "RO", true], - "OTQ": ["Otog Front Banner Oljoq Airport", "Otog Front Banner", "CN", false], - "OTR": ["Coto 47 Airport", "Corredores", "CR", false], - "OTS": ["Anacortes Airport", "Anacortes", "US", true], - "OTT": ["Andre Maggi Airport", "Cotriguaçu", "BR", false], - "OTU": ["Otu Airport", "Remedios", "CO", false], - "OTZ": ["Ralph Wien Memorial Airport", "Kotzebue", "US", true], - "OUA": ["Ouagadougou Thomas Sankara International Airport", "Ouagadougou", "BF", true], - "OUD": ["Oujda Angads Airport", "Ahl Angad", "MA", true], - "OUE": ["Ouesso Airport", "", "CG", false], - "OUG": ["Ouahigouya Airport", "Ouahigouya", "BF", false], - "OUH": ["Oudtshoorn Airport", "Oudtshoorn", "ZA", false], - "OUI": ["Ouessant Airport", "Ushant", "FR", true], - "OUL": ["Oulu Airport", "Oulu / Oulunsalo", "FI", true], - "OUM": ["Oum Hadjer Airport", "Oum Hadjer", "TD", false], - "OUN": ["University of Oklahoma Westheimer Airport", "Norman", "US", false], - "OUR": ["Batouri Airport", "Batouri", "CM", false], - "OUS": ["Jornalista Benedito Pimentel Airport", "Ourinhos", "BR", false], - "OUT": ["Bousso Airport", "Bousso", "TD", false], - "OUZ": ["Tazadit Airport", "Zouérate", "MR", true], - "OVA": ["Bekily Airport", "Bekily", "MG", false], - "OVB": ["Novosibirsk Tolmachevo Airport", "Novosibirsk", "RU", true], - "OVD": ["Asturias Airport", "Ranón", "ES", true], - "OVE": ["Oroville Municipal Airport", "Oroville", "US", false], - "OVG": ["Overberg Airport", "Overberg", "ZA", false], - "OVL": ["El Tuqui Airport", "Ovalle", "CL", false], - "OVR": ["Olavarria Airport", "Olavarria", "AR", false], - "OVS": ["Sovetskiy Airport", "Sovetskiy", "RU", true], - "OWA": ["Owatonna Degner Regional Airport", "Owatonna", "US", false], - "OWB": ["Owensboro Daviess County Airport", "Owensboro", "US", true], - "OWD": ["Norwood Memorial Airport", "Norwood", "US", false], - "OWK": ["Central Maine/Norridgewock Airport", "Norridgewock", "US", false], - "OXB": ["Osvaldo Vieira International Airport", "Bissau", "GW", true], - "OXC": ["Waterbury Oxford Airport", "Oxford", "US", false], - "OXD": ["Miami University Airport", "Oxford", "US", false], - "OXF": ["London Oxford Airport", "Kidlington, Oxfordshire", "GB", false], - "OXO": ["Orientos Airport", "Orientos", "AU", false], - "OXR": ["Oxnard Airport", "Oxnard", "US", false], - "OXY": ["Morney Airport", "", "AU", false], - "OYA": ["Goya Airport", "Goya", "AR", false], - "OYC": ["Camopi Airport", "Camopi", "GF", false], - "OYE": ["Oyem Airport", "Oyem", "GA", true], - "OYG": ["Moyo Airport", "Moyo", "UG", false], - "OYK": ["Oiapoque Airport", "Oiapoque", "BR", false], - "OYL": ["Moyale Airport", "Moyale", "KE", false], - "OYN": ["Ouyen Airport", "Ouyen", "AU", false], - "OYO": ["Tres Arroyos Airport", "Tres Arroyos", "AR", false], - "OYP": ["Saint-Georges-de-l'Oyapock Airport", "Saint-Georges-de-l'Oyapock", "GF", false], - "OZA": ["Ozona Municipal Airport", "Ozona", "US", false], - "OZC": ["Labo Airport", "Ozamiz", "PH", true], - "OZG": ["Zagora Airport", "Zagora", "MA", true], - "OZH": ["Zaporizhzhia International Airport", "Zaporizhia", "UA", false], - "OZP": ["Moron Air Base", "Morón", "ES", false], - "OZR": ["Cairns AAF (Fort Rucker) Air Field", "Fort Rucker/Ozark", "US", false], - "OZZ": ["Ouarzazate International Airport", "Ouarzazate", "MA", true], - "PAA": ["Hpa-N Airport", "Hpa-N", "MM", false], - "PAB": ["Bilaspur Airport", "Bilaspur", "IN", true], - "PAC": ["Marcos A. Gelabert International Airport", "Albrook", "PA", true], - "PAD": ["Paderborn Lippstadt Airport", "Büren", "DE", true], - "PAE": ["Seattle Paine Field International Airport", "Everett", "US", true], - "PAF": ["Pakuba Airport", "Pakuba", "UG", false], - "PAG": ["Pagadian Airport", "Pagadian", "PH", true], - "PAH": ["Barkley Regional Airport", "Paducah", "US", true], - "PAJ": ["Parachinar Airport", "Parachinar", "PK", false], - "PAK": ["Port Allen Airport", "Hanapepe", "US", false], - "PAL": ["German Olano Air Base", "La Dorada", "CO", false], - "PAM": ["Tyndall Air Force Base", "Panama City", "US", false], - "PAN": ["Pattani Airport", "", "TH", false], - "PAO": ["Palo Alto Airport", "Palo Alto", "US", false], - "PAP": ["Toussaint Louverture International Airport", "Port-au-Prince", "HT", true], - "PAQ": ["Warren \"Bud\" Woods Palmer Municipal Airport", "Palmer", "US", false], - "PAS": ["Paros National Airport", "Paros", "GR", true], - "PAT": ["Jay Prakash Narayan Airport", "Patna", "IN", true], - "PAU": ["Pauk Airport", "Pauk", "MM", false], - "PAV": ["Paulo Afonso Airport", "Paulo Afonso", "BR", true], - "PAW": ["Pambwa Airstrip", "Pambwa Station", "PG", false], - "PAX": ["Port-de-Paix Airport", "Port-de-Paix", "HT", false], - "PAY": ["Pamol Airport", "Pamol", "MY", false], - "PAZ": ["El Tajín National Airport", "Poza Rica", "MX", true], - "PBA": ["Fábio Perini Airport", "Cairu", "BR", false], - "PBB": ["Paranaíba Airport", "Paranaíba", "BR", false], - "PBC": ["Hermanos Serdán International Airport", "Puebla", "MX", true], - "PBD": ["Porbandar Airport", "Porbandar", "IN", true], - "PBE": ["Puerto Berrio Airport", "Puerto Berrio", "CO", false], - "PBF": ["Pine Bluff Regional Airport, Grider Field", "Pine Bluff", "US", false], - "PBG": ["Plattsburgh International Airport", "Plattsburgh", "US", true], - "PBH": ["Paro International Airport", "Paro", "BT", true], - "PBJ": ["Tavie Airport", "Paama Island", "VU", true], - "PBL": ["General Bartolome Salom International Airport", "Puerto Cabello", "VE", false], - "PBM": ["Johan Adolf Pengel International Airport", "Paramaribo", "SR", true], - "PBN": ["Porto Amboim Airport", "Port Amboim", "AO", false], - "PBO": ["Paraburdoo Airport", "Paraburdoo", "AU", true], - "PBP": ["Islita Airport", "Nandayure", "CR", false], - "PBQ": ["Pimenta Bueno Airport", "Pimenta Bueno", "BR", false], - "PBR": ["Puerto Barrios Airport", "Puerto Barrios", "GT", true], - "PBT": ["Aeródromo de Puerto Leda", "Puerto Leda", "PY", false], - "PBU": ["Putao Airport", "Putao", "MM", true], - "PBX": ["Fazenda Piraguassu Airport", "Porto Alegre do Norte", "BR", false], - "PBZ": ["Plettenberg Bay Airport", "Plettenberg Bay", "ZA", false], - "PCA": ["Portage Creek Airport", "Portage Creek", "US", false], - "PCB": ["Pondok Cabe Air Base", "Jakarta", "ID", false], - "PCC": ["Puerto Rico Airport", "Puerto Rico", "CO", false], - "PCD": ["Prairie Du Chien Municipal Airport", "Prairie Du Chien", "US", false], - "PCF": ["Potchefstroom Airport", "Potchefstroom", "ZA", false], - "PCG": ["Paso Caballos Airport", "Paso Caballos", "GT", false], - "PCH": ["Palacios Airport", "Palacios", "HN", false], - "PCJ": ["Puerto La Victoria Airport", "Puerto La Victoria", "PY", false], - "PCL": ["Cap FAP David Abenzur Rengifo International Airport", "Pucallpa", "PE", true], - "PCM": ["Playa del Carmen National Airport", "Playa del Carmen", "MX", false], - "PCN": ["Picton Aerodrome", "Koromiko", "NZ", true], - "PCO": ["Punta Colorada Airport", "La Ribera", "MX", false], - "PCP": ["Principe Airport", "São Tomé & Príncipe", "ST", true], - "PCQ": ["Boun Neau Airport", "Phongsaly", "LA", false], - "PCR": ["German Olano Airport", "Puerto Carreño", "CO", true], - "PCS": ["Picos Airport", "Picos", "BR", false], - "PCT": ["Princeton Airport", "Princeton/Rocky Hill", "US", false], - "PCU": ["Poplarville Pearl River County Airport", "Poplarville", "US", false], - "PCV": ["Punta Chivato Airport", "Mulegé", "MX", false], - "PDA": ["Obando Cesar Gaviria Trujillo Airport", "Puerto Inírida", "CO", true], - "PDB": ["Pedro Bay Airport", "Pedro Bay", "US", true], - "PDC": ["Mueo Airport", "Népoui", "NC", false], - "PDD": ["Ponta do Ouro Airport", "Ponta do Ouro", "MZ", false], - "PDE": ["Pandie Pandie Airport", "", "AU", false], - "PDF": ["Prado Airport", "Prado", "BR", false], - "PDG": ["Minangkabau International Airport", "Padang (Katapiang)", "ID", true], - "PDI": ["Pindiu Airport", "Pindiu", "PG", false], - "PDK": ["DeKalb Peachtree Airport", "Atlanta", "US", true], - "PDL": ["João Paulo II Airport", "Ponta Delgada", "PT", true], - "PDM": ["Capt. J. Montenegro Airport", "Pedasí", "PA", true], - "PDN": ["Parndana Airport", "Kangaroo Island", "AU", false], - "PDO": ["Pendopo Airport", "Talang Gudang-Sumatra Island", "ID", true], - "PDP": ["Capitan Corbeta CA Curbelo International Airport", "Punta del Este", "UY", true], - "PDR": ["Presidente José Sarney Airport", "Presidente Dutra", "BR", false], - "PDS": ["Piedras Negras International Airport", "Piedras Negras", "MX", true], - "PDT": ["Eastern Oregon Regional Airport at Pendleton", "Pendleton", "US", true], - "PDU": ["Tydeo Larre Borges Airport", "Paysandú", "UY", false], - "PDV": ["Plovdiv International Airport", "Plovdiv", "BG", true], - "PDX": ["Portland International Airport", "Portland", "US", true], - "PDZ": ["Pedernales Airport", "Capure", "VE", false], - "PEA": ["Penneshaw Airport", "Ironstone", "AU", false], - "PEB": ["Pebane Airport", "Pebane", "MZ", false], - "PEC": ["Pelican Seaplane Base", "Pelican", "US", true], - "PED": ["Pardubice Airport", "Pardubice", "CZ", true], - "PEE": ["Perm International Airport", "Perm", "RU", true], - "PEF": ["Peenemünde Airport", "Peenemünde", "DE", false], - "PEG": ["Perugia San Francesco d'Assisi – Umbria International Airport", "Perugia (PG)", "IT", true], - "PEH": ["Comodoro Pedro Zanni Airport", "Pehuajó", "AR", false], - "PEI": ["Matecaña International Airport", "Pereira", "CO", true], - "PEK": ["Beijing Capital International Airport", "Beijing", "CN", true], - "PEL": ["Pelaneng Airport", "Pelaneng", "LS", false], - "PEM": ["Padre Aldamiz International Airport", "Puerto Maldonado", "PE", true], - "PEN": ["Penang International Airport", "Penang", "MY", true], - "PEP": ["Peppimenarti Airport", "Peppimenarti", "AU", false], - "PEQ": ["Pecos Municipal Airport", "Pecos", "US", false], - "PER": ["Perth International Airport", "Perth", "AU", true], - "PES": ["Petrozavodsk Airport", "Petrozavodsk", "RU", true], - "PET": ["João Simões Lopes Neto International Airport", "Pelotas", "BR", true], - "PEU": ["Puerto Lempira Airport", "Puerto Lempira", "HN", true], - "PEV": ["Pécs-Pogány International Airport", "Pécs", "HU", true], - "PEW": ["Bacha Khan International Airport", "Peshawar", "PK", true], - "PEX": ["Pechora Airport", "Pechora", "RU", true], - "PEY": ["Penong Airport", "Penong", "AU", false], - "PEZ": ["Penza Airport", "Penza", "RU", true], - "PFA": ["Harbin Pingfang Airport", "Harbin", "CN", false], - "PFB": ["Lauro Kurtz Airport", "Passo Fundo", "BR", true], - "PFC": ["Pacific City State Airport", "Pacific City", "US", false], - "PFM": ["Primrose Airport", "Primrose", "CA", false], - "PFO": ["Paphos International Airport", "Paphos", "CY", true], - "PFQ": ["Parsabad-Moghan Airport", "Parsabad", "IR", true], - "PFR": ["Ilebo Airport", "Ilebo", "CD", true], - "PGA": ["Page Municipal Airport", "Page", "US", true], - "PGB": ["Pangoa Airport", "Pangoa", "PG", false], - "PGC": ["Grant County Airport", "Petersburg", "US", false], - "PGD": ["Punta Gorda Airport", "Punta Gorda", "US", true], - "PGE": ["Yegepa Airport", "", "PG", false], - "PGF": ["Perpignan-Rivesaltes (Llabanère) Airport", "Perpignan/Rivesaltes", "FR", true], - "PGH": ["Pantnagar Airport", "Pantnagar", "IN", true], - "PGI": ["Chitato Airport", "Chitato", "AO", false], - "PGK": ["Depati Amir Airport", "Pangkal Pinang", "ID", true], - "PGL": ["Trent Lott International Airport", "Pascagoula", "US", false], - "PGM": ["Port Graham Airport", "Port Graham", "US", true], - "PGN": ["Pangia Airport", "Pangia", "PG", false], - "PGO": ["Stevens Field", "Pagosa Springs", "US", false], - "PGQ": ["Buli Airport", "Pekaulang", "ID", false], - "PGR": ["Kirk Field", "Paragould", "US", false], - "PGS": ["Grand Canyon Caverns Airport", "Peach Springs", "US", false], - "PGU": ["Persian Gulf International Airport", "Khiyaroo", "IR", true], - "PGV": ["Pitt-Greenville Airport", "Greenville", "US", true], - "PGX": ["Périgueux-Bassillac Airport", "Périgueux/Bassillac", "FR", false], - "PGZ": ["Ponta Grossa Airport - Comandante Antonio Amilton Beraldo", "Ponta Grossa", "BR", true], - "PHA": ["Phan Rang Airport", "Phan Rang", "VN", false], - "PHB": ["Parnaíba - Prefeito Doutor João Silva Filho International Airport", "Parnaíba", "BR", true], - "PHC": ["Port Harcourt International Airport", "Port Harcourt", "NG", true], - "PHD": ["Harry Clever Field", "New Philadelphia", "US", false], - "PHE": ["Port Hedland International Airport", "Port Hedland", "AU", true], - "PHF": ["Newport News Williamsburg International Airport", "Newport News", "US", true], - "PHG": ["Port Harcourt City Airport / Port Harcourt Air Force Base", "Port Harcourt", "NG", true], - "PHH": ["Pokhara International Airport", "Pokhara", "NP", true], - "PHI": ["Pinheiro Airport", "Pinheiro", "BR", false], - "PHK": ["Palm Beach County Glades Airport", "Pahokee", "US", false], - "PHL": ["Philadelphia International Airport", "Philadelphia", "US", true], - "PHN": ["St Clair County International Airport", "Port Huron", "US", false], - "PHO": ["Point Hope Airport", "Point Hope", "US", true], - "PHP": ["Philip Airport", "Philip", "US", false], - "PHQ": ["The Monument Airport", "Phosphate Hill", "AU", false], - "PHR": ["Nanuku Airport", "Nanuku Auberge Resort", "FJ", false], - "PHS": ["Phitsanulok Airport", "Phitsanulok", "TH", true], - "PHT": ["Henry County Airport", "Paris", "US", false], - "PHW": ["Hendrik Van Eck Airport", "Phalaborwa", "ZA", true], - "PHX": ["Phoenix Sky Harbor International Airport", "Phoenix", "US", true], - "PHY": ["Phetchabun Airport", "", "TH", true], - "PIA": ["General Wayne A. Downing Peoria International Airport", "Peoria", "US", true], - "PIB": ["Hattiesburg Laurel Regional Airport", "Moselle", "US", true], - "PIC": ["Pine Cay Airport", "Pine Cay", "TC", false], - "PIE": ["St. Petersburg Clearwater International Airport", "Pinellas Park", "US", true], - "PIH": ["Pocatello Regional Airport", "Pocatello", "US", true], - "PIK": ["Glasgow Prestwick Airport", "Prestwick, South Ayrshire", "GB", true], - "PIL": ["Aeródromo Don Carlos Miguel Gimenez", "Pilar", "PY", false], - "PIM": ["Harris County Airport", "Pine Mountain", "US", false], - "PIN": ["Parintins Airport", "Parintins", "BR", false], - "PIO": ["Captain Renán Elías Olivera International Airport", "Pisco", "PE", false], - "PIP": ["Pilot Point Airport", "Pilot Point", "US", true], - "PIQ": ["Pipillipai Airport", "Pipillipai", "GY", false], - "PIR": ["Pierre Regional Airport", "Pierre", "US", true], - "PIS": ["Poitiers-Biard Airport", "Poitiers/Biard", "FR", true], - "PIT": ["Pittsburgh International Airport", "Pittsburgh", "US", true], - "PIU": ["PAF Captain Guillermo Concha Iberico International Airport", "Piura", "PE", true], - "PIV": ["Pirapora Airport", "Pirapora", "BR", false], - "PIW": ["Pikwitonei Airport", "Pikwitonei", "CA", false], - "PIX": ["Pico Airport", "Pico Island", "PT", true], - "PIZ": ["Point Lay LRRS Airport", "Point Lay", "US", true], - "PJA": ["Pajala Airport", "Pajala", "SE", true], - "PJB": ["Payson Airport", "Payson", "US", false], - "PJC": ["Aeropuerto Nacional Dr. Augusto Roberto Fuster", "Pedro Juan Caballero", "PY", false], - "PJG": ["Panjgur Airport", "Panjgur", "PK", false], - "PJM": ["Puerto Jimenez Airport", "Puerto Jimenez", "CR", true], - "PKA": ["Napaskiak Airport", "Napaskiak", "US", true], - "PKB": ["Mid Ohio Valley Regional Airport", "Parkersburg (Williamstown)", "US", true], - "PKC": ["Yelizovo Airport", "Petropavlovsk-Kamchatsky", "RU", true], - "PKD": ["Park Rapids Municipal Airport Konshok Field", "Park Rapids", "US", false], - "PKE": ["Parkes Airport", "Parkes", "AU", true], - "PKF": ["Park Falls Municipal Airport", "Park Falls", "US", false], - "PKG": ["Pulau Pangkor Airport", "Pangkor Island", "MY", true], - "PKJ": ["Playa Grande Airport", "Playa Grande", "GT", false], - "PKK": ["Pakhokku Airport", "Pakhokku", "MM", false], - "PKM": ["Port Kaituma Airport", "Port Kaituma", "GY", false], - "PKN": ["Iskandar Airport", "Pangkalanbun", "ID", true], - "PKO": ["Parakou Airport", "Parakou", "BJ", false], - "PKP": ["Puka Puka Airport", "", "PF", true], - "PKR": ["Pokhara Domestic Airport", "Pokhara", "NP", true], - "PKT": ["Port Keats Airport", "Wadeye", "AU", false], - "PKU": ["Sultan Syarif Kasim II International Airport / Roesmin Nurjadin AFB", "Pekanbaru", "ID", true], - "PKV": ["Princess Olga Pskov International Airport", "Pskov", "RU", true], - "PKW": ["Selebi Phikwe Airport", "Selebi Phikwe", "BW", false], - "PKX": ["Beijing Daxing International Airport", "Beijing", "CN", true], - "PKY": ["Tjilik Riwut Airport", "Palangkaraya", "ID", true], - "PKZ": ["Pakse International Airport", "Pakse", "LA", true], - "PLA": ["Planadas Airport", "Planadas", "CO", false], - "PLD": ["Playa Samara/Carrillo Airport", "Carrillo", "CR", false], - "PLE": ["Paiela Airport", "Paiela", "PG", false], - "PLF": ["Pala Airport", "Pala", "TD", false], - "PLJ": ["Placencia Airport", "Placencia", "BZ", true], - "PLK": ["M. Graham Clark Downtown Airport", "Branson / Hollister", "US", false], - "PLL": ["Ponta Pelada Airport / Manaus Air Base", "Manaus", "BR", false], - "PLM": ["Sultan Mahmud Badaruddin II Airport", "Palembang", "ID", true], - "PLN": ["Pellston Regional Airport of Emmet County Airport", "Pellston", "US", true], - "PLO": ["Port Lincoln Airport", "Port Lincoln", "AU", true], - "PLQ": ["Palanga International Airport", "Palanga", "LT", true], - "PLR": ["St Clair County Airport", "Pell City", "US", false], - "PLS": ["Providenciales International Airport", "Providenciales", "TC", true], - "PLT": ["Plato Airport", "Plato", "CO", false], - "PLU": ["Pampulha - Carlos Drummond de Andrade Airport", "Belo Horizonte", "BR", false], - "PLV": ["Suprunovka Airport", "Poltava", "UA", false], - "PLW": ["Mutiara - SIS Al-Jufrie Airport", "Palu", "ID", true], - "PLX": ["Semei International Airport", "Semey", "KZ", true], - "PLY": ["Plymouth Municipal Airport", "Plymouth", "US", false], - "PLZ": ["Chief Dawid Stuurman International Airport", "Gqeberha (Port Elizabeth)", "ZA", true], - "PMA": ["Pemba Airport", "Chake Chake", "TZ", false], - "PMB": ["Pembina Municipal Airport", "Pembina", "US", false], - "PMC": ["El Tepual International Airport", "Puerto Montt", "CL", true], - "PMD": ["Palmdale Regional Airport / USAF Plant 42 Airport", "Palmdale", "US", false], - "PMF": ["Parma Airport", "Parma (PR)", "IT", true], - "PMG": ["Ponta Porã Airport", "Ponta Porã", "BR", true], - "PMH": ["Greater Portsmouth Regional Airport", "Portsmouth", "US", false], - "PMI": ["Palma de Mallorca Airport", "Palma de Mallorca", "ES", true], - "PMK": ["Palm Island Airport", "", "AU", true], - "PML": ["Port Moller Airport", "Cold Bay", "US", false], - "PMN": ["Pumani Airport", "Pumani", "PG", false], - "PMO": ["Falcone–Borsellino Airport", "Palermo", "IT", true], - "PMP": ["Pimaga Airport", "Pimaga", "PG", false], - "PMQ": ["Perito Moreno Jalil Hamer Airport", "Perito Moreno", "AR", true], - "PMR": ["Palmerston North Airport", "Palmerston North", "NZ", true], - "PMS": ["Palmyra Airport", "Tadmur", "SY", false], - "PMT": ["Paramakatoi Airport", "Paramakatoi", "GY", false], - "PMV": ["Del Caribe Santiago Mariño International Airport", "Isla Margarita", "VE", true], - "PMW": ["Brigadeiro Lysias Rodrigues Airport", "Palmas", "BR", true], - "PMY": ["El Tehuelche Airport", "Puerto Madryn", "AR", true], - "PMZ": ["Palmar Sur Airport", "Palmar Sur", "CR", false], - "PNA": ["Pamplona Airport", "Pamplona", "ES", true], - "PNB": ["Porto Nacional Airport", "Porto Nacional", "BR", false], - "PNC": ["Ponca City Regional Airport", "Ponca City", "US", false], - "PND": ["Punta Gorda Airport", "Punta Gorda", "BZ", true], - "PNE": ["Northeast Philadelphia Airport", "Philadelphia", "US", false], - "PNG": ["Paranaguá Airport", "Paranaguá", "BR", false], - "PNH": ["Phnom Penh International Airport", "Phnom Penh (Pou Senchey)", "KH", false], - "PNI": ["Pohnpei International Airport", "Pohnpei Island", "FM", true], - "PNJ": ["Penglai Shahekou Airport", "Yantai", "CN", false], - "PNK": ["Supadio International Airport", "Pontianak", "ID", true], - "PNL": ["Pantelleria Airport", "Pantelleria (TP)", "IT", true], - "PNN": ["Princeton Municipal Airport", "Princeton", "US", false], - "PNP": ["Girua Airport", "Popondetta", "PG", true], - "PNQ": ["Pune International Airport", "Pune", "IN", true], - "PNR": ["Antonio Agostinho-Neto International Airport", "Pointe Noire", "CG", true], - "PNS": ["Pensacola International Airport", "Pensacola", "US", true], - "PNT": ["Lieutenant Julio Gallardo Airport", "Puerto Natales", "CL", true], - "PNU": ["Panguitch Municipal Airport", "Panguitch", "US", false], - "PNV": ["Panevėžys Air Base", "Panevėžys", "LT", false], - "PNX": ["North Texas Regional Airport Perrin Field", "Denison", "US", false], - "PNY": ["Pondicherry Airport", "Puducherry (Pondicherry)", "IN", true], - "PNZ": ["Senador Nilo Coelho Airport", "Petrolina", "BR", true], - "POA": ["Porto Alegre-Salgado Filho International Airport", "Porto Alegre", "BR", true], - "POB": ["Pope Field", "Fort Bragg", "US", false], - "POC": ["Brackett Field", "La Verne", "US", false], - "POD": ["Podor Airport", "Podor", "SN", false], - "POE": ["Polk Army Air Field", "Fort Polk", "US", false], - "POF": ["Poplar Bluff Municipal Airport", "Poplar Bluff", "US", false], - "POG": ["Port Gentil International Airport", "Port Gentil", "GA", true], - "POH": ["Pocahontas Municipal Airport", "Pocahontas", "US", false], - "POI": ["Capitan Nicolas Rojas Airport", "Potosí", "BO", false], - "POJ": ["Patos de Minas Airport", "Patos de Minas", "BR", false], - "POL": ["Pemba Airport", "Pemba", "MZ", true], - "POM": ["Port Moresby Jacksons International Airport", "Port Moresby", "PG", true], - "PON": ["Poptún Airport", "Poptún", "GT", false], - "POO": ["Poços de Caldas - Embaixador Walther Moreira Salles Airport", "Poços De Caldas", "BR", false], - "POP": ["Gregorio Luperon International Airport", "Puerto Plata", "DO", true], - "POR": ["Pori Airport", "Pori", "FI", true], - "POS": ["Piarco International Airport", "Port of Spain", "TT", true], - "POT": ["Ken Jones Airport", "Ken Jones", "JM", false], - "POU": ["Dutchess County Airport", "Poughkeepsie", "US", false], - "POV": ["Prešov Air Base", "Prešov", "SK", false], - "POW": ["Portorož Airport", "Sečovlje", "SI", false], - "POX": ["Pontoise-Cormeilles Aerodrome", "Cormeilles-en-Vexin, Val-d'Oise", "FR", false], - "POY": ["Powell Municipal Airport", "Powell", "US", false], - "POZ": ["Poznań-Ławica Airport", "Poznań", "PL", true], - "PPA": ["Perry Lefors Field", "Pampa", "US", false], - "PPB": ["Presidente Prudente Airport", "Presidente Prudente", "BR", true], - "PPC": ["Prospect Creek Airport", "Prospect Creek", "US", false], - "PPE": ["Mar de Cortés International Airport", "Puerto Peñasco", "MX", true], - "PPF": ["Tri-City Airport", "Parsons", "US", false], - "PPG": ["Pago Pago International Airport", "Pago Pago", "AS", true], - "PPH": ["Paraitepuy Airstrip", "Paraitepuy de Ikabarú", "VE", false], - "PPI": ["Port Pirie Airport", "", "AU", false], - "PPJ": ["Pulau Panjang Airport", "Jakarta", "ID", false], - "PPK": ["Petropavl International Airport", "Petropavl", "KZ", true], - "PPL": ["Phaplu Airport", "Phaplu", "NP", false], - "PPM": ["Pompano Beach Airpark", "Pompano Beach", "US", false], - "PPN": ["Guillermo León Valencia Airport", "Popayán", "CO", true], - "PPP": ["Proserpine Whitsunday Coast Airport", "Proserpine", "AU", true], - "PPQ": ["Paraparaumu Airport", "", "NZ", false], - "PPR": ["Tuanku Tambusai Airport", "Pasir Pengarayan", "ID", false], - "PPS": ["Puerto Princesa International Airport / PAF Antonio Bautista Air Base", "Puerto Princesa", "PH", true], - "PPT": ["Fa'a'ā International Airport", "Papeete", "PF", true], - "PPU": ["Hpapun Airport", "Pa Pun", "MM", false], - "PPV": ["Port Protection Seaplane Base", "Port Protection", "US", true], - "PPW": ["Papa Westray Airport", "Papa Westray, Orkney Islands", "GB", true], - "PPX": ["Param Airport", "Nepesi", "PG", false], - "PPY": ["Pouso Alegre Airport", "Pouso Alegre", "BR", false], - "PPZ": ["Puerto Paez Airport", "Puerto Paez", "VE", false], - "PQC": ["Phú Quốc International Airport", "Phu Quoc Island", "VN", true], - "PQD": ["Passikudah Helipad", "Batticaloa", "LK", false], - "PQI": ["Presque Isle International Airport", "Presque Isle", "US", true], - "PQM": ["Palenque International Airport", "Palenque", "MX", false], - "PQQ": ["Port Macquarie Airport", "Port Macquarie", "AU", true], - "PQS": ["Pilot Station Airport", "Pilot Station", "US", true], - "PQT": ["Qeqertaq Heliport", "Qeqertaq", "GL", true], - "PRA": ["General Urquiza Airport", "Parana", "AR", true], - "PRB": ["Paso Robles Municipal Airport", "Paso Robles", "US", false], - "PRC": ["Prescott Regional Airport - Ernest A. Love Field", "Prescott", "US", true], - "PRD": ["Pardoo Airport", "Pardoo", "AU", false], - "PRE": ["Pore Airport", "Pore", "CO", false], - "PRG": ["Václav Havel Airport Prague", "Prague", "CZ", true], - "PRH": ["Phrae Airport", "", "TH", false], - "PRI": ["Praslin Island Airport", "Praslin Island", "SC", true], - "PRK": ["Prieska Airport", "Prieska", "ZA", false], - "PRM": ["Portimão Airport", "Portimão", "PT", true], - "PRN": ["Priština Adem Jashari International Airport", "Prishtina", "XK", true], - "PRO": ["Perry Municipal Airport", "Perry", "US", false], - "PRP": ["Propriano Airport", "Propriano", "FR", false], - "PRQ": ["Termal Airport", "Presidencia Roque Sáenz Peña", "AR", false], - "PRR": ["Paruima Airport", "Paruima", "GY", false], - "PRS": ["Parasi Airport", "Parasi", "SB", false], - "PRU": ["Pyay Airport", "Pye", "MM", false], - "PRV": ["Přerov Air Base", "Přerov", "CZ", false], - "PRW": ["Prentice Airport", "Prentice", "US", false], - "PRX": ["Cox Field", "Paris", "US", false], - "PRY": ["Wonderboom Airport", "Pretoria", "ZA", false], - "PRZ": ["Prineville Airport", "Prineville", "US", false], - "PSA": ["Pisa International Airport", "Pisa (PI)", "IT", true], - "PSB": ["Mid-State Regional Airport", "Philipsburg", "US", false], - "PSC": ["Tri Cities Airport", "Pasco", "US", true], - "PSD": ["Port Said International Airport", "Port Said", "EG", true], - "PSE": ["Mercedita International Airport", "Ponce", "PR", true], - "PSF": ["Pittsfield Municipal Airport", "Pittsfield", "US", false], - "PSG": ["Petersburg James A Johnson Airport", "Petersburg", "US", true], - "PSH": ["Sankt Peter-Ording Airport", "Sankt Peter-Ording", "DE", false], - "PSI": ["Pasni Airport", "Pasni", "PK", false], - "PSJ": ["Kasiguncu Airport", "Poso-Celebes Island", "ID", false], - "PSK": ["New River Valley Airport", "Dublin", "US", false], - "PSL": ["Perth/Scone Airport", "Perth, Perth and Kinross", "GB", false], - "PSM": ["Portsmouth International Airport at Pease", "Portsmouth", "US", true], - "PSN": ["Palestine Municipal Airport", "Palestine", "US", false], - "PSO": ["Antonio Nariño Airport", "Chachagüí", "CO", true], - "PSP": ["Palm Springs International Airport", "Palm Springs", "US", true], - "PSQ": ["Philadelphia Seaplane Base", "Essington", "US", false], - "PSR": ["Abruzzo Airport", "Pescara", "IT", true], - "PSS": ["Libertador Gral D Jose De San Martin Airport", "Posadas", "AR", true], - "PSU": ["Pangsuma Airport", "Putussibau-Borneo Island", "ID", true], - "PSW": ["José Figueiredo Municipal Airport", "Passos", "BR", false], - "PSX": ["Palacios Municipal Airport", "Palacios", "US", false], - "PSY": ["Port Stanley Airport", "Stanley", "FK", true], - "PSZ": ["Capitán Av. Salvador Ogaya G. airport", "Puerto Suárez", "BO", true], - "PTA": ["Port Alsworth Airport", "Port Alsworth", "US", true], - "PTB": ["Dinwiddie County Airport", "Petersburg", "US", false], - "PTC": ["Port Alice Seaplane Base", "Port Alice", "US", false], - "PTD": ["Port Alexander Seaplane Base", "Port Alexander", "US", true], - "PTF": ["Malolo Lailai Island Airport", "Malolo Lailai Island", "FJ", true], - "PTG": ["Polokwane International Airport", "Polokwane", "ZA", true], - "PTH": ["Port Heiden Airport", "Port Heiden", "US", true], - "PTJ": ["Portland Airport", "", "AU", true], - "PTK": ["Oakland County International Airport", "Pontiac", "US", false], - "PTL": ["Walter Braedt Segú Airport", "Máncora", "PE", false], - "PTM": ["Palmarito Airport", "Palmarito", "VE", false], - "PTN": ["Harry P Williams Memorial Airport", "Patterson", "US", false], - "PTO": ["Juvenal Loureiro Cardoso Airport", "Pato Branco", "BR", true], - "PTP": ["Maryse Condé International Airport", "Pointe-à-Pitre", "GP", true], - "PTQ": ["Porto de Moz Airport", "Porto de Moz", "BR", false], - "PTS": ["Atkinson Municipal Airport", "Pittsburg", "US", false], - "PTT": ["Pratt Regional Airport", "Pratt", "US", false], - "PTU": ["Platinum Airport", "Platinum", "US", true], - "PTV": ["Porterville Municipal Airport", "Porterville", "US", false], - "PTW": ["Heritage Field", "Pottstown", "US", false], - "PTX": ["Pitalito Airport", "Pitalito", "CO", false], - "PTY": ["Tocumen International Airport", "Tocumen", "PA", true], - "PTZ": ["Rio Amazonas Airport", "Shell Mera", "EC", false], - "PUA": ["Puas Airport", "Puas Mission", "PG", false], - "PUB": ["Pueblo Memorial Airport", "Pueblo", "US", true], - "PUC": ["Carbon County Regional Airport / Buck Davis Field", "Price", "US", false], - "PUD": ["Puerto Deseado Airport", "Puerto Deseado", "AR", true], - "PUE": ["Puerto Obaldía Airport", "Puerto Obaldía", "PA", false], - "PUF": ["Pau Pyrénées Airport", "Pau/Pyrénées (Uzein)", "FR", true], - "PUG": ["Port Augusta Airport", "", "AU", true], - "PUJ": ["Punta Cana International Airport", "Punta Cana", "DO", true], - "PUK": ["Pukaruha Airport", "Pukaruha", "PF", false], - "PUL": ["Port of Poulsbo Marina Moorage Seaplane Base", "Poulsbo", "US", false], - "PUN": ["Punia Airport", "Punia", "CD", false], - "PUP": ["Po Airport", "Po", "BF", false], - "PUQ": ["President Carlos Ibáñez International Airport", "Punta Arenas", "CL", true], - "PUR": ["Puerto Rico Airport", "Puerto Rico/Manuripi", "BO", true], - "PUS": ["Gimhae International Airport", "Busan", "KR", true], - "PUT": ["Sri Sathya Sai Airport", "Puttaparthi", "IN", false], - "PUU": ["Tres De Mayo Airport", "Puerto Asís", "CO", true], - "PUV": ["Poum / Malabou Airport", "Malabou", "NC", false], - "PUW": ["Pullman-Moscow Regional Airport", "Pullman", "US", true], - "PUX": ["El Mirador Airport", "Puerto Varas", "CL", false], - "PUY": ["Pula Airport", "Pula", "HR", true], - "PUZ": ["Puerto Cabezas Airport", "Puerto Cabezas", "NI", true], - "PVA": ["El Embrujo Airport", "Providencia", "CO", true], - "PVC": ["Provincetown Municipal Airport", "Provincetown", "US", false], - "PVD": ["Rhode Island T. F. Green International Airport", "Providence/Warwick", "US", true], - "PVF": ["Placerville Airport", "Placerville", "US", false], - "PVG": ["Shanghai Pudong International Airport", "Shanghai (Pudong)", "CN", true], - "PVH": ["Governador Jorge Teixeira de Oliveira International Airport", "Porto Velho", "BR", true], - "PVI": ["Paranavaí Airport", "Paranavaí", "BR", false], - "PVK": ["Aktion National Airport", "Preveza", "GR", true], - "PVL": ["Pike County Airport Hatcher Field", "Pikeville", "US", false], - "PVO": ["Reales Tamarindos Airport", "Portoviejo", "EC", false], - "PVR": ["Puerto Vallarta International Airport", "Puerto Vallarta", "MX", true], - "PVS": ["Provideniya Bay Airport", "Chukotka", "RU", false], - "PVU": ["Provo Municipal Airport", "Provo", "US", true], - "PVW": ["Hale County Airport", "Plainview", "US", false], - "PWA": ["Wiley Post Airport", "Oklahoma City", "US", false], - "PWD": ["Sher-Wood Airport", "Plentywood", "US", false], - "PWE": ["Pevek Airport", "Apapelgino", "RU", true], - "PWI": ["Beles Airport", "Pawe", "ET", false], - "PWK": ["Chicago Executive Airport", "Chicago/Prospect Heights/Wheeling", "US", false], - "PWL": ["Jenderal Besar Soedirman Airport", "Purwokerto-Java Island", "ID", false], - "PWM": ["Portland International Jetport", "Portland", "US", true], - "PWN": ["Pitts Town Airport", "Pitts Town", "BS", false], - "PWO": ["Pweto Airport", "Pweto", "CD", false], - "PWQ": ["Pavlodar International Airport", "Pavlodar", "KZ", true], - "PWR": ["Port Walter Seaplane Base", "Port Walter", "US", false], - "PWT": ["Bremerton National Airport", "Bremerton", "US", false], - "PWY": ["Ralph Wenz Field", "Pinedale", "US", false], - "PXA": ["Atung Bungsu Airport", "Bentayan", "ID", false], - "PXH": ["Prominent Hill Airport", "Mount Eba", "AU", true], - "PXL": ["Polacca Airport", "Polacca", "US", false], - "PXM": ["Puerto Escondido International Airport", "Puerto Escondido", "MX", true], - "PXO": ["Porto Santo Airport", "Vila Baleira", "PT", true], - "PXR": ["Surin Airport", "Surin", "TH", true], - "PXU": ["Pleiku Airport", "Pleiku", "VN", true], - "PYA": ["Velásquez Airport", "Puerto Boyacá", "CO", false], - "PYB": ["Jeypore Airport", "Jeypore", "IN", false], - "PYC": ["Playón Chico Airport", "Ukupseni", "PA", false], - "PYE": ["Tongareva Airport", "Penrhyn Island", "CK", false], - "PYG": ["Pakyong Airport", "Pakyong", "IN", false], - "PYH": ["Cacique Aramare Airport", "Puerto Ayacucho", "VE", false], - "PYJ": ["Polyarny Airport", "Yakutia", "RU", true], - "PYK": ["Payam International Airport", "Karaj", "IR", true], - "PYL": ["Perry Island Seaplane Base", "Perry Island", "US", false], - "PYM": ["Plymouth Municipal Airport", "Plymouth", "US", false], - "PYN": ["Payán Airport", "Payán", "CO", false], - "PYO": ["Putumayo Airport", "Puerto Putumayo", "EC", false], - "PYR": ["Andravida Air Base", "Andravida", "GR", false], - "PYS": ["Paradise Skypark Airport", "Paradise", "US", false], - "PYT": ["Pedro Rabelo de Souza Airport", "Paracatu", "BR", true], - "PYV": ["Yaviza Airport", "Yaviza", "PA", false], - "PYY": ["Pai Airport", "Pai", "TH", false], - "PZA": ["Paz de Ariporo Airport", "Paz de Ariporo", "CO", false], - "PZB": ["Pietermaritzburg Airport", "Pietermaritzburg", "ZA", true], - "PZE": ["Penzance Heliport", "Penzance, Cornwall", "GB", true], - "PZH": ["Zhob Airport", "Fort Sandeman", "PK", true], - "PZI": ["Panzhihua Bao'anying Airport", "Panzhihua (Renhe)", "CN", true], - "PZK": ["Pukapuka Island Airport", "Pukapuka Atoll", "CK", false], - "PZL": ["Zulu Inyala Airport", "Phinda", "ZA", false], - "PZO": ["General Manuel Carlos Piar International Airport", "Guyana City", "VE", true], - "PZR": ["Pizarro Airport", "Bajo Baudó", "CO", false], - "PZS": ["Maquehue Airport", "Temuco", "CL", false], - "PZU": ["Port Sudan New International Airport", "Port Sudan", "SD", true], - "PZY": ["Piešťany Airport", "Piešťany", "SK", false], - "QBC": ["Bella Coola Airport", "Bella Coola", "CA", true], - "QCH": ["Colatina Airport", "Colatina", "BR", false], - "QCP": ["Currais Novos Airport", "Currais Novos", "BR", false], - "QCU": ["Akunnaaq Heliport", "Akunnaaq", "GL", true], - "QCY": ["RAF Coningsby", "Lincoln, Lincolnshire", "GB", false], - "QDB": ["Cachoeira do Sul Airport", "Cachoeira Do Sul", "BR", false], - "QDV": ["Comte. Rolim Adolfo Amaro–Jundiaí State Airport", "Jundiaí", "BR", false], - "QFG": ["Eqalugaarsuit Heliport", "Eqalugaarsuit", "GL", true], - "QFI": ["Iginniarfik Heliport", "Iginniarfik", "GL", true], - "QFN": ["Narsarmijit Heliport", "Narsarmijit", "GL", true], - "QFX": ["Igaliku Heliport", "Igaliku", "GL", true], - "QGF": ["Montenegro Airport", "Montenegro", "BR", false], - "QGP": ["Garanhuns Airport", "Garanhuns", "BR", false], - "QGQ": ["Attu Heliport", "Attu", "GL", true], - "QGU": ["Gifu Airport", "Gifu", "JP", false], - "QHR": ["Harar Meda Airport", "Debre Zeyit", "ET", false], - "QHU": ["Husum-Schwesing Airport", "Husum", "DE", false], - "QHV": ["Novo Hamburgo Airport", "Novo Hamburgo", "BR", false], - "QIG": ["Iguatu Airport", "Iguatu", "BR", false], - "QIT": ["Itapetinga Airport", "Itapetinga", "BR", false], - "QJE": ["Kitsissuarsuit Heliport", "Kitsissuarsuit", "GL", true], - "QJH": ["Qassimiut Heliport", "Qassimiut", "GL", true], - "QJI": ["Ikamiut Heliport", "Ikamiut", "GL", true], - "QJP": ["Pocheon (G-217) Airport", "Pocheon", "KR", false], - "QMJ": ["Shahid Asiyaee Airport", "Masjed Soleyman", "IR", false], - "QOQ": ["Saarloq Heliport", "Saarloq", "GL", true], - "QOW": ["Sam Mbakwe International Cargo Airport", "Owerri", "NG", true], - "QPG": ["Paya Lebar Air Base", "Paya Lebar", "SG", false], - "QPW": ["Kangaatsiaq Heliport", "Kangaatsiaq", "GL", true], - "QRA": ["Rand Airport", "Johannesburg", "ZA", false], - "QRF": ["Bragado Airport", "Bragado", "AR", false], - "QRM": ["Narromine Airport", "", "AU", false], - "QRO": ["Querétaro Intercontinental Airport", "Querétaro", "MX", true], - "QRW": ["Warri Airport", "Okpe", "NG", true], - "QRY": ["Ikerassaarsuk Heliport", "Ikerassaarsuk", "GL", true], - "QSC": ["Mário Pereira Lopes–São Carlos International Airport", "São Carlos", "BR", false], - "QSF": ["Ain Arnat Airport", "Sétif", "DZ", true], - "QSI": ["Moshi Airport", "Moshi", "TZ", false], - "QSR": ["Salerno Costa d'Amalfi Airport", "Salerno", "IT", true], - "QSX": ["New Amsterdam Airport", "New Amsterdam", "GY", false], - "QSZ": ["Shache Airport", "Shache", "CN", true], - "QUB": ["Ubari Airport", "Ubari", "LY", false], - "QUG": ["Goodwood Aerodrome", "Chichester, West Sussex", "GB", false], - "QUO": ["Akwa Ibom International Airport", "Uyo", "NG", true], - "QUV": ["Aappilattoq Heliport", "Aappilattoq", "GL", true], - "QUW": ["Ammassivik Heliport", "Ammassivik", "GL", true], - "QUY": ["RAF Wyton", "St Ives, Cambridgeshire", "GB", false], - "RAA": ["Rakanda Airport", "Rakanda", "PG", false], - "RAB": ["Tokua Airport", "Kokopo", "PG", true], - "RAC": ["John H Batten Airport", "Racine", "US", false], - "RAD": ["Road Town Seaplane Base", "Tortola", "VG", false], - "RAE": ["Arar Domestic Airport", "Arar", "SA", true], - "RAF": ["Rafaela Airport", "Rafaela", "AR", false], - "RAG": ["Raglan Aerodrome", "Raglan", "NZ", false], - "RAH": ["Rafha Domestic Airport", "Rafha", "SA", true], - "RAI": ["Nelson Mandela International Airport", "Praia", "CV", true], - "RAJ": ["Rajkot Airport", "Rajkot", "IN", false], - "RAK": ["Marrakesh Menara Airport", "Marrakesh", "MA", true], - "RAL": ["Riverside Municipal Airport", "Riverside", "US", false], - "RAM": ["Ramingining Airport", "", "AU", true], - "RAN": ["Ravenna Airport", "Ravenna (RA)", "IT", false], - "RAO": ["Leite Lopes Airport", "Ribeirão Preto", "BR", true], - "RAP": ["Rapid City Regional Airport", "Rapid City", "US", true], - "RAQ": ["Sugimanuru Airport", "Raha", "ID", false], - "RAR": ["Rarotonga International Airport", "Avarua", "CK", true], - "RAS": ["Sardar-e-Jangal Airport", "Rasht", "IR", true], - "RAV": ["Cravo Norte Airport", "Cravo Norte", "CO", false], - "RAW": ["Arawa Airport", "Arawa", "PG", false], - "RAX": ["Oram Airport", "", "PG", false], - "RAZ": ["Rawalakot Airport", "Rawalakot", "PK", false], - "RBA": ["Rabat-Salé Airport", "Rabat", "MA", true], - "RBB": ["Borba Airport", "Borba", "BR", true], - "RBC": ["Robinvale Airport", "Robinvale", "AU", false], - "RBD": ["Dallas Executive Airport", "Dallas", "US", false], - "RBE": ["Ratanakiri Airport", "Ratanakiri", "KH", false], - "RBF": ["Big Bear City Airport", "Big Bear", "US", false], - "RBG": ["Roseburg Regional Airport", "Roseburg", "US", false], - "RBI": ["Rabi Island Airport", "Rabi Island", "FJ", false], - "RBJ": ["Rebun Airport", "Rebun", "JP", false], - "RBK": ["French Valley Airport", "Murrieta", "US", false], - "RBL": ["Red Bluff Municipal Airport", "Red Bluff", "US", false], - "RBM": ["Straubing Airport", "Atting", "DE", false], - "RBO": ["Roboré Airport", "Roboré", "BO", false], - "RBP": ["Raba Raba Airport", "Rabaraba", "PG", false], - "RBQ": ["Rurrenabaque Airport", "Rurrenabaque", "BO", true], - "RBR": ["Rio Branco-Plácido de Castro International Airport", "Rio Branco", "BR", true], - "RBS": ["Orbost Airport", "", "AU", false], - "RBT": ["Marsabit Airport", "Marsabit", "KE", false], - "RBU": ["Roebourne Airport", "Roebourne", "AU", false], - "RBV": ["Ramata Airport", "Ramata", "SB", true], - "RBW": ["Lowcountry Regional Airport", "Walterboro", "US", false], - "RBX": ["Rumbek Airport", "Rumbek", "SS", false], - "RBY": ["Ruby Airport", "Ruby", "US", true], - "RBZ": ["Shahrisabz Southwest Airport", "Shahrisabz", "UZ", false], - "RCA": ["Ellsworth Air Force Base", "Rapid City", "US", false], - "RCB": ["Richards Bay Airport", "Richards Bay", "ZA", true], - "RCE": ["Roche Harbor Airport", "Roche Harbor", "US", true], - "RCH": ["Almirante Padilla Airport", "Riohacha", "CO", true], - "RCK": ["H H Coffield Regional Airport", "Rockdale", "US", false], - "RCL": ["Redcliffe Airport", "Redcliffe", "VU", false], - "RCM": ["Richmond Airport", "", "AU", true], - "RCO": ["Rochefort-Saint-Agnant (BA 721) Airport", "Rochefort/Saint-Agnant", "FR", false], - "RCQ": ["Reconquista Airport", "Reconquista", "AR", false], - "RCR": ["Fulton County Airport", "Rochester", "US", false], - "RCS": ["Rochester Airport", "Rochester, Kent", "GB", false], - "RCU": ["Area De Material Airport", "Rio Cuarto", "AR", false], - "RCY": ["Rum Cay International Airport", "Port Nelson", "BS", false], - "RDA": ["Rockhampton Downs Airport", "Rockhampton Downs", "AU", false], - "RDB": ["Red Dog Airport", "Red Dog", "US", false], - "RDC": ["Redenção Airport", "Redenção", "BR", false], - "RDD": ["Redding Municipal Airport", "Redding", "US", true], - "RDE": ["Merdey Airport", "Merdei-Papua Island", "ID", false], - "RDG": ["Reading Regional Airport (Carl A Spaatz Field)", "Reading", "US", false], - "RDL": ["Bardawil International Airport", "El Hassana", "EG", false], - "RDM": ["Roberts Field", "Redmond", "US", true], - "RDN": ["LTS Pulau Redang Airport", "Redang", "MY", false], - "RDO": ["Warsaw Radom Airport", "Radom", "PL", true], - "RDP": ["Kazi Nazrul Islam Airport", "Durgapur", "IN", true], - "RDR": ["Grand Forks Air Force Base", "Grand Forks", "US", false], - "RDS": ["Rincon De Los Sauces Airport", "Rincon de los Sauces", "AR", false], - "RDT": ["Richard Toll Airport", "Richard Toll", "SN", false], - "RDU": ["Raleigh-Durham International Airport", "Raleigh/Durham", "US", true], - "RDV": ["Red Devil Airport", "Red Devil", "US", true], - "RDZ": ["Rodez–Aveyron Airport", "Rodez/Marcillac", "FR", true], - "REA": ["Reao Airport", "Reao", "PF", false], - "REB": ["Müritz Airpark", "Lärz", "DE", false], - "REC": ["Recife/Guararapes - Gilberto Freyre International Airport", "Recife", "BR", true], - "RED": ["Mifflin County Airport", "Reedsville", "US", false], - "REE": ["Reese Airpark", "Lubbock", "US", false], - "REG": ["Reggio Calabria Airport", "Reggio Calabria", "IT", true], - "REI": ["Régina Airport", "Régina", "GF", false], - "REL": ["Almirante Marco Andres Zar Airport", "Rawson", "AR", true], - "REN": ["Orenburg Central Airport", "Orenburg", "RU", true], - "REO": ["Rome State Airport", "Rome", "US", false], - "REQ": ["Reko Diq Airport", "Chagai", "PK", false], - "RER": ["Retalhuleu Airport", "Retalhuleu", "GT", true], - "RES": ["Resistencia International Airport", "Resistencia", "AR", true], - "RET": ["Røst Airport", "Røst", "NO", true], - "REU": ["Reus Airport", "Reus", "ES", true], - "REW": ["Rewa Airport, Chorhata, REWA", "Rewa", "IN", true], - "REX": ["General Lucio Blanco International Airport", "Reynosa", "MX", true], - "REY": ["Reyes Airport", "Reyes", "BO", false], - "REZ": ["Resende Airport", "Resende", "BR", false], - "RFA": ["Rafaï Airport", "Rafaï", "CF", false], - "RFD": ["Chicago Rockford International Airport", "Chicago/Rockford", "US", true], - "RFG": ["Rooke Field", "Refugio", "US", false], - "RFK": ["Rollang Field", "Rolling Fork", "US", false], - "RFN": ["Raufarhöfn Airport", "Raufarhöfn", "IS", false], - "RFP": ["Raiatea Airport", "Uturoa", "PF", true], - "RFR": ["Rio Frio / Progreso Airport", "Rio Frio / Progreso", "CR", false], - "RFS": ["Rosita Airport", "La Rosita", "NI", false], - "RGA": ["Gobernador Ramón Trejo Noel International Airport", "Rio Grande", "AR", true], - "RGH": ["Balurghat Airport", "Balurghat", "IN", false], - "RGI": ["Rangiroa Airport", "", "PF", true], - "RGK": ["Gorno-Altaysk Airport", "Gorno-Altaysk", "RU", false], - "RGL": ["Piloto Civil Norberto Fernández International Airport", "Rio Gallegos", "AR", true], - "RGN": ["Yangon International Airport", "Yangon", "MM", true], - "RGO": ["Orang (Chongjin) Airport", "Hoemun-ri", "KP", true], - "RGR": ["Ranger Municipal Airport", "Ranger", "US", false], - "RGS": ["Burgos Airport", "Burgos", "ES", false], - "RGT": ["Japura Airport", "Rengat-Sumatra Island", "ID", false], - "RHA": ["Reykhólar Airport", "Reykhólar", "IS", false], - "RHD": ["Termas de Río Hondo international Airport", "Termas de Río Hondo", "AR", true], - "RHG": ["Ruhengeri Airport", "Ruhengeri", "RW", false], - "RHI": ["Rhinelander Oneida County Airport", "Rhinelander", "US", true], - "RHL": ["Roy Hill Station Airport", "", "AU", false], - "RHN": ["Skorpion Mine Airport", "Rosh Pinah", "NA", false], - "RHO": ["Rhodes International Airport \"Diagoras\"", "Rhodes", "GR", true], - "RHP": ["Ramechhap Airport", "Ramechhap", "NP", false], - "RHT": ["Alxa Right Banner Badanjilin Airport", "Badanjilin", "CN", true], - "RHV": ["Reid-Hillview Airport of Santa Clara County", "San Jose", "US", false], - "RIA": ["Santa Maria Airport", "Santa Maria", "BR", true], - "RIB": ["Capitán Av. Selin Zeitun Lopez Airport", "Riberalta", "BO", true], - "RIC": ["Richmond International Airport", "Richmond", "US", true], - "RID": ["Richmond Municipal Airport", "Richmond", "US", false], - "RIE": ["Rice Lake Regional Airport - Carl's Field", "Rice Lake", "US", false], - "RIF": ["Richfield Municipal Airport", "Richfield", "US", false], - "RIG": ["Rio Grande Regional Airport", "Rio Grande", "BR", false], - "RIH": ["Scarlett Martinez International Airport", "Río Hato", "PA", true], - "RIJ": ["Juan Simons Vela Airport", "Rioja", "PE", false], - "RIL": ["Garfield County Regional Airport", "Rifle", "US", false], - "RIM": ["San Nicolas Airport", "Rodriguez de Mendoza", "PE", false], - "RIN": ["Ringi Cove Airport", "Ringi Cove", "SB", false], - "RIR": ["Flabob Airport", "Riverside", "US", false], - "RIS": ["Rishiri Airport", "Rishiri", "JP", true], - "RIV": ["March Air Reserve Base", "Riverside", "US", false], - "RIW": ["Central Wyoming Regional Airport", "Riverton", "US", true], - "RIX": ["Riga International Airport", "Riga", "LV", true], - "RIY": ["Riyan International Airport", "Mukalla(Riyan)", "YE", true], - "RIZ": ["Rizhao Shanzihe Airport", "Rizhao (Donggang)", "CN", true], - "RJA": ["Rajahmundry Airport", "Madhurapudi", "IN", true], - "RJB": ["Rajbiraj Airport", "Rajbiraj", "NP", false], - "RJH": ["Shah Makhdum Airport", "Rajshahi", "BD", true], - "RJI": ["Rajouri Airport", "Rajouri", "IN", false], - "RJK": ["Rijeka Airport", "Rijeka(Omišalj)", "HR", true], - "RJL": ["Logroño-Agoncillo Airport", "Logroño", "ES", true], - "RJM": ["Marinda Airport", "Waisai", "ID", true], - "RJN": ["Rafsanjan Airport", "Rafsanjan", "IR", true], - "RKA": ["Aratika Nord Airport", "", "PF", false], - "RKD": ["Knox County Regional Airport", "Rockland", "US", true], - "RKE": ["Copenhagen Roskilde Airport", "Roskilde", "DK", true], - "RKH": ["Rock Hill - York County Airport", "Rock Hill", "US", false], - "RKI": ["Mentawai Airport", "Sipura Island", "ID", true], - "RKP": ["Aransas County Airport", "Rockport", "US", false], - "RKR": ["Robert S Kerr Airport", "Poteau", "US", false], - "RKS": ["Southwest Wyoming Regional Airport", "Rock Springs", "US", true], - "RKT": ["Ras Al Khaimah International Airport", "Ras Al Khaimah", "AE", true], - "RKU": ["Kairuku Airport", "Yule Island", "PG", false], - "RKV": ["Reykjavík Domestic Airport", "Reykjavík", "IS", true], - "RKW": ["Rockwood Municipal Airport", "Rockwood", "US", false], - "RKY": ["Rokeby Airport", "Rokeby", "AU", false], - "RKZ": ["Xigaze Peace Airport / Shigatse Air Base", "Xigazê (Samzhubzê)", "CN", true], - "RLA": ["Rolla Downtown Airport", "Rolla", "US", false], - "RLD": ["Richland Airport", "Richland", "US", false], - "RLG": ["Rostock-Laage Airport", "Laage", "DE", true], - "RLK": ["Bayannur Tianjitai Airport", "Bayannur", "CN", true], - "RLO": ["Valle Del Conlara International Airport", "Merlo", "AR", false], - "RLP": ["Rosella Plains Airport", "Rosella Plains", "AU", false], - "RLR": ["Relais de la Reine Airport", "Isalo", "MG", false], - "RLT": ["Arlit Airport", "Arlit", "NE", false], - "RMA": ["Roma Airport", "Roma", "AU", true], - "RMB": ["Buraimi Airport", "Buraimi", "OM", false], - "RMD": ["Basanth Nagar Airport", "Ramagundam", "IN", false], - "RME": ["Griffiss International Airport", "Rome", "US", false], - "RMF": ["Marsa Alam International Airport", "Marsa Alam", "EG", true], - "RMG": ["Richard B Russell Airport", "Rome", "US", false], - "RMI": ["Federico Fellini International Airport", "Rimini (RN)", "IT", true], - "RMK": ["Renmark Airport", "", "AU", false], - "RML": ["Colombo Ratmalana International Airport", "Colombo", "LK", true], - "RMN": ["Rumginae Airport", "", "PG", false], - "RMO": ["Chişinău International Airport", "Chişinău", "MD", true], - "RMP": ["Rampart Airport", "Rampart", "US", true], - "RMQ": ["Taichung International Airport / Ching Chuang Kang Air Base", "Taichung (Qingshui)", "TW", true], - "RMS": ["Ramstein Air Base", "Ramstein-Miesenbach", "DE", false], - "RMT": ["Rimatara Airport", "Rimatara Island", "PF", true], - "RMU": ["Region of Murcia International Airport", "Corvera", "ES", true], - "RMY": ["Mariposa Yosemite Airport", "Mariposa", "US", false], - "RMZ": ["Tobolsk Remezov Airport", "Tobolsk", "RU", true], - "RNA": ["Ulawa Airport", "Arona", "SB", false], - "RNB": ["Ronneby Airport", "Ronneby", "SE", true], - "RNC": ["Warren County Memorial Airport", "Mc Minnville", "US", false], - "RND": ["Randolph Air Force Base", "Universal City", "US", false], - "RNE": ["Roanne-Renaison Airport", "Saint-Léger-sur-Roanne", "FR", false], - "RNH": ["New Richmond Regional Airport", "New Richmond", "US", false], - "RNI": ["Corn Island Airport", "Corn Island", "NI", true], - "RNJ": ["Yoron Airport", "Yoron", "JP", true], - "RNL": ["Rennell/Tingoa Airport", "Rennell Island", "SB", true], - "RNM": ["Qarn Alam Airport", "Ghaba", "OM", false], - "RNN": ["Bornholm Airport", "Rønne", "DK", true], - "RNO": ["Reno Tahoe International Airport", "Reno", "US", true], - "RNP": ["Rongelap Island Airport", "Rongelap Island", "MH", true], - "RNS": ["Rennes-Saint-Jacques Airport", "Saint-Jacques-de-la-Lande, Ille-et-Vilaine", "FR", true], - "RNT": ["Renton Municipal Airport", "Renton", "US", false], - "RNU": ["Ranau Airport", "Ranau", "MY", false], - "RNZ": ["Jasper County Airport", "Rensselaer", "US", false], - "ROA": ["Roanoke–Blacksburg Regional Airport", "Roanoke", "US", true], - "ROB": ["Roberts International Airport", "Monrovia", "LR", true], - "ROC": ["Frederick Douglass Greater Rochester International Airport", "Rochester", "US", true], - "ROD": ["Robertson Airport", "Robertson", "ZA", false], - "ROF": ["Montague-Yreka Rohrer Field", "Montague", "US", false], - "ROG": ["Rogers Municipal Airport-Carter Field", "Rogers", "US", false], - "ROH": ["Robinhood Airport", "", "AU", false], - "ROI": ["Roi Et Airport", "Roi Et", "TH", true], - "ROK": ["Rockhampton Airport", "Rockhampton", "AU", true], - "ROL": ["Roosevelt Municipal Airport", "Roosevelt", "US", false], - "RON": ["Juan Jose Rondon Airport", "Paipa", "CO", false], - "ROO": ["Maestro Marinho Franco Airport", "Rondonópolis", "BR", true], - "ROP": ["Rota International Airport", "Rota Island", "MP", true], - "ROR": ["Roman Tmetuchl International Airport", "Babelthuap Island", "PW", true], - "ROS": ["Rosario Islas Malvinas International Airport", "Rosario", "AR", true], - "ROT": ["Rotorua Regional Airport", "Rotorua", "NZ", true], - "ROU": ["Ruse Airport", "Shtraklevo", "BG", false], - "ROV": ["Platov International Airport", "Rostov-on-Don", "RU", false], - "ROW": ["Roswell Air Center Airport", "Roswell", "US", true], - "ROX": ["Roseau Municipal Rudy Billberg Field", "Roseau", "US", false], - "ROY": ["Rio Mayo Airport", "Rio Mayo", "AR", false], - "ROZ": ["Rota Naval Station Airport", "Rota", "ES", false], - "RPA": ["Rolpa Airport", "Rolpa", "NP", false], - "RPB": ["Roper Bar Airport", "Roper Bar", "AU", false], - "RPM": ["Ngukurr Airport", "Roper River", "AU", false], - "RPN": ["Rosh Pina Airport", "Rosh Pina", "IL", false], - "RPR": ["Swami Vivekananda Airport", "Raipur", "IN", true], - "RPU": ["Urucu Airport", "Urucu", "BR", false], - "RPV": ["Roper Valley Airport", "Roper Valley", "AU", false], - "RPX": ["Roundup Airport", "Roundup", "US", false], - "RQA": ["Ruoqiang Loulan Airport", "Ruoqiang Town", "CN", true], - "RQO": ["El Reno Regional Airport", "El Reno", "US", false], - "RQW": ["Qayyarah West Airport", "Qayyarah", "IQ", false], - "RQY": ["Rashtrakavi Kuvempu Airport", "Shimoga", "IN", false], - "RRE": ["Marree Airport", "", "AU", false], - "RRG": ["Sir Charles Gaetan Duval Airport", "Port Mathurin", "MU", true], - "RRJ": ["Jacarepaguá - Roberto Marinho Airport", "Rio de Janeiro", "BR", false], - "RRK": ["Rourkela Airport", "Rourkela", "IN", false], - "RRL": ["Merrill Municipal Airport", "Merrill", "US", false], - "RRM": ["Marromeu Airport", "Marromeu", "MZ", false], - "RRR": ["Raroia Airport", "Raroia", "PF", true], - "RRS": ["Røros Airport", "Røros", "NO", true], - "RRT": ["Warroad International Memorial Airport", "Warroad", "US", false], - "RRV": ["Robinson River Airport", "", "AU", false], - "RSA": ["Santa Rosa Airport", "Santa Rosa", "AR", true], - "RSB": ["Roseberth Airport", "", "AU", false], - "RSD": ["Rock Sound International Airport", "Rock Sound", "BS", true], - "RSE": ["Rose Bay Seaplane Base", "Sydney", "AU", false], - "RSH": ["Russian Mission Airport", "Russian Mission", "US", true], - "RSI": ["Red Sea International Airport", "Hanak", "SA", true], - "RSJ": ["Rosario Seaplane Base", "Rosario", "US", true], - "RSK": ["Abresso Airport", "Ransiki-Papua Island", "ID", false], - "RSL": ["Russell Municipal Airport", "Russell", "US", false], - "RSN": ["Ruston Regional Airport", "Ruston", "US", false], - "RSS": ["Damazin Airport", "Ad Damazin", "SD", false], - "RST": ["Rochester International Airport", "Rochester", "US", true], - "RSU": ["Yeosu Airport", "Yeosu", "KR", true], - "RSW": ["Southwest Florida International Airport", "Fort Myers", "US", true], - "RSX": ["Rouses Point Seaplane Base", "Rouses Point", "US", false], - "RTA": ["Rotuma Airport", "Rotuma", "FJ", true], - "RTB": ["Juan Manuel Gálvez International Airport", "Coxen Hole", "HN", true], - "RTC": ["Ratnagiri Airport", "", "IN", false], - "RTE": ["Campo de Marte Airport", "São Paulo", "BR", false], - "RTG": ["Frans Sales Lega Airport", "Satar Tacik, Manggarai", "ID", true], - "RTI": ["David Constantijn Saudale Airport", "Ba'a - Rote Island", "ID", true], - "RTL": ["Spirit Lake Municipal Airport", "Okoboji", "US", false], - "RTM": ["Rotterdam The Hague Airport", "Rotterdam", "NL", true], - "RTN": ["Raton Municipal Airport / Crews Field", "Raton", "US", false], - "RTO": ["Budiarto Airport", "Tangerang-Java Island", "ID", false], - "RTP": ["Rutland Plains Airport", "Yagoonya", "AU", false], - "RTS": ["Rottnest Island Airport", "", "AU", false], - "RTU": ["Maratua Airport", "Maratua", "ID", false], - "RTY": ["Merty Merty Airport", "", "AU", false], - "RUA": ["Arua Airport", "Arua", "UG", true], - "RUD": ["Shahrud International Airport", "Shahrud", "IR", false], - "RUE": ["Rughenda Airfield", "Butembo", "CD", false], - "RUF": ["Yuruf Airport [UNUSABLE]", "Amgotro", "ID", false], - "RUG": ["Rugao Air Base", "Rugao (Nantong)", "CN", false], - "RUH": ["King Khalid International Airport", "Riyadh", "SA", true], - "RUI": ["Sierra Blanca Regional Airport", "Alto", "US", false], - "RUK": ["Rukum Chaurjahari Airport", "Rukumkot", "NP", false], - "RUL": ["Maavaarulaa Airport", "Maavaarulu", "MV", true], - "RUM": ["Rumjatar Airport", "Rumjatar", "NP", false], - "RUN": ["Roland Garros Airport", "Sainte-Marie", "RE", true], - "RUP": ["Rupsi Airport", "Rupsi", "IN", false], - "RUR": ["Rurutu Airport", "", "PF", true], - "RUS": ["Marau Airport", "Marau", "SB", true], - "RUT": ["Rutland - Southern Vermont Regional Airport", "Rutland", "US", true], - "RUU": ["Ruti Airport", "Kawbenaberi", "PG", false], - "RUV": ["Rubelsanto Airport", "Rubelsanto", "GT", false], - "RUY": ["Rioamarillo Airport", "Copán Ruinas", "HN", false], - "RVA": ["Farafangana Airport", "Farafangana", "MG", false], - "RVC": ["River Cess Airport and Heliport", "River Cess", "LR", false], - "RVD": ["General Leite de Castro Airport", "Rio Verde", "BR", false], - "RVE": ["Los Colonizadores Airport", "Saravena", "CO", true], - "RVK": ["Rørvik Airport, Ryum", "Rørvik", "NO", true], - "RVN": ["Rovaniemi Airport", "Rovaniemi", "FI", true], - "RVO": ["Reivilo Airport", "Reivilo", "ZA", false], - "RVR": ["Green River Municipal Airport", "Green River", "US", false], - "RVS": ["Tulsa Riverside Airport", "Tulsa", "US", false], - "RVT": ["Ravensthorpe Airport", "", "AU", false], - "RVV": ["Raivavae Airport", "", "PF", true], - "RVY": ["Pres. Gral. Óscar D. Gestido Binational Airport", "Rivera/Santana do Livramento", "UY", true], - "RWF": ["Redwood Falls Municipal Airport", "Redwood Falls", "US", false], - "RWI": ["Rocky Mount Wilson Regional Airport", "Rocky Mount", "US", false], - "RWL": ["Rawlins Municipal Airport/Harvey Field", "Rawlins", "US", false], - "RWN": ["Rivne International Airport", "Rivne", "UA", false], - "RXA": ["Ar Rawdah Airport", "Ar Rawdah", "YE", false], - "RXE": ["Rexburg Madison County Airport", "Rexburg", "US", false], - "RXS": ["Roxas Airport", "Roxas City", "PH", true], - "RYB": ["Staroselye Airport", "Rybinsk", "RU", false], - "RYK": ["Shaikh Zaid Airport", "Rahim Yar Khan", "PK", true], - "RYL": ["Royal Zambezi Lodge Airstrip", "Lower Zambezi River", "ZM", false], - "RYN": ["Royan-Médis Airport", "Royan/Médis", "FR", false], - "RYO": ["28 de Noviembre Airport", "Rio Turbio", "AR", true], - "RZA": ["Santa Cruz Airport", "Puerto Santa Cruz", "AR", false], - "RZE": ["Rzeszów-Jasionka Airport", "Jasionka", "PL", true], - "RZH": ["Preobrazheniye Airport", "Preobrazheniye", "RU", false], - "RZP": ["Cesar Lim Rodriguez (Taytay-Sandoval) Airport", "Taytay", "PH", false], - "RZR": ["Ramsar Airport", "Ramsar", "IR", true], - "RZS": ["Sawan Airport", "Sawan Gas Field", "PK", false], - "RZV": ["Rize–Artvin Airport", "Rize", "TR", true], - "SAA": ["Shively Field", "Saratoga", "US", false], - "SAB": ["Juancho E. Yrausquin Airport", "Zion's Hill", "BQ", true], - "SAC": ["Sacramento Executive Airport", "Sacramento", "US", false], - "SAD": ["Safford Regional Airport", "Safford", "US", false], - "SAE": ["Saattut Heliport", "Saattut", "GL", true], - "SAF": ["Santa Fe Municipal Airport", "Santa Fe", "US", true], - "SAG": ["Shirdi International Airport", "Kakadi", "IN", true], - "SAH": ["Sanaa International Airport", "Sanaa", "YE", true], - "SAI": ["Siem Reap-Angkor International Airport", "Siem Reap", "KH", true], - "SAK": ["Sauðárkrókur Airport", "Sauðárkrókur", "IS", false], - "SAL": ["El Salvador International Airport Saint Óscar Arnulfo Romero y Galdámez", "San Salvador (San Luis Talpa)", "SV", true], - "SAM": ["Salamo Airport", "Salamo", "PG", false], - "SAN": ["San Diego International Airport", "San Diego", "US", true], - "SAP": ["Ramón Villeda Morales International Airport", "San Pedro Sula", "HN", true], - "SAQ": ["San Andros Airport", "Andros Island", "BS", true], - "SAR": ["Sparta Community Airport - Hunter Field", "Sparta", "US", false], - "SAS": ["Salton Sea Airport", "Salton City", "US", false], - "SAT": ["San Antonio International Airport", "San Antonio", "US", true], - "SAU": ["Sabu-Tardanu Airport", "Sabu-Sawu Island", "ID", false], - "SAV": ["Savannah Hilton Head International Airport", "Savannah", "US", true], - "SAW": ["Istanbul Sabiha Gökçen International Airport", "Pendik, Istanbul", "TR", true], - "SAX": ["Sambú Airport", "Boca de Sábalo", "PA", false], - "SAZ": ["Sasstown Airport", "Sasstown", "LR", false], - "SBA": ["Santa Barbara Municipal Airport", "Santa Barbara", "US", true], - "SBB": ["Santa Bárbara de Barinas Airport", "Santa Bárbara", "VE", false], - "SBC": ["Selbang Airport", "Selbang", "PG", false], - "SBD": ["San Bernardino International Airport", "San Bernardino", "US", true], - "SBE": ["Suabi Airport", "", "PG", false], - "SBF": ["Sardeh Band Airport", "Sardeh Band", "AF", false], - "SBG": ["Maimun Saleh Airport", "Sabang", "ID", false], - "SBH": ["St. Jean Airport", "Gustavia", "BL", true], - "SBI": ["Sambailo Airport", "Koundara", "GN", false], - "SBJ": ["São Mateus - Ernesto Bonomo Airport", "São Mateus", "BR", false], - "SBK": ["Saint-Brieuc-Armor Airport", "Trémuson, Côtes-d'Armor", "FR", false], - "SBL": ["Santa Ana Del Yacuma Airport", "Santa Ana del Yacuma", "BO", false], - "SBM": ["Sheboygan County Memorial Airport", "Sheboygan", "US", false], - "SBN": ["South Bend International Airport", "South Bend", "US", true], - "SBO": ["Salina Gunnison Airport", "Salina", "US", false], - "SBP": ["San Luis County Regional Airport", "San Luis Obispo", "US", true], - "SBQ": ["Sibi Airport", "Sibi", "PK", false], - "SBR": ["Saibai Island Airport", "Saibai Island", "AU", true], - "SBS": ["Steamboat Springs Bob Adams Field", "Steamboat Springs", "US", false], - "SBT": ["Sabetta International Airport", "Sabetta", "RU", true], - "SBU": ["Springbok Airport", "Springbok", "ZA", false], - "SBV": ["Sabah Airport", "Sabah", "PG", false], - "SBW": ["Sibu Airport", "Sibu", "MY", true], - "SBX": ["Shelby Airport", "Shelby", "US", false], - "SBY": ["Salisbury Ocean City Wicomico Regional Airport", "Salisbury", "US", true], - "SBZ": ["Sibiu International Airport", "Sibiu", "RO", true], - "SCA": ["Santa Catalina Airport", "Santa Catalina", "CO", false], - "SCB": ["Scribner State Airport", "Scribner", "US", false], - "SCC": ["Deadhorse Airport", "Deadhorse", "US", true], - "SCD": ["Sulaco Airport", "Sulaco", "HN", false], - "SCE": ["State College Regional Airport", "State College", "US", true], - "SCF": ["Scottsdale Airport", "Scottsdale", "US", false], - "SCG": ["Spring Creek Airport", "", "AU", false], - "SCH": ["Schenectady County Airport", "Schenectady", "US", false], - "SCI": ["Paramillo Airport", "San Cristóbal", "VE", false], - "SCK": ["Stockton Metropolitan Airport", "Stockton", "US", true], - "SCL": ["Comodoro Arturo Merino Benítez International Airport", "Santiago", "CL", true], - "SCM": ["Scammon Bay Airport", "Scammon Bay", "US", true], - "SCN": ["Saarbrücken Airport", "Saarbrücken", "DE", true], - "SCO": ["Aktau International Airport", "Aktau", "KZ", true], - "SCP": ["Mont-Dauphin Saint-Crépin Airfield", "Saint-Crépin, Hautes-Alpes", "FR", false], - "SCQ": ["Santiago-Rosalía de Castro Airport", "Santiago de Compostela", "ES", true], - "SCR": ["Scandinavian Mountains Airport", "Malung-Sälen", "SE", true], - "SCT": ["Socotra Airport", "Mori", "YE", true], - "SCU": ["Antonio Maceo International Airport", "Santiago", "CU", true], - "SCV": ["Suceava Ștefan cel Mare International Airport", "Suceava", "RO", true], - "SCW": ["Syktyvkar Airport", "Syktyvkar", "RU", true], - "SCX": ["Estación Aeronaval de Salina Cruz", "Salina Cruz", "MX", false], - "SCY": ["San Cristóbal Airport", "Puerto Baquerizo Moreno", "EC", true], - "SCZ": ["Santa Cruz/Graciosa Bay/Luova Airport", "Santa Cruz/Graciosa Bay/Luova", "SB", true], - "SDB": ["Langebaanweg Airport", "Langebaanweg", "ZA", false], - "SDC": ["Sand Creek Airport", "Sand Creek", "GY", false], - "SDD": ["Lubango Mukanka International Airport", "Lubango", "AO", true], - "SDE": ["Vicecomodoro Angel D. La Paz Aragonés Airport", "Santiago del Estero", "AR", true], - "SDF": ["Louisville Muhammad Ali International Airport", "Louisville", "US", true], - "SDG": ["Sanandaj Airport", "", "IR", true], - "SDI": ["Saidor Airport", "Saidor", "PG", false], - "SDJ": ["Sendai Airport", "Natori", "JP", true], - "SDK": ["Sandakan Airport", "Sandakan", "MY", true], - "SDL": ["Sundsvall-Härnösand Airport", "Sundsvall/ Härnösand", "SE", true], - "SDM": ["Brown Field Municipal Airport", "San Diego", "US", false], - "SDN": ["Sandane Airport, Anda", "Sandane", "NO", true], - "SDP": ["Sand Point Airport", "Sand Point", "US", true], - "SDQ": ["Las Américas International Airport", "Santo Domingo", "DO", true], - "SDR": ["Seve Ballesteros-Santander Airport", "Santander", "ES", true], - "SDS": ["Sado Airport", "Sado", "JP", true], - "SDT": ["Saidu Sharif Airport", "Saidu Sharif", "PK", false], - "SDU": ["Santos Dumont Airport", "Rio de Janeiro", "BR", true], - "SDW": ["Sindhudurg Airport", "Chipi", "IN", true], - "SDX": ["Sedona Airport", "Sedona", "US", false], - "SDY": ["Sidney - Richland Regional Airport", "Sidney", "US", true], - "SEA": ["Seattle–Tacoma International Airport", "Seattle", "US", true], - "SEB": ["Sabha Airport", "Sabha", "LY", true], - "SEE": ["Gillespie Field", "San Diego/El Cajon", "US", false], - "SEF": ["Sebring Regional Airport", "Sebring", "US", false], - "SEG": ["Penn Valley Airport", "Selinsgrove", "US", false], - "SEH": ["Senggeh Airport", "Senggeh", "ID", false], - "SEK": ["Srednekolymsk Airport", "Srednekolymsk", "RU", true], - "SEM": ["Craig Field", "Selma", "US", false], - "SEN": ["London Southend Airport", "Southend-on-Sea, Essex", "GB", true], - "SEO": ["Séguéla Airport", "Séguéla", "CI", false], - "SEP": ["Stephenville Clark Regional Airport", "Stephenville", "US", false], - "SEQ": ["Sungai Pakning Bengkalis Airport", "Bengkalis-Sumatra Island", "ID", false], - "SER": ["Freeman Municipal Airport", "Seymour", "US", false], - "SES": ["Svetlogorsk Airport", "Svetlogorsk", "RU", false], - "SET": ["Santa Magalhães Airport", "Serra Talhada", "BR", true], - "SEU": ["Seronera Airport", "Seronera", "TZ", true], - "SEW": ["Siwa Oasis North Airport", "Siwa Oasis", "EG", false], - "SEY": ["Sélibaby Airport", "Sélibaby", "MR", false], - "SEZ": ["Seychelles International Airport", "Victoria", "SC", true], - "SFA": ["Sfax Thyna International Airport", "Sfax", "TN", true], - "SFB": ["Orlando Sanford International Airport", "Orlando", "US", true], - "SFC": ["St-François Airport", "St-François", "GP", true], - "SFD": ["San Fernando de Apure Las Flecheras National Airport", "San Fernando de Apure", "VE", false], - "SFE": ["San Fernando Airport", "San Fernando", "PH", false], - "SFF": ["Felts Field", "Spokane", "US", false], - "SFG": ["Grand Case-l'Espérance Airport", "Grand Case", "MF", true], - "SFH": ["San Felipe International Airport", "Mexicali", "MX", false], - "SFJ": ["Kangerlussuaq International Airport", "Kangerlussuaq", "GL", true], - "SFK": ["Soure Airport", "Soure", "BR", false], - "SFL": ["São Filipe Airport", "São Filipe", "CV", true], - "SFM": ["Sanford Seacoast Regional Airport", "Sanford", "US", false], - "SFN": ["Sauce Viejo Airport", "Santa Fe", "AR", true], - "SFO": ["San Francisco International Airport", "San Francisco", "US", true], - "SFS": ["Subic Bay International Airport / Naval Air Station Cubi Point", "Olongapo", "PH", true], - "SFT": ["Skellefteå Airport", "Skellefteå", "SE", true], - "SFU": ["Safia Airport", "Safia", "PG", false], - "SFV": ["Santa Fé do Sul Airport", "Santa Fé do Sul", "BR", false], - "SFX": ["Macagua Airport", "Ciudad Guayana", "VE", false], - "SFZ": ["North Central State Airport", "Pawtucket", "US", false], - "SGA": ["Sheghnan Airport", "Shiveh", "AF", false], - "SGC": ["Surgut International Airport", "Surgut", "RU", true], - "SGD": ["Sønderborg Airport", "Sønderborg", "DK", true], - "SGE": ["Siegerland Airport", "Burbach", "DE", false], - "SGF": ["Springfield Branson National Airport", "Springfield", "US", true], - "SGG": ["Sermiligaaq Heliport", "Sermiligaaq", "GL", true], - "SGH": ["Springfield-Beckley Municipal Airport", "Springfield", "US", false], - "SGI": ["Mushaf Air Base", "Sargodha", "PK", false], - "SGK": ["Sengapi Airstrip", "Sengapi", "PG", false], - "SGL": ["Danilo Atienza Air Base", "Cavite", "PH", false], - "SGM": ["San Ignacio Airport", "Mulegé", "MX", false], - "SGN": ["Tan Son Nhat International Airport", "Ho Chi Minh City", "VN", true], - "SGO": ["St George Airport", "", "AU", true], - "SGP": ["Shay Gap Airport", "Shay Gap", "AU", false], - "SGQ": ["Sangkimah Airport", "Sanggata", "ID", false], - "SGR": ["Sugar Land Regional Airport", "Houston", "US", false], - "SGT": ["Stuttgart Municipal Airport / Carl Humphrey Field", "Stuttgart", "US", false], - "SGU": ["St George Regional Airport", "St George", "US", true], - "SGV": ["Sierra Grande Airport", "Sierra Grande", "AR", false], - "SGW": ["Saginaw Seaplane Base", "Saginaw Bay", "US", false], - "SGX": ["Songea Airport", "Songea", "TZ", false], - "SGY": ["Skagway Airport", "Skagway", "US", true], - "SGZ": ["Songkhla Airport", "", "TH", false], - "SHA": ["Shanghai Hongqiao International Airport", "Shanghai (Minhang)", "CN", true], - "SHB": ["Nakashibetsu Airport", "Nakashibetsu", "JP", true], - "SHC": ["Shire Inda Selassie Airport", "Shire Inda Selassie", "ET", true], - "SHD": ["Shenandoah Valley Regional Airport", "Weyers Cave", "US", true], - "SHE": ["Shenyang Taoxian International Airport", "Shenyang", "CN", true], - "SHF": ["Shihezi Huayuan Airport", "Shihezi", "CN", true], - "SHG": ["Shungnak Airport", "Shungnak", "US", true], - "SHH": ["Shishmaref Airport", "Shishmaref", "US", true], - "SHI": ["Shimojishima Airport", "Miyakojima", "JP", true], - "SHJ": ["Sharjah International Airport", "Sharjah", "AE", true], - "SHK": ["Sehonghong Airport", "Sehonghong", "LS", false], - "SHL": ["Shillong Airport", "Shillong", "IN", true], - "SHM": ["Nanki Shirahama Airport", "Shirahama", "JP", true], - "SHN": ["Sanderson Field", "Shelton", "US", false], - "SHO": ["King Mswati III International Airport", "Mpaka", "SZ", true], - "SHQ": ["Southport Airport", "", "AU", false], - "SHR": ["Sheridan County Airport", "Sheridan", "US", true], - "SHS": ["Jingzhou Shashi Airport", "Jingzhou (Shashi)", "CN", true], - "SHT": ["Shepparton Airport", "", "AU", false], - "SHU": ["Smith Point Airport", "", "AU", false], - "SHV": ["Shreveport Regional Airport", "Shreveport", "US", true], - "SHW": ["Sharurah Domestic Airport", "Sharurah", "SA", true], - "SHX": ["Shageluk Airport", "Shageluk", "US", false], - "SHY": ["Shinyanga Airport", "Shinyanga", "TZ", true], - "SHZ": ["Seshutes Airport", "Seshutes", "LS", false], - "SIA": ["Xi'an Xiguan Airport", "Xi'an (Baqiao)", "CN", false], - "SIB": ["Sibiti Airport", "Sibiti", "CG", false], - "SIC": ["San José Island Airport", "Las Perlas", "PA", false], - "SID": ["Amílcar Cabral International Airport", "Espargos", "CV", true], - "SIF": ["Simara Airport", "Simara", "NP", true], - "SIG": ["Fernando Luis Ribas Dominicci Airport", "San Juan", "PR", true], - "SIH": ["Silgadi Doti Airport", "Silgadi Doti", "NP", true], - "SIJ": ["Siglufjörður Airport", "Siglufjörður", "IS", false], - "SIK": ["Sikeston Memorial Municipal Airport", "Sikeston", "US", false], - "SIL": ["Sila Airport", "Sila Mission", "PG", false], - "SIM": ["Simbai Airport", "Simbai", "PG", false], - "SIN": ["Singapore Changi Airport", "Singapore", "SG", true], - "SIO": ["Smithton Airport", "", "AU", false], - "SIP": ["Simferopol International Airport", "Simferopol", "UA", false], - "SIQ": ["Dabo Airport", "Pasirkuning-Singkep Island", "ID", false], - "SIR": ["Sion Airport", "Sion", "CH", false], - "SIS": ["Sishen Airport", "Sishen", "ZA", true], - "SIT": ["Sitka Rocky Gutierrez Airport", "Sitka", "US", true], - "SIU": ["Siuna", "Siuna", "NI", false], - "SIV": ["Sullivan County Airport", "Sullivan", "US", false], - "SIW": ["Sibisa Airport", "Parapat-Sumatra Island", "ID", false], - "SIX": ["Singleton Airport", "Singleton", "AU", false], - "SIY": ["Siskiyou County Airport", "Montague", "US", false], - "SIZ": ["Sissano Airport", "Sissano", "PG", false], - "SJA": ["San Juan de Marcona Airport", "San Juan de Marcona", "PE", false], - "SJB": ["San Joaquín Airport", "San Joaquín", "BO", false], - "SJC": ["Mineta San Jose International Airport", "San Jose", "US", true], - "SJD": ["Los Cabos International Airport", "San José del Cabo", "MX", true], - "SJE": ["Jorge E. Gonzalez Torres Airport", "San José Del Guaviare", "CO", true], - "SJF": ["Cruz Bay Seaplane Base", "Cruz Bay", "VI", false], - "SJH": ["San Juan Del César Airport", "San Juan Del César", "CO", false], - "SJI": ["San Jose Airport", "San Jose", "PH", true], - "SJJ": ["Sarajevo International Airport", "Sarajevo", "BA", true], - "SJK": ["Professor Urbano Ernesto Stumpf Airport", "São José Dos Campos", "BR", true], - "SJL": ["São Gabriel da Cachoeira Airport", "São Gabriel da Cachoeira", "BR", true], - "SJN": ["St Johns Industrial Air Park", "St Johns", "US", false], - "SJO": ["Juan Santamaría International Airport", "San José (Alajuela)", "CR", true], - "SJP": ["Prof. Eribelto Manoel Reino State Airport", "São José do Rio Preto", "BR", true], - "SJQ": ["Sesheke Airport", "Sesheke", "ZM", false], - "SJR": ["San Juan De Uraba Airport", "San Juan De Uraba", "CO", false], - "SJS": ["San José De Chiquitos Airport", "San José de Chiquitos", "BO", false], - "SJT": ["San Angelo Regional Mathis Field", "San Angelo", "US", true], - "SJU": ["Luis Munoz Marin International Airport", "San Juan", "PR", true], - "SJV": ["San Javier Airport", "San Javier", "BO", false], - "SJW": ["Shijiazhuang Zhengding International Airport", "Shijiazhuang", "CN", true], - "SJX": ["Sartaneja Airport", "Sartaneja", "BZ", false], - "SJY": ["Seinäjoki Airport", "Seinäjoki / Ilmajoki", "FI", false], - "SJZ": ["São Jorge Airport", "Velas", "PT", true], - "SKA": ["Fairchild Air Force Base", "Spokane", "US", false], - "SKB": ["Robert L. Bradshaw International Airport", "Basseterre", "KN", true], - "SKC": ["Suki Airport", "Suki", "PG", false], - "SKD": ["Samarkand International Airport", "Samarkand", "UZ", true], - "SKF": ["Lackland Air Force Base", "San Antonio", "US", false], - "SKG": ["Thessaloniki Macedonia International Airport", "Thessaloniki", "GR", true], - "SKH": ["Surkhet Airport", "Surkhet", "NP", true], - "SKJ": ["Singkawang Airport", "Singkawang", "ID", false], - "SKK": ["Shaktoolik Airport", "Shaktoolik", "US", true], - "SKL": ["Broadford Airstrip", "Ashaig, Highland", "GB", false], - "SKM": ["Skeldon Airport", "Skeldon", "GY", false], - "SKN": ["Stokmarknes Airport, Skagen", "Hadsel", "NO", true], - "SKO": ["Sadiq Abubakar III International Airport", "Sokoto", "NG", true], - "SKP": ["Skopje International Airport", "Ilinden", "MK", true], - "SKQ": ["Sekakes Airport", "Sekakes", "LS", false], - "SKR": ["Shakiso Airport", "Shakiso", "ET", false], - "SKS": ["Skrydstrup Air Base", "Vojens", "DK", false], - "SKT": ["Sialkot International Airport", "Sialkot", "PK", true], - "SKU": ["Skiros Airport", "Skiros Island", "GR", true], - "SKV": ["Saint Catherine International Airport", "Saint Catherine", "EG", false], - "SKW": ["Skwentna Airport", "Skwentna", "US", false], - "SKX": ["Saransk International Airport", "Saransk", "RU", true], - "SKZ": ["Begum Nusrat Bhutto International Airport Sukkur", "Sukkur", "PK", true], - "SLA": ["Martín Miguel de Güemes International Airport", "Salta", "AR", true], - "SLB": ["Storm Lake Municipal Airport", "Storm Lake", "US", false], - "SLC": ["Salt Lake City International Airport", "Salt Lake City", "US", true], - "SLD": ["Sliač Airport", "Sliač", "SK", true], - "SLE": ["Salem-Willamette Valley Airport/McNary Field", "Salem", "US", true], - "SLF": ["As-Sulayyil Airport", "As-Sulayyil", "SA", false], - "SLG": ["Smith Field", "Siloam Springs", "US", false], - "SLH": ["Sola Airport", "Sola", "VU", true], - "SLI": ["Solwesi Airport", "Solwesi", "ZM", true], - "SLJ": ["Solomon Airport", "Karijini National Park", "AU", false], - "SLK": ["Adirondack Regional Airport", "Saranac Lake", "US", true], - "SLL": ["Salalah International Airport", "Salalah", "OM", true], - "SLM": ["Salamanca Airport", "Salamanca", "ES", true], - "SLN": ["Salina Municipal Airport", "Salina", "US", true], - "SLO": ["Salem–Leckrone Airport", "Salem", "US", false], - "SLP": ["Ponciano Arriaga International Airport", "San Luis Potosí", "MX", true], - "SLQ": ["Sleetmute Airport", "Sleetmute", "US", true], - "SLR": ["Sulphur Springs Municipal Airport", "Sulphur Springs", "US", false], - "SLT": ["Salida Airport - Harriet Alexander Field", "Salida", "US", false], - "SLU": ["George F. L. Charles Airport", "Castries", "LC", true], - "SLV": ["Shimla Airport", "Jubbarhatti", "IN", false], - "SLW": ["Plan de Guadalupe International Airport", "Saltillo", "MX", true], - "SLX": ["Salt Cay Airport", "Salt Cay", "TC", true], - "SLY": ["Salekhard Airport", "Salekhard", "RU", true], - "SLZ": ["Marechal Cunha Machado International Airport", "São Luís", "BR", true], - "SMA": ["Santa Maria Airport", "Vila do Porto", "PT", true], - "SMB": ["Franco Bianco Airport", "Cerro Sombrero", "CL", false], - "SMD": ["Smith Field", "Fort Wayne", "US", false], - "SME": ["Lake Cumberland Regional Airport", "Somerset", "US", false], - "SMF": ["Sacramento International Airport", "Sacramento", "US", true], - "SMH": ["Sapmanga Airport", "Sapmanga", "PG", false], - "SMI": ["Samos Airport", "Samos Island", "GR", true], - "SMJ": ["Sim Airport", "Sim", "PG", false], - "SMK": ["St Michael Airport", "St Michael", "US", true], - "SML": ["Stella Maris Airport", "Stella Maris", "BS", true], - "SMM": ["Semporna Airport", "Semporna", "MY", false], - "SMN": ["Lemhi County Airport", "Salmon", "US", true], - "SMO": ["Santa Monica Municipal Airport", "Santa Monica", "US", false], - "SMP": ["Stockholm Landing Strip", "Stockholm", "PG", false], - "SMQ": ["Sampit (H.Asan) Airport", "Sampit", "ID", true], - "SMR": ["Simón Bolívar International Airport", "Santa Marta", "CO", true], - "SMS": ["Sainte Marie Airport", "Vohilava", "MG", true], - "SMT": ["Adolino Bedin Regional Airport", "Sorriso", "BR", true], - "SMU": ["Sheep Mountain Airport", "Sheep Mountain", "US", false], - "SMV": ["Engadin Airport", "Samedan", "CH", false], - "SMW": ["Smara Airport", "Smara", "EH", true], - "SMX": ["Santa Maria Public Airport Captain G Allan Hancock Field", "Santa Maria", "US", true], - "SMY": ["Simenti Airport", "Simenti", "SN", false], - "SMZ": ["Stoelmanseiland Airport", "Stoelmanseiland", "SR", false], - "SNA": ["John Wayne Orange County International Airport", "Santa Ana", "US", true], - "SNB": ["Snake Bay Airport", "Milikapiti", "AU", true], - "SNC": ["General Ulpiano Paez International Airport", "Salinas/La Libertad", "EC", true], - "SNE": ["Preguiça Airport", "Preguiça", "CV", true], - "SNF": ["Sub Teniente Nestor Arias Airport", "San Felipe", "VE", false], - "SNG": ["Capitán Av. Juan Cochamanidis S. Airport", "San Ignacio de Velasco", "BO", false], - "SNH": ["Stanthorpe Airport", "", "AU", false], - "SNI": ["Greenville/Sinoe Airport", "Greenville", "LR", false], - "SNJ": ["San Julián Air Base", "Sandino", "CU", false], - "SNK": ["Winston Field", "Snyder", "US", false], - "SNL": ["Shawnee Regional Airport", "Shawnee", "US", false], - "SNM": ["San Ignacio de Moxos Airport", "San Ignacio de Moxos", "BO", false], - "SNN": ["Shannon Airport", "Shannon", "IE", true], - "SNO": ["Sakon Nakhon Airport", "", "TH", true], - "SNP": ["St Paul Island Airport", "St Paul Island", "US", true], - "SNQ": ["San Quintín Military Airstrip", "San Quintín", "MX", false], - "SNR": ["Saint-Nazaire-Montoir Airport", "Saint-Nazaire/Montoir", "FR", false], - "SNS": ["Salinas Municipal Airport", "Salinas", "US", false], - "SNT": ["Las Cruces Airport", "Sabana de Torres", "CO", false], - "SNU": ["Abel Santamaria International Airport", "Santa Clara", "CU", true], - "SNV": ["Santa Elena de Uairén Airport", "Santa Elena de Uairén", "VE", true], - "SNW": ["Thandwe Airport", "Thandwe", "MM", true], - "SNX": ["Semnan Municipal Airport", "Semnan", "IR", true], - "SNY": ["Sidney Municipal Airport Lloyd W Carr Field", "Sidney", "US", false], - "SNZ": ["Santa Cruz Air Force Base", "Rio de Janeiro", "BR", false], - "SOA": ["Sóc Trăng Airport", "Sóc Trăng", "VN", false], - "SOB": ["Hévíz–Balaton Airport", "Sármellék", "HU", true], - "SOC": ["Adisoemarmo International Airport", "Surakarta", "ID", true], - "SOD": ["Sorocaba Airport", "Sorocaba", "BR", true], - "SOE": ["Souanke Airport", "Souanke", "CG", false], - "SOF": ["Sofia Airport", "Sofia", "BG", true], - "SOG": ["Sogndal Airport, Haukåsen", "Sogndal", "NO", true], - "SOH": ["Solita Airport", "Solita", "CO", false], - "SOI": ["South Molle Island Helipad", "South Molle Island Resort", "AU", false], - "SOJ": ["Sørkjosen Airport", "Sørkjosen", "NO", true], - "SOK": ["Semonkong Airport", "Semonkong", "LS", false], - "SOL": ["Solomon State Field", "Solomon", "US", false], - "SOM": ["San Tomé Airport", "El Tigre", "VE", true], - "SON": ["Santo Pekoa International Airport", "Luganville", "VU", true], - "SOO": ["Söderhamn Airport", "Söderhamn", "SE", false], - "SOP": ["Moore County Airport", "Carthage", "US", false], - "SOQ": ["Domine Eduard Osok Airport", "Sorong", "ID", true], - "SOR": ["Al Thaurah Airport", "T2", "SY", false], - "SOT": ["Sodankyla Airport", "Sodankyla", "FI", false], - "SOU": ["Southampton Airport", "Southampton", "GB", true], - "SOV": ["Seldovia Airport", "Seldovia", "US", true], - "SOW": ["Show Low Regional Airport", "Show Low", "US", true], - "SOX": ["Alberto Lleras Camargo Airport", "Sogamoso", "CO", false], - "SOY": ["Stronsay Airport", "Stronsay", "GB", true], - "SOZ": ["Solenzara (BA 126) Air Base", "Solenzara", "FR", false], - "SPA": ["Spartanburg Downtown Memorial Airport", "Spartanburg", "US", false], - "SPB": ["Charlotte Amalie Harbor Seaplane Base", "Charlotte Amalie", "VI", true], - "SPC": ["La Palma Airport", "Sta Cruz de la Palma, La Palma Island", "ES", true], - "SPD": ["Saidpur Airport", "Saidpur", "BD", true], - "SPE": ["Sepulot Airport", "Sepulot", "MY", false], - "SPF": ["Black Hills Airport-Clyde Ice Field", "Spearfish", "US", false], - "SPG": ["Albert Whitted Airport", "St Petersburg", "US", false], - "SPH": ["Sopu Airport", "Sopu", "PG", false], - "SPI": ["Abraham Lincoln Capital Airport", "Springfield", "US", true], - "SPJ": ["Sparti Airfield", "Sparti", "GR", false], - "SPM": ["Spangdahlem Air Base", "Trier", "DE", false], - "SPN": ["Saipan International Airport", "I Fadang, Saipan", "MP", true], - "SPP": ["Menongue Airport", "Menongue", "AO", true], - "SPQ": ["Catalina Air-Sea Terminal Heliport", "San Pedro", "US", false], - "SPR": ["John Greif II Airport", "San Pedro", "BZ", true], - "SPS": ["Wichita Falls Municipal Airport / Sheppard Air Force Base", "Wichita Falls", "US", true], - "SPT": ["Sipitang Airport", "Sipitang", "MY", false], - "SPU": ["Split Saint Jerome Airport", "Split", "HR", true], - "SPV": ["Sepik Plains Airport", "Sepik Plains", "PG", false], - "SPW": ["Spencer Municipal Airport", "Spencer", "US", false], - "SPX": ["Sphinx International Airport", "Al Jiza", "EG", true], - "SPY": ["San Pedro Airport", "", "CI", true], - "SPZ": ["Springdale Municipal Airport", "Springdale", "US", false], - "SQA": ["Santa Ynez Airport", "Santa Ynez", "US", false], - "SQB": ["Santa Ana Airport", "Piedras", "CO", false], - "SQC": ["Southern Cross Airport", "", "AU", false], - "SQD": ["Shangrao Sanqingshan Airport", "Shangrao (Hengfeng)", "CN", true], - "SQE": ["San Luis De Palenque Airport", "San Luis De Palenque", "CO", false], - "SQF": ["Solano Airport", "Solano", "CO", false], - "SQG": ["Tebelian Airport", "Sintang", "ID", true], - "SQH": ["Na San Airport", "Mai Son", "VN", false], - "SQI": ["Whiteside County Airport - Joseph H Bittorf Field", "Rock Falls", "US", false], - "SQJ": ["Sanming Shaxian Airport", "Sanming (Sha)", "CN", true], - "SQK": ["Sidi EL Barrani Airport", "Sidi El Barrani", "EG", false], - "SQL": ["San Carlos Airport", "San Carlos", "US", true], - "SQM": ["São Miguel do Araguaia Airport", "São Miguel do Araguaia", "BR", false], - "SQN": ["Emalamo Airport", "Sanana", "ID", false], - "SQO": ["Storuman Airport", "Storuman", "SE", false], - "SQQ": ["Šiauliai International Airport", "Šiauliai", "LT", false], - "SQR": ["Soroako Airport", "Soroako", "ID", false], - "SQS": ["Matthew Spain Airport", "Spanish Lookout", "BZ", false], - "SQT": ["China Strait Airstrip", "Samarai Island", "PG", false], - "SQU": ["Saposoa Airport", "Plaza Saposoa", "PE", false], - "SQV": ["Sequim Valley Airport", "Sequim", "US", false], - "SQW": ["Skive Airport", "Skive", "DK", false], - "SQX": ["São Miguel do Oeste Hélio Wasum Airport", "São Miguel do Oeste", "BR", false], - "SQY": ["São Lourenço do Sul Airport", "São Lourenço Do Sul", "BR", false], - "SRA": ["Luis Alberto Lehr Airport", "Santa Rosa", "BR", true], - "SRB": ["Santa Rosa de Yacuma Airport", "Santa Rosa", "BO", false], - "SRC": ["Searcy Municipal Airport", "Searcy", "US", false], - "SRD": ["San Ramón Airport", "San Ramón / Mamoré", "BO", false], - "SRE": ["Alcantarí International Airport", "Sucre", "BO", true], - "SRG": ["Jenderal Ahmad Yani Airport", "Semarang", "ID", true], - "SRH": ["Sarh Airport", "Sarh", "TD", false], - "SRJ": ["Capitán Av. German Quiroga G. Airport", "San Borja", "BO", false], - "SRK": ["Siorapaluk Heliport", "Siorapaluk", "GL", true], - "SRL": ["Palo Verde Airport", "Mulegé", "MX", true], - "SRM": ["Sandringham Airport", "Sandringham Station", "AU", false], - "SRN": ["Strahan Airport", "", "AU", false], - "SRO": ["Santana Ramos Airport", "Santana Ramos", "CO", false], - "SRP": ["Stord Airport, Sørstokken", "Leirvik", "NO", true], - "SRQ": ["Sarasota Bradenton International Airport", "Sarasota/Bradenton", "US", true], - "SRR": ["Dunwich Airport", "North Stradbroke Island", "AU", false], - "SRS": ["San Marcos Airport", "San Marcos", "CO", false], - "SRT": ["Soroti Airport", "Soroti", "UG", true], - "SRU": ["Surbung Airport", "Falam", "MM", false], - "SRV": ["Stony River 2 Airport", "Stony River", "US", true], - "SRW": ["Mid-Carolina Regional Airport", "Salisbury", "US", false], - "SRX": ["Sirt International Airport / Ghardabiya Airbase", "Sirt", "LY", false], - "SRY": ["Sari Dasht-e Naz International Airport", "Sari", "IR", true], - "SRZ": ["El Trompillo Airport", "Santa Cruz", "BO", true], - "SSA": ["Deputado Luiz Eduardo Magalhães International Airport", "Salvador", "BR", true], - "SSB": ["Christiansted Harbor Seaplane Base", "Christiansted", "VI", true], - "SSC": ["Shaw Air Force Base", "Sumter", "US", false], - "SSD": ["Víctor Lafón Airport", "San Felipe", "CL", false], - "SSE": ["Solapur Airport", "Solapur", "IN", false], - "SSF": ["Stinson Municipal Airport", "San Antonio", "US", false], - "SSG": ["Malabo International Airport", "Malabo", "GQ", true], - "SSH": ["Sharm El Sheikh International Airport", "Sharm El Sheikh", "EG", true], - "SSI": ["St Simons Island Airport", "St Simons Island", "US", false], - "SSJ": ["Sandnessjøen Airport, Stokka", "Alstahaug", "NO", true], - "SSK": ["Sturt Creek Airport", "Sturt Creek", "AU", false], - "SSL": ["Santa Rosalia Airport", "Santa Rosalia", "CO", false], - "SSN": ["Seoul Air Base (K-16)", "Seongnam", "KR", false], - "SSO": ["São Lourenço Airport", "São Lourenço", "BR", false], - "SSP": ["Silver Plains Airport", "Silver Plains", "AU", false], - "SSQ": ["La Sarre Airport", "La Sarre", "CA", false], - "SSR": ["Sara Airport", "Pentecost Island", "VU", true], - "SSS": ["Siassi Airport", "Siassi", "PG", false], - "SST": ["Santa Teresita Airport", "Santa Teresita", "AR", true], - "SSW": ["Stuart Island Airpark", "Friday Harbor", "US", true], - "SSX": ["Singita Safari Lodge Airport", "Singita Safari Lodge", "ZA", false], - "SSY": ["Mbanza Congo Airport", "Mbanza Congo", "AO", true], - "SSZ": ["Santos Nero Moura Air Base / Guarujá Airport", "Guarujá", "BR", false], - "STA": ["Stauning Vestjylland Airport", "Skjern", "DK", false], - "STB": ["Miguel Urdaneta Fernández Airport", "San Carlos del Zulia", "VE", false], - "STC": ["Saint Cloud Regional Airport", "Saint Cloud", "US", true], - "STD": ["Mayor Buenaventura Vivas International Airport", "Santo Domingo", "VE", true], - "STE": ["Stevens Point Municipal Airport", "Stevens Point", "US", false], - "STF": ["Stephens Island Seaplane Base", "Stephens Island", "AU", false], - "STG": ["St George Airport", "St George", "US", true], - "STH": ["Strathmore Airport", "Strathmore", "AU", false], - "STI": ["Cibao International Airport", "Santiago", "DO", true], - "STJ": ["Rosecrans Memorial Airport", "St Joseph", "US", false], - "STK": ["Sterling Municipal Airport", "Sterling", "US", false], - "STL": ["St. Louis Lambert International Airport", "St Louis", "US", true], - "STM": ["Santarém - Maestro Wilson Fonseca International Airport", "Santarém", "BR", true], - "STN": ["London Stansted Airport", "London, Essex", "GB", true], - "STP": ["Saint Paul Downtown Holman Field", "Saint Paul", "US", false], - "STQ": ["St Marys Municipal Airport", "St Marys", "US", false], - "STR": ["Stuttgart Airport", "Stuttgart", "DE", true], - "STS": ["Charles M. Schulz Sonoma County Airport", "Santa Rosa", "US", true], - "STT": ["Cyril E. King Airport", "Charlotte Amalie", "VI", true], - "STU": ["Santa Cruz Airport", "Santa Cruz", "BZ", false], - "STV": ["Surat International Airport", "Surat", "IN", true], - "STW": ["Stavropol Shpakovskoye Airport", "Stavropol", "RU", true], - "STX": ["Henry E. Rohlsen Airport", "Christiansted", "VI", true], - "STY": ["Nueva Hesperides International Airport", "Salto", "UY", false], - "SUA": ["Witham Field", "Stuart", "US", false], - "SUB": ["Juanda International Airport", "Surabaya", "ID", true], - "SUD": ["Stroud Municipal Airport", "Stroud", "US", false], - "SUE": ["Door County Cherryland Airport", "Sturgeon Bay", "US", false], - "SUF": ["Lamezia Terme Sant'Eufemia International Airport", "Lamezia Terme (CZ)", "IT", true], - "SUG": ["Surigao Airport", "Surigao City", "PH", true], - "SUI": ["Vladislav Ardzinba Sukhum International Airport", "Sukhumi", "GE", true], - "SUJ": ["Satu Mare International Airport", "Satu Mare", "RO", true], - "SUK": ["Sakkyryr Airport", "Batagay-Alyta", "RU", true], - "SUL": ["Sui Airport", "Sui", "PK", false], - "SUM": ["Sumter Airport", "Sumter", "US", false], - "SUN": ["Friedman Memorial Airport", "Hailey", "US", true], - "SUO": ["Sunriver Airport", "Sunriver", "US", false], - "SUP": ["Trunojoyo Airport", "Sumenep", "ID", false], - "SUQ": ["Sucúa Airport", "Sucúa", "EC", false], - "SUR": ["Summer Beaver Airport", "Summer Beaver", "CA", true], - "SUS": ["Spirit of St Louis Airport", "St Louis", "US", false], - "SUT": ["Sumbawanga Airport", "Sumbawanga", "TZ", false], - "SUU": ["Travis Air Force Base", "Fairfield", "US", false], - "SUV": ["Nausori International Airport", "Nausori", "FJ", true], - "SUW": ["Richard I Bong Memorial Airport", "Superior", "US", false], - "SUX": ["Sioux Gateway Airport / Brigadier General Bud Day Field", "Sioux City", "US", true], - "SUY": ["Suntar Airport", "Suntar", "RU", true], - "SVA": ["Savoonga Airport", "Savoonga", "US", true], - "SVB": ["Sambava Airport", "Sambava", "MG", true], - "SVC": ["Grant County Airport", "Silver City", "US", true], - "SVD": ["Argyle International Airport", "Kingstown", "VC", true], - "SVE": ["Susanville Municipal Airport", "Susanville", "US", false], - "SVF": ["Savé Airport", "Savé", "BJ", false], - "SVG": ["Stavanger Airport, Sola", "Stavanger", "NO", true], - "SVH": ["Statesville Regional Airport", "Statesville", "US", false], - "SVI": ["Eduardo Falla Solano Airport", "San Vicente Del Caguán", "CO", true], - "SVJ": ["Svolvær Airport, Helle", "Svolvær", "NO", true], - "SVK": ["Silver Creek Airport", "Silver Creek", "BZ", false], - "SVL": ["Savonlinna Airport", "Savonlinna", "FI", true], - "SVN": ["Hunter Army Air Field", "Savannah", "US", false], - "SVO": ["Sheremetyevo International Airport", "Moscow", "RU", true], - "SVP": ["Kuito Airport", "Kuito", "AO", false], - "SVQ": ["Seville Airport", "Seville", "ES", true], - "SVR": ["Savissivik Heliport", "Savissivik", "GL", true], - "SVS": ["Stevens Village Airport", "Stevens Village", "US", true], - "SVT": ["Savuti Airport", "Savuti", "BW", false], - "SVU": ["Savusavu Airport", "Savusavu", "FJ", true], - "SVV": ["San Salvador de Paul Airport", "San Salvador de Paul", "VE", false], - "SVW": ["Sparrevohn LRRS Airport", "Sparrevohn", "US", false], - "SVX": ["Koltsovo Airport", "Yekaterinburg", "RU", true], - "SVZ": ["Juan Vicente Gómez International Airport", "San Antonio del Tachira", "VE", true], - "SWA": ["Jieyang Chaoshan International Airport", "Jieyang (Rongcheng)", "CN", true], - "SWB": ["Shaw River Airport", "Shaw River", "AU", false], - "SWC": ["Stawell Airport", "", "AU", false], - "SWD": ["Seward Airport", "Seward", "US", false], - "SWE": ["Siwea Airport", "Siwea", "PG", false], - "SWF": ["New York Stewart International Airport", "Newburgh", "US", true], - "SWG": ["Satwag Airport", "Satwag", "PG", false], - "SWH": ["Swan Hill Airport", "", "AU", false], - "SWJ": ["Southwest Bay Airport", "Malekula Island", "VU", false], - "SWL": ["San Vicente Airport", "San Vicente", "PH", true], - "SWN": ["Sarsawa Air Force Station", "Sherpur Naqeebpur", "IN", false], - "SWO": ["Stillwater Regional Airport", "Stillwater", "US", true], - "SWP": ["Swakopmund Municipal Aerodrome", "Swakopmund", "NA", false], - "SWQ": ["Sultan Muhammad Kaharuddin III Airport", "Sumbawa Besar", "ID", true], - "SWR": ["Silur Airport", "Silur Mission", "PG", false], - "SWS": ["Swansea Airport", "Swansea", "GB", false], - "SWT": ["Strezhevoy Airport", "Strezhevoy", "RU", false], - "SWU": ["Suwon Airport", "Suwon", "KR", false], - "SWV": ["Severo-Evensk Airport", "Evensk", "RU", false], - "SWW": ["Avenger Field", "Sweetwater", "US", false], - "SWX": ["Shakawe Airport", "Shakawe", "BW", true], - "SWY": ["Sitiawan Airport", "Sitiawan", "MY", false], - "SXA": ["Sialum Airport", "Sialum", "PG", false], - "SXB": ["Strasbourg Airport", "Strasbourg", "FR", true], - "SXE": ["West Sale Airport", "Sale", "AU", false], - "SXG": ["Senanga Airport", "Senanga", "ZM", false], - "SXH": ["Sehulea Airport", "Sehulea", "PG", false], - "SXI": ["Siri Airport", "Siri", "IR", false], - "SXJ": ["Shanshan Airport", "Shanshan", "CN", false], - "SXK": ["Mathilda Batlayeri Airport", "Saumlaki-Yamdena Island", "ID", true], - "SXL": ["Sligo Airport", "Sligo", "IE", false], - "SXM": ["Princess Juliana International Airport", "Sint Maarten", "SX", true], - "SXN": ["Sua Pan Airport", "Sowa", "BW", false], - "SXO": ["São Félix do Araguaia Airport", "São Félix Do Araguaia", "BR", false], - "SXP": ["Nunam Iqua Airport", "Nunam Iqua", "US", true], - "SXQ": ["Soldotna Airport", "Soldotna", "US", false], - "SXR": ["Srinagar International Airport", "Srinagar", "IN", true], - "SXS": ["Sahabat [Sahabat 16] Airport", "Sahabat", "MY", false], - "SXT": ["Sungai Tiang Airport", "Taman Negara", "MY", false], - "SXU": ["Soddu Airport", "Soddu", "ET", false], - "SXV": ["Salem Airport", "Salem", "IN", false], - "SXW": ["Sauren Airport", "Sauren", "PG", false], - "SXX": ["São Félix do Xingu Airport", "São Félix do Xingu", "BR", false], - "SXY": ["Sidney Municipal Airport", "Sidney", "US", false], - "SXZ": ["Siirt Airport", "Siirt", "TR", false], - "SYA": ["Eareckson Air Station", "Shemya", "US", false], - "SYB": ["Seal Bay Seaplane Base", "Seal Bay", "US", true], - "SYC": ["Shiringayoc Airport", "Shiringayoc", "PE", false], - "SYD": ["Sydney Kingsford Smith International Airport", "Sydney (Mascot)", "AU", true], - "SYE": ["Sadah Airport", "Sa'dah", "YE", false], - "SYF": ["Silva Bay Seaplane Base", "Gabriola Island", "CA", true], - "SYH": ["Syangboche Airport", "Namche Bazaar", "NP", false], - "SYI": ["Bomar Field / Shelbyville Municipal Airport", "Shelbyville", "US", false], - "SYJ": ["Sirjan Airport", "Sirjan", "IR", false], - "SYK": ["Stykkishólmur Airport", "Stykkishólmur", "IS", false], - "SYL": ["Roberts Army Heliport", "Camp Roberts/San Miguel", "US", false], - "SYM": ["Pu'er Simao Airport", "Pu'er", "CN", true], - "SYN": ["Stanton Airfield", "Dennison", "US", false], - "SYO": ["Shonai Airport", "Shonai", "JP", true], - "SYP": ["Ruben Cantu Airport", "Santiago", "PA", false], - "SYQ": ["Tobías Bolaños International Airport", "San Jose", "CR", true], - "SYR": ["Syracuse Hancock International Airport", "Syracuse", "US", true], - "SYS": ["Saskylakh Airport", "Saskylakh", "RU", true], - "SYT": ["Saint-Yan Airport", "L'Hôpital-le-Mercier, Saône-et-Loire", "FR", false], - "SYU": ["Warraber Island Airport", "Sue Islet", "AU", true], - "SYV": ["Sylvester Airport", "Sylvester", "US", false], - "SYW": ["Sehwan Sharif Airport", "Sehwan Sharif", "PK", false], - "SYX": ["Sanya Phoenix International Airport", "Sanya (Tianya)", "CN", true], - "SYY": ["Stornoway Airport", "Stornoway, Western Isles", "GB", true], - "SYZ": ["Shiraz Shahid Dastghaib International Airport", "Shiraz", "IR", true], - "SZA": ["Soyo Airport", "Soyo", "AO", true], - "SZB": ["Sultan Abdul Aziz Shah International Airport", "Subang", "MY", true], - "SZE": ["Semera Airport", "Semera", "ET", true], - "SZF": ["Samsun-Çarşamba Airport", "Samsun", "TR", true], - "SZG": ["Salzburg Airport", "Salzburg", "AT", true], - "SZH": ["Shuozhou Zirun Airport", "Shuozhou", "CN", true], - "SZI": ["Zaysan Airport", "Zaysan", "KZ", true], - "SZJ": ["Siguanea Airport", "Isla de la Juventud", "CU", false], - "SZK": ["Skukuza Airport", "Skukuza", "ZA", true], - "SZL": ["Whiteman Air Force Base", "Knob Noster", "US", false], - "SZM": ["Sesriem Airport", "Sesriem", "NA", false], - "SZN": ["Santa Cruz Island Airport", "Santa Cruz Island", "US", false], - "SZP": ["Santa Paula Airport", "Santa Paula", "US", false], - "SZS": ["Ryan's Creek Aerodrome", "Oban", "NZ", false], - "SZV": ["Suzhou Guangfu Airport", "Suzhou", "CN", false], - "SZX": ["Shenzhen Bao'an International Airport", "Shenzhen", "CN", true], - "SZY": ["Olsztyn-Mazury Airport", "Szymany", "PL", true], - "SZZ": ["Solidarity Szczecin–Goleniów Airport", "Szczecin(Glewice)", "PL", true], - "TAB": ["A.N.R. Robinson International Airport", "Scarborough", "TT", true], - "TAC": ["Daniel Z. Romualdez Airport", "Tacloban City", "PH", true], - "TAD": ["Perry Stokes Airport", "Trinidad", "US", false], - "TAE": ["Daegu International Airport", "Daegu", "KR", true], - "TAF": ["Oran Tafraoui Airport", "Tafraoui", "DZ", false], - "TAG": ["Bohol-Panglao International Airport", "Panglao", "PH", true], - "TAH": ["Whitegrass Airport", "Tanna Island", "VU", true], - "TAI": ["Taiz International Airport", "Taiz", "YE", true], - "TAJ": ["Tadji Airport", "Aitape", "PG", false], - "TAK": ["Takamatsu Airport", "Takamatsu", "JP", true], - "TAL": ["Ralph M Calhoun Memorial Airport", "Tanana", "US", true], - "TAM": ["General Francisco Javier Mina International Airport", "Ciudad Madero", "MX", true], - "TAN": ["Tangalooma Airport", "", "AU", false], - "TAO": ["Qingdao Jiaodong International Airport", "Qingdao (Jiaozhou)", "CN", true], - "TAP": ["Tapachula International Airport", "Tapachula", "MX", true], - "TAQ": ["Tarcoola Airport", "Tarcoola", "AU", false], - "TAR": ["Taranto-Grottaglie Marcello Arlotta Airport", "Grottaglie", "IT", false], - "TAS": ["Tashkent International Airport", "Tashkent", "UZ", true], - "TAT": ["Poprad-Tatry Airport", "Poprad", "SK", true], - "TAU": ["Tauramena Airport", "Tauramena", "CO", false], - "TAW": ["Tacuarembo Airport", "Tacuarembo", "UY", false], - "TAX": ["Taliabu Island Airport", "Tikong-Taliabu Island", "ID", false], - "TAY": ["Tartu Airport", "Tartu", "EE", true], - "TAZ": ["Dashoguz International Airport", "Daşoguz", "TM", true], - "TBB": ["Dong Tac Airport", "Tuy Hoa", "VN", true], - "TBC": ["Tuba City Airport", "Tuba City", "US", false], - "TBD": ["Timbiqui Airport", "Timbiqui", "CO", false], - "TBE": ["Timbunke Airport", "Timbunke", "PG", false], - "TBF": ["Tabiteuea North Airport", "", "KI", false], - "TBG": ["Tabubil Airport", "Tabubil", "PG", true], - "TBH": ["Tugdan Airport", "Tablas Island", "PH", true], - "TBI": ["New Bight Airport", "Cat Island", "BS", true], - "TBJ": ["Tabarka-Aïn Draham International Airport", "Tabarka", "TN", true], - "TBK": ["Timber Creek Airport", "", "AU", false], - "TBL": ["Tableland Homestead Airport", "", "AU", false], - "TBM": ["Tumbang Samba Airport", "Tumbang Samba-Borneo Island", "ID", true], - "TBN": ["Waynesville-St. Robert Regional Airport-Forney Field", "Fort Leonard Wood", "US", true], - "TBO": ["Tabora Airport", "Tabora", "TZ", true], - "TBP": ["Captain Pedro Canga Rodríguez International Airport", "Tumbes", "PE", true], - "TBQ": ["Tarabo Airport", "Tarabo", "PG", false], - "TBR": ["Statesboro Bulloch County Airport", "Statesboro", "US", false], - "TBS": ["Tbilisi International Airport", "Tbilisi", "GE", true], - "TBT": ["Tabatinga International Airport", "Tabatinga", "BR", true], - "TBU": ["Fua'amotu International Airport", "Nuku'alofa", "TO", true], - "TBV": ["Tabal Airstrip", "Tabal Island", "MH", false], - "TBW": ["Donskoye Airport", "Tambov", "RU", false], - "TBX": ["Tambelan Airport", "Tambelan", "ID", false], - "TBY": ["Tshabong Airport", "Tshabong", "BW", false], - "TBZ": ["Tabriz International Airport", "Tabriz", "IR", true], - "TCA": ["Tennant Creek Airport", "Tennant Creek", "AU", true], - "TCB": ["Treasure Cay Airport", "Treasure Cay", "BS", true], - "TCC": ["Tucumcari Municipal Airport", "Tucumcari", "US", false], - "TCD": ["Tarapacá Airport", "Tarapacá", "CO", true], - "TCE": ["Tulcea Danube Delta Airport", "Mihail Kogălniceanu", "RO", false], - "TCF": ["Tocoa Airport", "Tocoa", "HN", false], - "TCG": ["Tacheng Qianquan Airport", "Tacheng", "CN", true], - "TCH": ["Tchibanga Airport", "Tchibanga", "GA", false], - "TCJ": ["Torembi Airport", "Torembi", "PG", false], - "TCK": ["Tinboli Airport", "Tinboli", "PG", false], - "TCL": ["Tuscaloosa National Airport", "Tuscaloosa", "US", false], - "TCM": ["McChord Air Force Base", "Tacoma", "US", false], - "TCN": ["Tehuacán National Airport", "Tehuacán", "MX", false], - "TCO": ["La Florida Airport", "Tumaco", "CO", true], - "TCP": ["Taba International Airport", "Taba", "EG", true], - "TCQ": ["Coronel FAP Carlos Ciriani Santa Rosa International Airport", "Tacna", "PE", true], - "TCR": ["Tuticorin Airport", "Vagaikulam", "IN", true], - "TCS": ["Truth or Consequences Municipal Airport", "Truth or Consequences", "US", false], - "TCT": ["Takotna Airport", "Takotna", "US", true], - "TCU": ["Thaba Nchu Tar Airport", "Homeward", "ZA", false], - "TCW": ["Tocumwal Airport", "", "AU", false], - "TCX": ["Tabas Airport", "Tabas", "IR", false], - "TCY": ["Terrace Bay Airport", "Terrace Bay", "NA", false], - "TCZ": ["Tengchong Tuofeng Airport", "Baoshan (Tengchong)", "CN", true], - "TDA": ["Trinidad Airport", "Trinidad", "CO", false], - "TDB": ["Tetebedi Airport", "Tetebedi", "PG", false], - "TDD": ["Teniente Av. Jorge Henrich Arauz Airport", "Trinidad", "BO", true], - "TDG": ["Tandag Airport", "Tandag", "PH", false], - "TDJ": ["Tadjoura Airport", "Tadjoura", "DJ", false], - "TDK": ["Taldykorgan Airport", "Taldykorgan", "KZ", true], - "TDL": ["Héroes de Malvinas Airport", "Tandil", "AR", false], - "TDN": ["Theda Station Airport", "Drysdale River", "AU", false], - "TDO": ["Ed Carlson Memorial Field South Lewis County Airport", "Toledo", "US", false], - "TDP": ["Trompeteros Airport", "Corrientes", "PE", false], - "TDR": ["Theodore Airport", "", "AU", false], - "TDS": ["Sasereme Airport", "Sasereme", "PG", true], - "TDT": ["Tanda Tula Airport", "Timbavati", "ZA", false], - "TDV": ["Samangoky Airport", "Tanandava", "MG", false], - "TDW": ["Tradewind Airport", "Amarillo", "US", false], - "TDX": ["Trat Airport", "Laem Ngop", "TH", true], - "TDZ": ["Toledo Executive Airport", "Toledo", "US", false], - "TEA": ["Tela Airport", "Tela", "HN", false], - "TEB": ["Teterboro Airport", "Teterboro", "US", true], - "TEC": ["Telêmaco Borba Airport", "Telêmaco Borba", "BR", false], - "TED": ["Thisted Airport", "Thisted", "DK", false], - "TEE": ["Cheikh Larbi Tébessi Airport", "Tébessi", "DZ", true], - "TEF": ["Telfer Airport", "", "AU", false], - "TEG": ["Tenkodogo Airport", "Tenkodogo", "BF", false], - "TEH": ["Tetlin Airport", "Tetlin", "US", false], - "TEI": ["Tezu Airport", "Tezu", "IN", false], - "TEK": ["Tatitlek Airport", "Tatitlek", "US", true], - "TEL": ["Telupid Airport", "Telupid", "MY", false], - "TEM": ["Temora Airport", "Temora", "AU", false], - "TEN": ["Tongren Fenghuang Airport", "Tongren (Daxing)", "CN", true], - "TEO": ["Terapo Airport", "Terapo Mission", "PG", false], - "TEP": ["Tep Tep Airport", "Teptep", "PG", false], - "TEQ": ["Tekirdağ Çorlu Airport", "Çorlu", "TR", true], - "TER": ["Lajes Airport", "Praia da Vitória", "PT", true], - "TES": ["Tessenei Airport", "Tessenei", "ER", false], - "TET": ["Tete Airport", "Tete", "MZ", true], - "TEU": ["Manapouri Airport", "", "NZ", false], - "TEV": ["Teruel Airport", "Teruel", "ES", false], - "TEX": ["Telluride Regional Airport", "Telluride", "US", true], - "TEY": ["Þingeyri Airport", "Þingeyri", "IS", false], - "TEZ": ["Tezpur Airport", "", "IN", true], - "TFB": ["Tifalmin Airport", "Tifalmin", "PG", false], - "TFF": ["Tefé Airport", "Tefé", "BR", true], - "TFI": ["Tufi Airport", "Tufi", "PG", true], - "TFL": ["Juscelino Kubitscheck Airport", "Teófilo Otoni", "BR", false], - "TFM": ["Telefomin Airport", "Telefomin", "PG", false], - "TFN": ["Tenerife Norte-Ciudad de La Laguna Airport", "Tenerife", "ES", true], - "TFS": ["Tenerife Sur Airport", "Tenerife", "ES", true], - "TFT": ["Taftan Airport", "Taftan", "PK", false], - "TFU": ["Chengdu Tianfu International Airport", "Chengdu (Jianyang)", "CN", true], - "TFY": ["Muhammad Taufiq Kiemas Airport", "Krui", "ID", false], - "TGA": ["Tengah Air Base", "Western Water Catchment", "SG", false], - "TGB": ["Tagbita Airstrip", "Rizal", "PH", false], - "TGC": ["Tanjung Manis Airport", "Belawai", "MY", false], - "TGD": ["Podgorica Airport / Podgorica Golubovci Airbase", "Podgorica", "ME", true], - "TGG": ["Sultan Mahmud Airport", "Kuala Terengganu", "MY", true], - "TGH": ["Tongoa Airport", "Tongoa Island", "VU", true], - "TGI": ["Tingo Maria Airport", "Tingo Maria", "PE", false], - "TGJ": ["Tiga Airport", "Tiga", "NC", true], - "TGK": ["Taganrog Yuzhny Airport", "Taganrog", "RU", false], - "TGL": ["Tagula Airport", "Sudest Island", "PG", false], - "TGM": ["Târgu Mureş Transilvania International Airport", "Recea", "RO", true], - "TGN": ["Latrobe Valley Airport", "Morwell", "AU", false], - "TGO": ["Tongliao Airport", "Tongliao", "CN", true], - "TGP": ["Podkamennaya Tunguska Airport", "Bor", "RU", false], - "TGQ": ["Tangará da Serra Airport", "Tangará da Serra", "BR", true], - "TGR": ["Touggourt Sidi Madhi Airport", "Touggourt", "DZ", true], - "TGS": ["Chokwé Airport", "Chokwé", "MZ", false], - "TGT": ["Tanga Airport", "Tanga", "TZ", true], - "TGU": ["Toncontín Airport", "Tegucigalpa", "HN", true], - "TGZ": ["Angel Albino Corzo International Airport", "Tuxtla Gutiérrez", "MX", true], - "THA": ["Tullahoma Regional Airport William Northern Field", "Tullahoma", "US", false], - "THB": ["Thaba-Tseka Airport", "Thaba-Tseka", "LS", false], - "THC": ["Tchien Airport", "Zwedru", "LR", false], - "THD": ["Tho Xuan Airport", "Thanh Hóa", "VN", true], - "THE": ["Senador Petrônio Portela Airport", "Teresina", "BR", true], - "THG": ["Thangool Airport", "Biloela", "AU", true], - "THI": ["Tichitt Airport", "Tichitt", "MR", false], - "THK": ["Thakhek Airport", "Thakhek", "LA", false], - "THL": ["Tachileik Airport", "Tachileik", "MM", true], - "THM": ["Thompson Falls Airport", "Thompson Falls", "US", false], - "THN": ["Trollhättan-Vänersborg Airport", "Trollhättan", "SE", true], - "THO": ["Þórshöfn Airport", "Þórshöfn", "IS", true], - "THP": ["Hot Springs County Airport", "Thermopolis", "US", false], - "THQ": ["Tianshui Maijishan Airport", "Tianshui (Maiji)", "CN", true], - "THR": ["Mehrabad International Airport", "Tehran", "IR", true], - "THS": ["Sukhothai Airport", "", "TH", true], - "THT": ["Tamchakett Airport", "Tamchakett", "MR", false], - "THU": ["Pituffik Space Base", "Pituffik", "GL", false], - "THV": ["York Airport", "Thomasville", "US", false], - "THW": ["Trincomalee Harbor Waterdrome", "Trincomalee", "LK", false], - "THX": ["Turukhansk Airport", "Turukhansk", "RU", true], - "THY": ["Thohoyandou Airport", "Thohoyandou", "ZA", false], - "THZ": ["Tahoua Airport", "Tahoua", "NE", false], - "TIA": ["Tirana International Airport Mother Teresa", "Rinas", "AL", true], - "TIB": ["Tibú Airport", "Tibú", "CO", false], - "TIC": ["Tinak Airport", "Arno Atoll", "MH", false], - "TID": ["Abdelhafid Boussouf Bou Chekif Airport", "Tiaret", "DZ", false], - "TIE": ["Tippi Airport", "Tippi", "ET", true], - "TIF": ["Taif International Airport", "Taif", "SA", true], - "TIH": ["Tikehau Airport", "Tuherahera", "PF", true], - "TII": ["Tarinkot Airport", "Tarinkot", "AF", false], - "TIJ": ["General Abelardo L. Rodriguez International Airport", "Tijuana", "MX", true], - "TIK": ["Tinker Air Force Base", "Oklahoma City", "US", false], - "TIL": ["Cheadle Airport", "Cheadle", "CA", false], - "TIM": ["Mozes Kilangin Airport", "Timika", "ID", true], - "TIN": ["Tindouf Airport", "Tindouf", "DZ", true], - "TIO": ["Tilin Airport", "Tilin", "MM", false], - "TIQ": ["Francisco Manglona Borja / Tinian International Airport", "Tinian Island", "MP", true], - "TIR": ["Tirupati International Airport", "Tirupati", "IN", true], - "TIU": ["Timaru Airport", "", "NZ", true], - "TIV": ["Tivat Airport", "Tivat", "ME", true], - "TIW": ["Tacoma Narrows Airport", "Tacoma", "US", true], - "TIX": ["Space Coast Regional Airport", "Titusville", "US", false], - "TIY": ["Tidjikja Airport", "Tidjikja", "MR", false], - "TIZ": ["Tari Airport", "Tari", "PG", true], - "TJA": ["Capitan Oriel Lea Plaza Airport", "Tarija", "BO", true], - "TJB": ["Raja Haji Abdullah Airport", "Tanjung Balai-Karinmunbesar Island", "ID", false], - "TJC": ["Ticantiquí Airport", "Ticantiquí", "PA", false], - "TJG": ["Warukin Airport", "Tanta-Tabalong", "ID", true], - "TJH": ["Konotori Tajima Airport", "Toyooka", "JP", true], - "TJI": ["Trujillo Airport", "Trujillo", "HN", false], - "TJK": ["Tokat Airport", "Tokat", "TR", true], - "TJL": ["Plínio Alarcom Airport", "Três Lagoas", "BR", true], - "TJM": ["Roshchino International Airport", "Tyumen", "RU", true], - "TJN": ["Takume Airport", "Takume", "PF", false], - "TJQ": ["H A S Hanandjoeddin International Airport", "Tanjung Pandan", "ID", true], - "TJS": ["Tanjung Harapan Airport", "Tanjung Selor-Borneo Island", "ID", true], - "TJU": ["Kulob International Airport", "Kulob", "TJ", true], - "TJV": ["Thanjavur Air Force Station", "Thanjavur", "IN", false], - "TKA": ["Talkeetna Airport", "Talkeetna", "US", false], - "TKB": ["Tekadu Airport", "Tekadu", "PG", false], - "TKC": ["Tiko Airport", "Tiko", "CM", false], - "TKD": ["Takoradi Airport", "Sekondi-Takoradi", "GH", true], - "TKE": ["Tenakee Seaplane Base", "Tenakee Springs", "US", true], - "TKF": ["Truckee Tahoe Airport", "Truckee", "US", true], - "TKG": ["Radin Inten II International Airport", "Bandar Lampung", "ID", true], - "TKH": ["Takhli Royal Thai Air Force Base", "Takhli", "TH", false], - "TKI": ["Tokeen Seaplane Base", "Tokeen", "US", false], - "TKJ": ["Tok Junction Airport", "Tok", "US", true], - "TKK": ["Chuuk International Airport", "Weno Island", "FM", true], - "TKL": ["Taku Lodge Seaplane Base", "Taku Lodge", "US", false], - "TKM": ["Taksimo Airport", "Taksimo", "RU", true], - "TKN": ["Tokunoshima Airport", "Amagi", "JP", true], - "TKO": ["Tlokoeng Airport", "Tlokoeng", "LS", false], - "TKP": ["Takapoto Airport", "", "PF", true], - "TKQ": ["Kigoma Airport", "Kigoma", "TZ", true], - "TKR": ["Thakurgaon Airport", "Thakurgaon", "BD", false], - "TKS": ["Tokushima Awaodori Airport / JMSDF Tokushima Air Base", "Tokushima", "JP", true], - "TKT": ["Tak Airport", "", "TH", false], - "TKU": ["Turku Airport", "Turku", "FI", true], - "TKV": ["Tatakoto Airport", "Tatakoto", "PF", true], - "TKW": ["Tekin Airport", "Tekin", "PG", false], - "TKX": ["Takaroa Airport", "", "PF", true], - "TKY": ["Turkey Creek Airport", "Turkey Creek", "AU", false], - "TKZ": ["Tokoroa Airfield", "Tokoroa", "NZ", false], - "TLA": ["Teller Airport", "Teller", "US", true], - "TLB": ["Tarbela Dam Airport", "Tarbela", "PK", false], - "TLC": ["Adolfo López Mateos International Airport", "Toluca", "MX", true], - "TLD": ["Tuli Lodge Airport", "Tuli Lodge", "BW", false], - "TLE": ["Toliara Airport", "Toliara", "MG", true], - "TLF": ["Telida Airport", "Telida", "US", false], - "TLG": ["Tulaghi Heliport", "Tulaghi Island", "SB", false], - "TLH": ["Tallahassee International Airport", "Tallahassee", "US", true], - "TLI": ["Sultan Bantilan Airport", "Toli Toli-Celebes Island", "ID", true], - "TLJ": ["Tatalina LRRS Airport", "Takotna", "US", false], - "TLK": ["Talakan Airport", "Talakan Oil Field", "RU", false], - "TLL": ["Lennart Meri Tallinn Airport", "Tallinn", "EE", true], - "TLM": ["Zenata – Messali El Hadj Airport", "Zenata", "DZ", true], - "TLN": ["Toulon-Hyères Airport", "Hyères, Var", "FR", true], - "TLO": ["Tol Airport", "Tol", "PG", false], - "TLP": ["Tumolbil Airport", "Tumolbil", "PG", false], - "TLQ": ["Turpan Jiaohe Airport", "Turpan", "CN", true], - "TLR": ["Mefford Field", "Tulare", "US", false], - "TLS": ["Toulouse-Blagnac Airport", "Toulouse/Blagnac", "FR", true], - "TLT": ["Tuluksak Airport", "Tuluksak", "US", true], - "TLU": ["Golfo de Morrosquillo Airport", "Santiago de Tolú", "CO", true], - "TLV": ["Ben Gurion International Airport", "Tel Aviv", "IL", true], - "TLW": ["Talasea Airport", "Talasea", "PG", false], - "TLX": ["Panguilemo Airport", "Talca", "CL", false], - "TLY": ["Plastun Airport", "Plastun", "RU", true], - "TLZ": ["Catalão Airport", "Catalão", "BR", false], - "TMA": ["Henry Tift Myers Airport", "Tifton", "US", false], - "TMB": ["Miami Executive Airport", "Miami", "US", false], - "TMC": ["Tambolaka Airport", "Radamata", "ID", true], - "TMD": ["Timbedra Airport", "Timbedra", "MR", false], - "TME": ["Gustavo Vargas Airport", "Tame", "CO", true], - "TMF": ["Thimarafushi Airport", "Thimarafushi", "MV", true], - "TMG": ["Tomanggong Airport", "Tomanggong", "MY", true], - "TMH": ["Tanah Merah Airport", "Tanah Merah", "ID", true], - "TMI": ["Tumling Tar Airport", "Tumling Tar", "NP", true], - "TMJ": ["Termez Airport", "Termez", "UZ", true], - "TML": ["Yakubu Tali International Airport", "Tamale", "GH", true], - "TMM": ["Toamasina Ambalamanasy Airport", "Toamasina", "MG", true], - "TMN": ["Tamana Island Airport", "Tamana Island", "KI", false], - "TMO": ["Tumeremo Airport", "", "VE", false], - "TMP": ["Tampere-Pirkkala Airport", "Tampere / Pirkkala", "FI", true], - "TMQ": ["Tambao Airport", "Tambao", "BF", false], - "TMR": ["Aguenar – Hadj Bey Akhamok Airport", "Tamanrasset", "DZ", true], - "TMS": ["São Tomé International Airport", "São Tomé", "ST", true], - "TMT": ["Trombetas Airport", "Oriximiná", "BR", true], - "TMU": ["Tambor Airport", "Nicoya", "CR", false], - "TMW": ["Tamworth Airport", "Tamworth", "AU", true], - "TMX": ["Timimoun Airport", "Timimoun", "DZ", true], - "TMY": ["Tiom Airport", "Tiom-Papua Island", "ID", false], - "TMZ": ["Thames Aerodrome", "", "NZ", false], - "TNA": ["Jinan Yaoqiang International Airport", "Jinan (Licheng)", "CN", true], - "TNB": ["Tanah Grogot Airport", "Tanah Grogot", "ID", false], - "TNC": ["Tin City Long Range Radar Station Airport", "Tin City", "US", true], - "TND": ["Alberto Delgado Airport", "Trinidad", "CU", true], - "TNE": ["New Tanegashima Airport", "Tanegashima", "JP", true], - "TNF": ["Toussus-le-Noble Airport", "Toussus-le-Noble, Yvelines", "FR", false], - "TNG": ["Tangier Ibn Battuta Airport", "Tangier", "MA", true], - "TNH": ["Tonghua Sanyuanpu Airport", "Tonghua", "CN", true], - "TNI": ["Satna Airport", "", "IN", false], - "TNJ": ["Raja Haji Fisabilillah International Airport", "Tanjung Pinang-Bintan Island", "ID", true], - "TNK": ["Tununak Airport", "Tununak", "US", true], - "TNL": ["Ternopil International Airport", "Ternopil", "UA", false], - "TNM": ["Teniente Rodolfo Marsh Martin Airport", "Villa Las Estrellas", "AQ", false], - "TNN": ["Tainan International Airport / Tainan Air Base", "Tainan (Rende)", "TW", true], - "TNO": ["Tamarindo Airport", "Tamarindo", "CR", false], - "TNP": ["Twentynine Palms Airport", "Twentynine Palms", "US", false], - "TNQ": ["Washington Island Airstrip", "Teraina", "KI", false], - "TNR": ["Ivato International Airport", "Antananarivo", "MG", true], - "TNS": ["Tungsten (Cantung) Airport", "Tungsten", "CA", false], - "TNT": ["Dade Collier Training and Transition Airport", "Miami", "US", false], - "TNU": ["Newton Municipal Airport", "Newton", "US", false], - "TNV": ["Tabuaeran Island Airport", "Tabuaeran Island", "KI", false], - "TNW": ["Jumandy Airport", "Ahuano", "EC", false], - "TNZ": ["Tosontsengel Airport", "Tosontsengel", "MN", false], - "TOA": ["Zamperini Field", "Torrance", "US", false], - "TOB": ["Tobruk International Airport", "Adam", "LY", false], - "TOC": ["Toccoa Airport - R.G. Letourneau Field", "Toccoa", "US", false], - "TOD": ["Tioman Airport", "Tioman Island", "MY", true], - "TOE": ["Tozeur Nefta International Airport", "Tozeur", "TN", true], - "TOF": ["Tomsk Kamov Airport", "Tomsk", "RU", true], - "TOG": ["Togiak Airport", "Togiak Village", "US", true], - "TOH": ["Torres Airstrip", "Loh/Linua", "VU", false], - "TOI": ["Troy Municipal Airport at N Kenneth Campbell Field", "Troy", "US", false], - "TOJ": ["Madrid–Torrejón Airport / Torrejón Air Base", "Madrid", "ES", false], - "TOK": ["Torokina Airport", "Torokina", "PG", false], - "TOL": ["Eugene F. Kranz Toledo Express Airport", "Toledo", "US", true], - "TOM": ["Tombouktou Airport", "Timbuktu", "ML", true], - "TON": ["Tonu Airport", "Tonu", "PG", false], - "TOO": ["San Vito de Java Airport", "Coto Brus", "CR", false], - "TOP": ["Philip Billard Municipal Airport", "Topeka", "US", false], - "TOQ": ["Barriles Airport", "Tocopilla", "CL", false], - "TOR": ["Torrington Municipal Airport", "Torrington", "US", false], - "TOS": ["Tromsø Airport", "Tromsø", "NO", true], - "TOT": ["Totness Airport", "Totness", "SR", false], - "TOU": ["Touho Airport", "Touho", "NC", true], - "TOV": ["Tortola West End Seaplane Base", "Tortola", "VG", false], - "TOW": ["Toledo - Luiz Dalcanale Filho Municipal Airport", "Toledo", "BR", true], - "TOX": ["Tobolsk Airport", "Tobolsk", "RU", false], - "TOY": ["Toyama Kitokito Airport", "Toyama", "JP", true], - "TOZ": ["Mahana Airport", "Touba", "CI", false], - "TPA": ["Tampa International Airport", "Tampa", "US", true], - "TPC": ["Tarapoa Airport", "Tarapoa", "EC", false], - "TPE": ["Taiwan Taoyuan International Airport", "Taoyuan", "TW", true], - "TPF": ["Peter O Knight Airport", "Tampa", "US", false], - "TPG": ["Taiping (Tekah) Airport", "Taiping", "MY", false], - "TPH": ["Tonopah Airport", "Tonopah", "US", false], - "TPI": ["Tapini Airport", "Tapini", "PG", true], - "TPJ": ["Taplejung Airport", "Taplejung", "NP", true], - "TPK": ["Teuku Cut Ali Airport", "Tapaktuan", "ID", false], - "TPL": ["Draughon Miller Central Texas Regional Airport", "Temple", "US", false], - "TPN": ["Tiputini Airport", "Tiputini", "EC", false], - "TPP": ["Cadete FAP Guillermo Del Castillo Paredes Airport", "Tarapoto", "PE", true], - "TPQ": ["Amado Nervo National Airport", "Tepic", "MX", true], - "TPR": ["Tom Price Airport", "Tom Price", "AU", false], - "TPS": ["Vincenzo Florio Airport Trapani-Birgi", "Trapani (TP)", "IT", true], - "TPT": ["Tapeta Airport", "Tapeta", "LR", false], - "TPU": ["Tikapur Airport", "Tikapur", "NP", false], - "TPX": ["Tupai Airport", "Tupai Atoll", "PF", false], - "TQA": ["Tasiusaq Heliport", "Tasiusaq", "GL", true], - "TQD": ["Al Taqaddum Air Base", "Al Habbaniyah", "IQ", false], - "TQI": ["Tiniteqilaaq Heliport", "Tiniteqilaaq", "GL", true], - "TQL": ["Tarko-Sale Airport", "Tarko-Sale", "RU", false], - "TQN": ["Taleqan Airport", "Taleqan", "AF", false], - "TQO": ["Felipe Carrillo Puerto International Airport Tulum", "Tulum", "MX", true], - "TQP": ["Trepell Airport", "Trepell", "AU", false], - "TQQ": ["Maranggo Airport", "Waha-Tomea Island", "ID", false], - "TQR": ["San Domino Island Heliport", "Tremiti Islands", "IT", true], - "TQS": ["Captain Ernesto Esguerra Cubides Air Base", "Tres Esquinas", "CO", false], - "TRA": ["Tarama Airport", "Tarama", "JP", true], - "TRB": ["Gonzalo Mejía Airport", "Turbo", "CO", false], - "TRC": ["Francisco Sarabia Tinoco International Airport", "Torreón", "MX", true], - "TRD": ["Trondheim Airport, Værnes", "Trondheim", "NO", true], - "TRE": ["Tiree Airport", "Balemartine, Argyll and Bute", "GB", true], - "TRF": ["Sandefjord Airport, Torp", "Sandefjord(Torp)", "NO", true], - "TRG": ["Tauranga Airport", "Tauranga", "NZ", true], - "TRH": ["Trona Airport", "Trona", "US", false], - "TRI": ["Tri-Cities Regional TN/VA Airport", "Blountville", "US", true], - "TRJ": ["Tarakbits Airport", "Tarakbits", "PG", false], - "TRK": ["Juwata International Airport / Suharnoko Harbani AFB", "Tarakan", "ID", true], - "TRL": ["Terrell Municipal Airport", "Terrell", "US", false], - "TRM": ["Jacqueline Cochran Regional Airport", "Palm Springs", "US", false], - "TRN": ["Turin Airport", "Caselle Torinese (TO)", "IT", true], - "TRO": ["Taree Airport", "Taree", "AU", false], - "TRQ": ["Tarauacá Airport", "Tarauacá", "BR", false], - "TRR": ["China Bay Airport", "Trincomalee", "LK", true], - "TRS": ["Trieste Airport", "Ronchi dei Legionari/Trieste", "IT", true], - "TRT": ["Toraja Airport", "Toraja", "ID", true], - "TRU": ["Capitán FAP Carlos Martínez de Pinillos International Airport", "Trujillo", "PE", true], - "TRV": ["Thiruvananthapuram International Airport", "Thiruvananthapuram", "IN", true], - "TRW": ["Bonriki International Airport", "South Tarawa", "KI", true], - "TRX": ["Trenton Municipal Airport", "Trenton", "US", false], - "TRY": ["Tororo Airport", "Tororo", "UG", false], - "TRZ": ["Tiruchirappalli International Airport", "Tiruchirappalli", "IN", true], - "TSA": ["Taipei Songshan International Airport", "Taipei (Songshan)", "TW", true], - "TSB": ["Tsumeb Airport", "Tsumeb", "NA", false], - "TSC": ["Taisha Airport", "Taisha", "EC", false], - "TSD": ["Tshipise Airport", "Tshipise", "ZA", false], - "TSF": ["Treviso Airport", "Treviso (TV)", "IT", true], - "TSG": ["Tanacross Airport", "Tanacross", "US", false], - "TSH": ["Tshikapa Airport", "Tshikapa", "CD", false], - "TSI": ["Tsile Tsile Airport", "Tsile Tsile", "PG", false], - "TSJ": ["Tsushima Airport", "Tsushima", "JP", true], - "TSL": ["Tamuín National Airport", "Tamuín", "MX", false], - "TSM": ["Taos Regional Airport", "Taos", "US", true], - "TSN": ["Tianjin Binhai International Airport", "Tianjin", "CN", true], - "TSP": ["Tehachapi Municipal Airport", "Tehachapi", "US", false], - "TSQ": ["Torres Airport", "Torres", "BR", false], - "TSR": ["Timișoara Traian Vuia International Airport", "Timişoara", "RO", true], - "TSS": ["East 34th Street Heliport", "New York", "US", true], - "TST": ["Trang Airport", "Trang", "TH", true], - "TSU": ["Tabiteuea South Airport", "Tabiteuea South", "KI", false], - "TSV": ["Townsville Airport / RAAF Base Townsville", "Townsville", "AU", true], - "TSW": ["Tsewi Airport", "Tsewi", "PG", false], - "TSY": ["Wiriadinata Airport", "Cibeureum, Tasikmalaya-Java Island", "ID", false], - "TSZ": ["Tsetserleg Airport", "Tsetserleg", "MN", false], - "TTA": ["Tan Tan Airport", "Tan Tan", "MA", true], - "TTC": ["Las Breas Airport", "Taltal", "CL", false], - "TTD": ["Portland Troutdale Airport", "Portland", "US", false], - "TTE": ["Sultan Babullah Airport", "Ternate", "ID", true], - "TTG": ["General Enrique Mosconi Airport", "Tartagal", "AR", false], - "TTH": ["Thumrait Air Base", "Thumrait", "OM", false], - "TTI": ["Tetiaroa Airport", "Tetiaroa", "PF", false], - "TTJ": ["Tottori Sand Dunes Conan Airport", "Tottori", "JP", true], - "TTL": ["Turtle Island Seaplane Base", "Nanuya Levu Island", "FJ", false], - "TTM": ["Tablón De Tamara Airport", "Tablón De Tamara", "CO", false], - "TTN": ["Trenton Mercer Airport", "Ewing Township", "US", true], - "TTO": ["Britton Municipal Airport", "Britton", "US", false], - "TTQ": ["Tortuguero Airport", "Tortuguero", "CR", false], - "TTR": ["Pongtiku Airport", "Makale", "ID", false], - "TTS": ["Tsaratanana Airport", "Tsaratanana", "MG", true], - "TTT": ["Taitung Airport", "Taitung City", "TW", true], - "TTU": ["Sania Ramel Airport", "Tétouan", "MA", true], - "TTW": ["Tissa Tank Waterdrome", "Tissamaharama", "LK", true], - "TTX": ["Truscott-Mungalalu Airport", "Anjo Peninsula", "AU", false], - "TUA": ["Lieutenant Colonel Luis A. Mantilla International Airport", "Tulcán", "EC", true], - "TUB": ["Tubuai Airport", "", "PF", true], - "TUC": ["Teniente Benjamín Matienzo International Airport", "San Miguel de Tucumán", "AR", true], - "TUD": ["Tambacounda Airport", "Tambacounda", "SN", false], - "TUF": ["Tours Val de Loire Airport", "Tours, Indre-et-Loire", "FR", true], - "TUG": ["Tuguegarao Airport", "Tuguegarao City", "PH", true], - "TUH": ["Arnold Air Force Base", "Manchester", "US", false], - "TUI": ["Turaif Domestic Airport", "Turaif", "SA", true], - "TUJ": ["Tum Airport", "Tum", "ET", false], - "TUK": ["Turbat International Airport", "Turbat", "PK", true], - "TUL": ["Tulsa International Airport", "Tulsa", "US", true], - "TUM": ["Tumut Aerodrome", "Tumut", "AU", false], - "TUN": ["Tunis Carthage International Airport", "Tunis", "TN", true], - "TUO": ["Taupo Airport", "Taupo", "NZ", true], - "TUP": ["Tupelo Regional Airport", "Tupelo", "US", true], - "TUQ": ["Tougan Airport", "Tougan", "BF", false], - "TUR": ["Tucuruí Airport", "Tucuruí", "BR", true], - "TUS": ["Tucson International Airport", "Tucson", "US", true], - "TUU": ["Prince Sultan bin Abdulaziz International Airport", "Tabuk", "SA", true], - "TUV": ["Tucupita Airport", "Tucupita", "VE", false], - "TUW": ["Tubualá Airport", "Coetupo", "PA", false], - "TUX": ["Tumbler Ridge Airport", "Tumbler Ridge", "CA", false], - "TUY": ["Base Aeronaval de Tulum", "Tulum", "MX", false], - "TVA": ["Morafenobe Airport", "Morafenobe", "MG", false], - "TVC": ["Cherry Capital Airport", "Traverse City", "US", true], - "TVF": ["Thief River Falls Regional Airport", "Thief River Falls", "US", true], - "TVI": ["Thomasville Regional Airport", "Thomasville", "US", false], - "TVL": ["Lake Tahoe Airport", "South Lake Tahoe", "US", false], - "TVS": ["Tangshan Sannühe Airport", "Tangshan (Fengrun)", "CN", true], - "TVT": ["Tashkent-Khumo International Airport", "Tashkent", "UZ", true], - "TVU": ["Matei Airport", "Matei", "FJ", true], - "TVY": ["Dawei Airport", "Dawei", "MM", true], - "TWA": ["Twin Hills Airport", "Twin Hills", "US", true], - "TWB": ["Toowoomba Airport", "Toowoomba", "AU", false], - "TWC": ["Tumxuk Tangwangcheng Airport", "Tumxuk", "CN", true], - "TWD": ["Jefferson County International Airport", "Port Townsend", "US", false], - "TWE": ["Taylor Airport", "Taylor", "US", false], - "TWF": ["Joslin Field Magic Valley Regional Airport", "Twin Falls", "US", true], - "TWH": ["Two Harbors Amphibious Terminal", "Two Harbors", "US", false], - "TWP": ["Torwood Airport", "Torwood", "AU", false], - "TWT": ["Sanga Sanga Airport", "Bongao", "PH", true], - "TWU": ["Tawau Airport", "Tawau", "MY", true], - "TWY": ["Tawa Airport", "Tawa", "PG", false], - "TWZ": ["Pukaki Airport", "Twitzel", "NZ", false], - "TXC": ["Orsha Airport - Balbasovo Air Base", "Orsha", "BY", false], - "TXE": ["Rembele Airport", "Takengon", "ID", true], - "TXF": ["9 de Maio - Teixeira de Freitas Airport", "Teixeira de Freitas", "BR", false], - "TXK": ["Texarkana Regional Airport (Webb Field)", "Texarkana", "US", true], - "TXM": ["Teminabuan Airport", "Atinjoe", "ID", false], - "TXN": ["Huangshan Tunxi International Airport", "Huangshan", "CN", true], - "TXR": ["Tanbar Airport", "Tanbar Station", "AU", false], - "TXU": ["Tabou Airport", "Tabou", "CI", false], - "TYA": ["Klokovo Air Base", "Tula", "RU", false], - "TYB": ["Tibooburra Airport", "", "AU", false], - "TYC": ["Taiyuan Yaocheng Airport", "Taiyuan", "CN", false], - "TYD": ["Tynda Airport", "Tynda", "RU", false], - "TYE": ["Tyonek Airport", "Tyonek", "US", false], - "TYF": ["Torsby Airport", "Torsby", "SE", true], - "TYG": ["Thylungra Airport", "", "AU", false], - "TYL": ["Captain Victor Montes Arias International Airport", "Talara", "PE", true], - "TYM": ["Staniel Cay Airport", "Staniel Cay", "BS", false], - "TYN": ["Taiyuan Wusu International Airport", "Taiyuan", "CN", true], - "TYP": ["Tobermorey Airport", "Tobermorey", "AU", false], - "TYR": ["Tyler Pounds Regional Airport", "Tyler", "US", true], - "TYS": ["McGhee Tyson Airport", "Knoxville/Maryville", "US", true], - "TYT": ["Treinta y Tres Airport", "Treinta y Tres", "UY", false], - "TYZ": ["Taylor Airport", "Taylor", "US", false], - "TZA": ["Sir Barry Bowen Municipal Airport", "Belize City", "BZ", true], - "TZC": ["Tuscola Area Airport", "Caro", "US", false], - "TZL": ["Tuzla International Airport", "Dubrave Gornje", "BA", true], - "TZM": ["Aeródromo de Cupul", "Tizimin", "MX", false], - "TZN": ["Congo Town Airport", "Andros", "BS", true], - "TZO": ["Tsimiroro Airport", "Ankisatra", "MG", false], - "TZX": ["Trabzon International Airport", "Trabzon", "TR", true], - "UAB": ["İncirlik Air Base", "Sarıçam", "TR", false], - "UAC": ["San Luis Río Colorado Airport", "San Luis Río Colorado", "MX", false], - "UAE": ["Mount Aue Airport", "Aue", "PG", false], - "UAH": ["Ua Huka Airport", "Ua Huka", "PF", true], - "UAI": ["Commander in Chief of FALINTIL, Kay Rala Xanana Gusmão, International Airport", "Suai", "TL", true], - "UAK": ["Narsarsuaq Airport", "Narsarsuaq", "GL", true], - "UAL": ["Luau Airport", "Luau", "AO", false], - "UAM": ["Andersen Air Force Base", "Yigo", "GU", false], - "UAP": ["Ua Pou Airport", "Ua Pou", "PF", true], - "UAQ": ["Domingo Faustino Sarmiento Airport", "San Juan", "AR", true], - "UAR": ["Bouarfa Airport", "Bouarfa", "MA", true], - "UAS": ["Samburu Airport", "Isiolo West", "KE", false], - "UAX": ["Uaxactun Airport", "Uaxactun", "GT", false], - "UBA": ["Mário de Almeida Franco Airport", "Uberaba", "BR", true], - "UBB": ["Mabuiag Island Airport", "Mabuiag Island", "AU", true], - "UBI": ["Buin Airport", "Buin", "PG", false], - "UBJ": ["Yamaguchi Ube Airport", "Ube", "JP", true], - "UBN": ["Ulaanbaatar Chinggis Khaan International Airport", "Ulaanbaatar (Sergelen)", "MN", true], - "UBP": ["Ubon Ratchathani Airport", "Ubon Ratchathani", "TH", true], - "UBS": ["Columbus Lowndes County Airport", "Columbus", "US", false], - "UBT": ["Ubatuba Gastão Madeira State Airport", "Ubatuba", "BR", false], - "UBU": ["Kalumburu Airport", "Kalumburu", "AU", false], - "UCB": ["Ulanqab Jining Airport", "Ulanqab", "CN", true], - "UCC": ["Yucca Airstrip", "Mercury", "US", false], - "UCE": ["Eunice Airport", "Eunice", "US", false], - "UCK": ["Lutsk Airport", "Lutsk", "UA", false], - "UCN": ["Buchanan Airport", "Buchanan", "LR", false], - "UCT": ["Ukhta Airport", "Ukhta", "RU", true], - "UCY": ["Everett-Stewart Regional Airport", "Union City", "US", false], - "UCZ": ["Uchiza Airport", "Uchiza", "PE", false], - "UDA": ["Undara Airport", "Undara", "AU", false], - "UDD": ["Bermuda Dunes Airport", "Bermuda Dunes", "US", false], - "UDE": ["Volkel Air Base", "Uden", "NL", false], - "UDI": ["Ten. Cel. Aviador César Bombonato Airport", "Uberlândia", "BR", true], - "UDJ": ["Uzhhorod International Airport", "Uzhhorod", "UA", false], - "UDR": ["Maharana Pratap Airport", "Udaipur", "IN", true], - "UEE": ["Queenstown Airport", "", "AU", false], - "UEL": ["Quelimane Airport", "Quelimane", "MZ", true], - "UEN": ["Urengoy Airport", "Urengoy", "RU", false], - "UEO": ["Kumejima Airport", "Kumejima", "JP", true], - "UES": ["Waukesha County Airport", "Waukesha", "US", false], - "UET": ["Quetta International Airport", "Quetta", "PK", true], - "UFA": ["Ufa International Airport", "Ufa", "RU", true], - "UGA": ["Bulgan Airport", "Bulgan", "MN", true], - "UGB": ["Ugashik Bay Airport", "Pilot Point", "US", false], - "UGC": ["Urgench International Airport", "Urgench", "UZ", true], - "UGI": ["San Juan /Uganik/ Seaplane Base", "San Juan", "US", true], - "UGL": ["Union Glacier Blue-Ice Runway", "Union Glacier", "AQ", false], - "UGN": ["Waukegan National Airport", "Chicago/Waukegan", "US", false], - "UGO": ["Uige Airport", "Uige", "AO", false], - "UGS": ["Ugashik Airport", "Ugashik", "US", false], - "UGT": ["Bulagtai Resort Airport", "Khankhongor", "MN", false], - "UGU": ["Bilorai Airport", "Bilogai", "ID", true], - "UHE": ["Kunovice Airport", "Uherské Hradiště", "CZ", false], - "UHS": ["Aleksandrovsk-Sakhalinskiy Airport", "Aleksandrovsk-Sakhalinskiy", "RU", false], - "UIB": ["El Caraño Airport", "Quibdó", "CO", true], - "UIH": ["Phu Cat Airport", "Quy Nohn", "VN", true], - "UII": ["Utila Airport", "Utila Island", "HN", true], - "UIK": ["Ust-Ilimsk Airport", "Ust-Ilimsk", "RU", false], - "UIL": ["Quillayute Airport", "Quillayute", "US", false], - "UIN": ["Quincy Regional Airport Baldwin Field", "Quincy", "US", true], - "UIO": ["Mariscal Sucre International Airport", "Quito", "EC", true], - "UIP": ["Quimper-Cornouaille Airport", "Quimper/Pluguffan", "FR", false], - "UIR": ["Quirindi Airport", "", "AU", false], - "UIT": ["Jaluit Airport", "Jabor Jaluit Atoll", "MH", true], - "UJE": ["Ujae Atoll Airport", "Ujae Atoll", "MH", true], - "UJN": ["Uljin Airport", "Bongsan-ri, Uljin", "KR", false], - "UJU": ["Uiju Airport", "Uiju", "KP", false], - "UKA": ["Ukunda Airstrip", "Ukunda", "KE", false], - "UKB": ["Kobe Airport", "Kobe", "JP", true], - "UKE": ["Utkela Airport", "Bhawanipatna", "IN", true], - "UKG": ["Ust-Kuyga Airport", "Ust-Kuyga", "RU", true], - "UKH": ["Mukhaizna Airport", "Mukhaizna Oil Field", "OM", false], - "UKI": ["Ukiah Municipal Airport", "Ukiah", "US", false], - "UKK": ["Oskemen International Airport", "Ust-Kamenogorsk (Oskemen)", "KZ", true], - "UKN": ["Waukon Municipal Airport", "Waukon", "US", false], - "UKR": ["Mukeiras Airport", "Mukayras", "YE", false], - "UKS": ["Sevastopol International Airport / Belbek Air Base", "Sevastopol", "UA", false], - "UKT": ["Quakertown Airport", "Quakertown", "US", false], - "UKU": ["Nuku Airport", "Nuku", "PG", false], - "UKX": ["Ust-Kut Airport", "Ust-Kut", "RU", true], - "ULA": ["Capitan D Daniel Vazquez Airport", "San Julian", "AR", false], - "ULB": ["Uléi Airport", "Ambrym Island", "VU", false], - "ULD": ["Prince Mangosuthu Buthelezi Airport", "Ulundi", "ZA", false], - "ULE": ["Sule Airport", "Sule", "PG", false], - "ULG": ["Ölgii Mongolei International Airport", "Ölgii", "MN", true], - "ULH": ["Al-Ula International Airport", "Al-Ula", "SA", true], - "ULI": ["Ulithi Airport", "Falalop Island", "FM", false], - "ULK": ["Lensk Airport", "Lensk", "RU", true], - "ULM": ["New Ulm Municipal Airport", "New Ulm", "US", false], - "ULN": ["Buyant-Ukhaa International Airport", "Ulaanbaatar", "MN", false], - "ULO": ["Ulaangom Airport", "Ulaangom", "MN", true], - "ULP": ["Quilpie Airport", "", "AU", true], - "ULQ": ["Heriberto Gíl Martínez Airport", "Tuluá", "CO", false], - "ULS": ["Mulatos Airport", "Mulatos", "CO", false], - "ULU": ["Gulu Airport", "Gulu", "UG", true], - "ULV": ["Ulyanovsk Baratayevka Airport", "Ulyanovsk", "RU", true], - "ULX": ["Ulusaba Airport", "Ulusaba", "ZA", false], - "ULY": ["Ulyanovsk Vostochny Airport", "Cherdakly", "RU", true], - "ULZ": ["Donoi Airport", "Uliastai", "MN", true], - "UMA": ["Alfredo Noa Díaz Airport", "Maisí", "CU", false], - "UMB": ["Kalumbila Airport", "Kalumbila", "ZM", false], - "UMC": ["Umba Airport", "Umba", "PG", false], - "UMD": ["Uummannaq Heliport", "Uummannaq", "GL", true], - "UME": ["Umeå Airport", "Umeå", "SE", true], - "UMI": ["Quincemil Air Base", "Quince Mil", "PE", false], - "UMM": ["Summit Airport", "Cantwell", "US", false], - "UMR": ["Woomera Airfield", "Woomera", "AU", false], - "UMS": ["Ust-Maya Airport", "Ust-Maya", "RU", true], - "UMT": ["Umiat Airport", "Umiat", "US", false], - "UMU": ["Orlando de Carvalho Airport", "Umuarama", "BR", true], - "UMY": ["Sumy Airport", "Sumy", "UA", false], - "UMZ": ["Mena Intermountain Municipal Airport", "Mena", "US", false], - "UNA": ["Una-Comandatuba Airport", "Una", "BR", true], - "UNC": ["Unguía Airport", "Unguía", "CO", false], - "UND": ["Kunduz Airport", "Kunduz", "AF", false], - "UNE": ["Qacha's Nek Airport", "Qacha's Nek", "LS", false], - "UNG": ["Kiunga Airport", "Kiunga", "PG", true], - "UNI": ["Union Island International Airport", "Union Island", "VC", true], - "UNK": ["Unalakleet Airport", "Unalakleet", "US", true], - "UNN": ["Ranong Airport", "Ranong", "TH", true], - "UNR": ["Öndörkhaan Airport", "Öndörkhaan", "MN", false], - "UNU": ["Dodge County Airport", "Juneau", "US", false], - "UOA": ["Moruroa Airport", "Moruroa Atoll", "PF", false], - "UOL": ["Buol - Pogogul Airport", "Buol", "ID", true], - "UOS": ["Franklin County Airport", "Sewanee", "US", false], - "UOX": ["University Oxford Airport", "Oxford", "US", false], - "UPB": ["Playa Baracoa Airport", "Havana", "CU", false], - "UPG": ["Sultan Hasanuddin International Airport", "Makassar", "ID", true], - "UPL": ["Upala Airport", "Upala", "CR", false], - "UPN": ["Uruapan - Licenciado y General Ignacio Lopez Rayon International Airport", "Uruapan", "MX", true], - "UPP": ["Upolu Airport", "Hawi", "US", false], - "UPR": ["Upiara Airport", "Upiara", "PG", false], - "UPV": ["Upavon Aerodrome", "Pewsey, Wiltshire", "GB", false], - "URA": ["Manshuk Mametova International Airport", "Uralsk", "KZ", true], - "URC": ["Ürümqi Tianshan International Airport", "Ürümqi", "CN", true], - "URD": ["Burg Feuerstein Airport", "Ebermannstadt", "DE", false], - "URE": ["Kuressaare Airport", "Kuressaare", "EE", true], - "URG": ["Rubem Berta Airport", "Uruguaiana", "BR", true], - "URI": ["Uribe Airport", "La Uribe", "CO", false], - "URJ": ["Uray Airport", "Uray", "RU", true], - "URM": ["Uriman Airport", "Urimán", "VE", false], - "URN": ["Urgun Airport", "Urgun", "AF", false], - "URO": ["Rouen Vallée de Seine Airport", "Boos", "FR", false], - "URR": ["Urrao Airport", "Urrao", "CO", false], - "URS": ["Kursk East Airport", "Kursk", "RU", false], - "URT": ["Surat Thani Airport", "Surat Thani", "TH", true], - "URU": ["Uroubi Airport", "Uroubi", "PG", false], - "URY": ["Gurayat Domestic Airport", "Gurayat", "SA", true], - "URZ": ["Orūzgān Airport", "Orūzgān", "AF", false], - "USA": ["Concord-Padgett Regional Airport", "Concord", "US", true], - "USC": ["Union County, Troy Shelton Field", "Union", "US", false], - "USH": ["Ushuaia - Malvinas Argentinas International Airport", "Ushuaia", "AR", true], - "USI": ["Mabaruma Airport", "Mabaruma", "GY", false], - "USJ": ["Usharal Airport", "Usharal", "KZ", true], - "USK": ["Usinsk Airport", "Usinsk", "RU", true], - "USL": ["Useless Loop Airport", "Useless Loop", "AU", false], - "USM": ["Samui International Airport", "Na Thon (Ko Samui Island)", "TH", true], - "USN": ["Ulsan Airport", "Ulsan", "KR", true], - "USQ": ["Uşak Airport", "Uşak", "TR", false], - "USR": ["Ust-Nera Airport", "Ust-Nera", "RU", true], - "USS": ["Sancti Spiritus Airport", "Sancti Spiritus", "CU", false], - "UST": ["Northeast Florida Regional Airport", "St Augustine", "US", true], - "USU": ["Francisco B. Reyes (Busuanga) Airport", "Coron", "PH", true], - "UTA": ["Mutare Airport", "Mutare", "ZW", false], - "UTB": ["Muttaburra Airport", "", "AU", false], - "UTD": ["Nutwood Downs Airport", "Nutwood Downs", "AU", false], - "UTE": ["Bultfontein Airport", "Bultfontein", "ZA", false], - "UTG": ["Quthing Airport", "Quthing", "LS", false], - "UTH": ["Udon Thani International Airport", "Udon Thani", "TH", true], - "UTI": ["Utti Air Base", "Utti / Valkeala", "FI", false], - "UTK": ["Utirik Airport", "Utirik Island", "MH", true], - "UTM": ["Tunica Municipal Airport", "Tunica", "US", false], - "UTN": ["Upington Airport", "Upington", "ZA", true], - "UTO": ["Indian Mountain LRRS Airport", "Utopia Creek", "US", true], - "UTP": ["U-Tapao–Rayong–Pattaya International Airport", "Rayong", "TH", true], - "UTS": ["Ust-Tsylma Airport", "Ust-Tsylma", "RU", false], - "UTT": ["K. D. Matanzima Airport", "Mthatha", "ZA", true], - "UTU": ["Ustupu Airport", "Ustupu", "PA", false], - "UTW": ["Queenstown Airport", "Queenstown", "ZA", false], - "UUA": ["Bugulma Airport", "Bugulma", "RU", true], - "UUD": ["Baikal International Airport", "Ulan Ude", "RU", true], - "UUK": ["Ugnu-Kuparuk Airport", "Kuparuk", "US", false], - "UUN": ["Baruun Urt Airport", "", "MN", false], - "UUS": ["Yuzhno-Sakhalinsk International Airport", "Yuzhno-Sakhalinsk", "RU", true], - "UUU": ["Manumu Airport", "Manumu", "PG", false], - "UVA": ["Garner Field", "Uvalde", "US", false], - "UVE": ["Ouvéa Airport", "Ouvéa", "NC", true], - "UVF": ["Hewanorra International Airport", "Vieux Fort", "LC", true], - "UVI": ["José Cleto Airport", "União da Vitória", "BR", true], - "UVL": ["El Kharja Airport", "Kharja Oases", "EG", false], - "UVO": ["Uvol Airport", "Uvol", "PG", false], - "UWA": ["Ware Airport", "Ware", "US", false], - "UYL": ["Nyala Airport", "Nyala", "SD", true], - "UYN": ["Yulin Yuyang Airport", "Yulin", "CN", true], - "UYU": ["Joya Andina International Airport", "Quijarro", "BO", true], - "UZC": ["Ponikve Airport", "Stapari", "RS", false], - "UZM": ["Hope Bay Aerodrome", "Hope Bay", "CA", false], - "UZR": ["Urzhar Airport", "Urzhar", "KZ", true], - "UZU": ["Curuzu Cuatia Airport", "Curuzu Cuatia", "AR", false], - "VAA": ["Vaasa Airport", "Vaasa", "FI", true], - "VAB": ["Yavarate Airport", "Yavarate", "CO", false], - "VAC": ["Varrelbusch Airfield", "Cloppenburg", "DE", false], - "VAD": ["Moody Air Force Base", "Valdosta", "US", false], - "VAF": ["Valence-Chabeuil Airport", "Chabeuil, Drôme", "FR", false], - "VAG": ["Major Brigadeiro Trompowsky Airport", "Varginha", "BR", false], - "VAH": ["Capitán Av. Vidal Villagomez Toledo Airport", "Vallegrande", "BO", false], - "VAI": ["Vanimo Airport", "Vanimo", "PG", true], - "VAK": ["Chevak Airport", "Chevak", "US", true], - "VAL": ["Valença Airport", "Valença", "BR", true], - "VAM": ["Villa International Airport Maamigili", "Maamigili", "MV", true], - "VAN": ["Van Ferit Melen Airport", "Van", "TR", true], - "VAO": ["Suavanao Airport", "Suavanao", "SB", true], - "VAP": ["Rodelillo Airport", "Viña Del Mar", "CL", false], - "VAQ": ["Vanavara Airport", "Vanavara", "RU", true], - "VAR": ["Varna Airport", "Varna", "BG", true], - "VAS": ["Sivas Nuri Demirağ Airport", "Sivas", "TR", true], - "VAT": ["Vatomandry Airport", "Vatomandry", "MG", false], - "VAV": ["Vava'u International Airport", "Vava'u Island", "TO", true], - "VAW": ["Vardø Airport, Svartnes", "Vardø", "NO", true], - "VBA": ["Ann Airport", "Aeng", "MM", false], - "VBC": ["Chanmyathazi Airport", "Mandalay", "MM", false], - "VBG": ["Vandenberg Space Force Base", "Lompoc", "US", false], - "VBP": ["Bokpyinn Airport", "Bokpyinn", "MM", false], - "VBS": ["Brescia Gabriele d'Annunzio Airport", "Montichiari (BS)", "IT", true], - "VBV": ["Vanua Balavu Airport", "Vanua Balavu", "FJ", true], - "VBY": ["Visby Airport", "Visby", "SE", true], - "VCA": ["Can Tho International Airport", "Can Tho", "VN", true], - "VCC": ["Vacaria Airport", "Vacaria", "BR", false], - "VCD": ["Victoria River Downs Airport", "Victoria River", "AU", false], - "VCE": ["Venice Marco Polo Airport", "Venezia (VE)", "IT", true], - "VCF": ["Valcheta Airport", "Valcheta", "AR", false], - "VCH": ["Vichadero Airport", "Vichadero", "UY", false], - "VCL": ["Chu Lai Airport", "Tam Nghĩa", "VN", true], - "VCP": ["Viracopos International Airport", "Campinas", "BR", true], - "VCS": ["Con Dao Airport", "Con Dao", "VN", true], - "VCT": ["Victoria Regional Airport", "Victoria", "US", true], - "VCV": ["Southern California Logistics Airport", "Victorville", "US", false], - "VDA": ["Ovda Airport", "Eilat", "IL", false], - "VDC": ["Glauber de Andrade Rocha Airport", "Vitória da Conquista", "BR", true], - "VDE": ["El Hierro Airport", "El Hierro Island", "ES", true], - "VDH": ["Dong Hoi Airport", "Dong Hoi", "VN", true], - "VDI": ["Vidalia Regional Airport", "Vidalia", "US", false], - "VDM": ["Gobernador Castello Airport", "Viedma / Carmen de Patagones", "AR", true], - "VDO": ["Van Don International Airport", "Van Don", "VN", true], - "VDP": ["Valle de La Pascua Airport", "", "VE", false], - "VDR": ["Villa Dolores Airport", "Villa Dolores", "AR", false], - "VDS": ["Vadsø Airport", "Vadsø", "NO", true], - "VDY": ["Jindal Vijaynagar Airport", "Toranagallu", "IN", false], - "VDZ": ["Valdez Pioneer Field", "Valdez", "US", true], - "VEE": ["Venetie Airport", "Venetie", "US", true], - "VEG": ["Maikwak Airport", "Maikwak", "GY", false], - "VEL": ["Vernal Regional Airport", "Vernal", "US", true], - "VEO": ["Severo-Yeniseysk Airport", "Severo-Yeniseysk", "RU", true], - "VER": ["General Heriberto Jara International Airport", "Veracruz", "MX", true], - "VEV": ["Barakoma Airport", "Barakoma", "SB", false], - "VEX": ["Tioga Municipal Airport", "Tioga", "US", false], - "VEY": ["Vestmannaeyjar Airport", "Vestmannaeyjar", "IS", false], - "VFA": ["Victoria Falls International Airport", "Victoria Falls", "ZW", true], - "VGA": ["Vijayawada International Airport", "Vijayawada", "IN", true], - "VGD": ["Vologda Airport", "Vologda", "RU", false], - "VGO": ["Vigo Airport", "Vigo", "ES", true], - "VGS": ["General Villegas Airport", "General Villegas", "AR", false], - "VGT": ["North Las Vegas Airport", "Las Vegas", "US", false], - "VGZ": ["Villa Garzón Airport", "Villa Garzón", "CO", false], - "VHC": ["Saurimo Airport", "Saurimo", "AO", false], - "VHM": ["Vilhelmina South Lapland Airport", "Vilhelmina", "SE", true], - "VHN": ["Culberson County Airport", "Van Horn", "US", false], - "VHV": ["Verkhnevilyuisk Airport", "Verkhnevilyuisk", "RU", true], - "VHY": ["Vichy-Charmeil Airport", "Charmeil, Allier", "FR", false], - "VHZ": ["Vahitahi Airport", "Vahitahi", "PF", false], - "VIA": ["Videira Airport", "Videira", "BR", false], - "VIE": ["Vienna International Airport", "Vienna", "AT", true], - "VIG": ["Juan Pablo Pérez Alfonso Airport", "El Vigía", "VE", true], - "VIH": ["Rolla National Airport", "Vichy", "US", false], - "VII": ["Vinh Airport", "Vinh", "VN", true], - "VIJ": ["Virgin Gorda Airport", "Spanish Town", "VG", true], - "VIL": ["Dakhla Airport", "Dakhla", "EH", true], - "VIN": ["Vinnytsia/Gavyryshivka International Airport", "Vinnitsa", "UA", false], - "VIP": ["Payerne Air Base", "Payerne", "CH", false], - "VIQ": ["Viqueque Airport", "Viqueque", "TL", false], - "VIR": ["Virginia Airport", "Durban", "ZA", false], - "VIS": ["Visalia Municipal Airport", "Visalia", "US", false], - "VIT": ["Vitoria Airport", "Alava", "ES", true], - "VIU": ["Viru Harbour Airstrip", "Viru", "SB", false], - "VIV": ["Vivigani Airfield", "Vivigani", "PG", false], - "VIX": ["Eurico de Aguiar Salles International Airport", "Vitória", "BR", true], - "VIY": ["Vélizy-Villacoublay Air Base", "Vélizy-Villacoublay, Yvelines", "FR", false], - "VJB": ["Xai-Xai Chongoene Airport", "Xai-Xai", "MZ", true], - "VJI": ["Virginia Highlands Airport", "Abingdon", "US", false], - "VJQ": ["Gurue Airport", "Gurue", "MZ", false], - "VKG": ["Rach Gia Airport", "Rach Gia", "VN", true], - "VKO": ["Vnukovo International Airport", "Moscow", "RU", true], - "VKS": ["Vicksburg Municipal Airport", "Vicksburg", "US", false], - "VKT": ["Vorkuta Airport", "Vorkuta", "RU", true], - "VKV": ["Vaskovo Airport", "Arkhangelsk", "RU", false], - "VLA": ["Vandalia Municipal Airport", "Vandalia", "US", false], - "VLC": ["Valencia Airport", "Valencia", "ES", true], - "VLD": ["Valdosta Regional Airport", "Valdosta", "US", true], - "VLE": ["Valle Airport", "Grand Canyon", "US", false], - "VLG": ["Villa Gesell Airport", "Villa Gesell", "AR", false], - "VLI": ["Bauerfield International Airport", "Port Vila", "VU", true], - "VLL": ["Valladolid Airport", "Valladolid", "ES", true], - "VLM": ["Teniente Coronel Rafael Pabón Airport", "Villamontes", "BO", false], - "VLN": ["Arturo Michelena International Airport", "Valencia", "VE", true], - "VLP": ["Vila Rica Airport", "Vila Rica", "BR", false], - "VLR": ["Vallenar Airport", "Vallenar", "CL", false], - "VLS": ["Valesdir Airport", "Epi Island", "VU", true], - "VLU": ["Velikiye Luki Airport", "Velikiye Luki", "RU", false], - "VLV": ["Dr. Antonio Nicolás Briceño Airport", "Valera", "VE", true], - "VLY": ["Anglesey Airport", "Angelsey", "GB", false], - "VME": ["Villa Reynolds Airport", "Villa Mercedes", "AR", false], - "VMI": ["Aeropuerto Nacional Doctor Juan Plate", "Puerto Vallemi", "PY", false], - "VMU": ["Baimuru Airport", "Baimuru", "PG", true], - "VNA": ["Salavan Airport", "Salavan", "LA", false], - "VNC": ["Venice Municipal Airport", "Venice", "US", false], - "VND": ["Vangaindrano Airport", "Vangaindrano", "MG", false], - "VNE": ["Vannes-Meucon Airport", "Vannes/Meucon", "FR", false], - "VNO": ["Vilnius International Airport", "Vilnius", "LT", true], - "VNR": ["Vanrook Station Airport", "", "AU", false], - "VNS": ["Lal Bahadur Shastri International Airport", "Varanasi", "IN", true], - "VNT": ["Ventspils International Airport", "Ventspils", "LV", false], - "VNX": ["Vilankulo Airport", "Vilanculo", "MZ", true], - "VNY": ["Van Nuys Airport", "Van Nuys", "US", false], - "VOD": ["Vodochody Airport", "Vodochody", "CZ", false], - "VOG": ["Volgograd International Airport", "Volgograd", "RU", true], - "VOH": ["Vohemar Airport", "Vohemar", "MG", false], - "VOI": ["Voinjama Airport", "Voinjama", "LR", false], - "VOK": ["Volk Field", "Camp Douglas", "US", false], - "VOL": ["Nea Anchialos National Airport", "Nea Anchialos", "GR", true], - "VOT": ["Domingos Pignatari Airport", "Votuporanga", "BR", false], - "VOZ": ["Voronezh International Airport", "Voronezh", "RU", true], - "VPE": ["Ngjiva Pereira Airport", "Ngiva", "AO", true], - "VPG": ["Vipingo Estate Airport", "Kikambala", "KE", false], - "VPN": ["Vopnafjörður Airport", "Vopnafjörður", "IS", true], - "VPS": ["Destin-Fort Walton Beach Airport", "Valparaiso", "US", true], - "VPY": ["Chimoio Airport", "Chimoio", "MZ", true], - "VPZ": ["Porter County Municipal Airport", "Valparaiso", "US", false], - "VQQ": ["Cecil Airport", "Jacksonville", "US", false], - "VQS": ["Antonio Rivera Rodriguez Airport", "Vieques", "PR", true], - "VRA": ["Juan Gualberto Gomez International Airport", "Matanzas", "CU", true], - "VRB": ["Vero Beach Regional Airport", "Vero Beach", "US", true], - "VRC": ["Virac Airport", "Virac", "PH", true], - "VRE": ["Vredendal Airport", "Vredendal", "ZA", false], - "VRI": ["Varandey Airport", "Varandey", "RU", false], - "VRK": ["Varkaus Airport", "Varkaus / Joroinen", "FI", false], - "VRL": ["Vila Real Airport", "Vila Real", "PT", true], - "VRN": ["Verona Villafranca Valerio Catullo Airport", "Caselle (VR)", "IT", true], - "VRO": ["Kawama Airport", "Santa Marta", "CU", false], - "VRS": ["Roy Otten Memorial Airfield", "Versailles", "US", false], - "VRU": ["Vryburg Airport", "Vryburg", "ZA", false], - "VRY": ["Værøy Heliport", "Værøy", "NO", true], - "VRZ": ["Padre Israel Airport", "Lavras", "BR", false], - "VSA": ["Carlos Rovirosa Pérez International Airport", "Villahermosa", "MX", true], - "VSE": ["Aerodromo Goncalves Lobato (Viseu Airport)", "Viseu", "PT", true], - "VSF": ["Hartness State Airport", "North Springfield", "US", false], - "VST": ["Stockholm Västerås Airport", "Stockholm / Västerås", "SE", true], - "VSV": ["Shravasti Airport", "Shravasti", "IN", true], - "VTB": ["Vitebsk Vostochny Airport", "Vitebsk", "BY", false], - "VTE": ["Wattay International Airport", "Vientiane", "LA", true], - "VTF": ["Vatulele Airport", "Vatulele", "FJ", false], - "VTG": ["Vung Tau Airport", "Vũng Tàu", "VN", false], - "VTM": ["Nevatim Air Base", "Beersheba", "IL", false], - "VTN": ["Miller Field", "Valentine", "US", false], - "VTU": ["Hermanos Ameijeiras Airport", "Las Tunas", "CU", true], - "VTZ": ["Alluri Sitarama Raju International Airport (Vizag)", "Visakhapatnam", "IN", true], - "VUP": ["Alfonso López Pumarejo Airport", "Valledupar", "CO", true], - "VUS": ["Velikiy Ustyug Airport", "Velikiy Ustyug", "RU", true], - "VUU": ["Mvuu Camp Airport", "Liwonde National Park", "MW", false], - "VVB": ["Mahanoro Airport", "Mahanoro", "MG", false], - "VVC": ["Vanguardia Airport", "Villavicencio", "CO", true], - "VVI": ["Viru Viru International Airport", "Santa Cruz", "BO", true], - "VVK": ["Västervik Airport", "Västervik", "SE", false], - "VVN": ["Las Malvinas/Echarate Airport", "Las Malvinas", "PE", false], - "VVO": ["Vladivostok International Airport", "Artyom", "RU", true], - "VVZ": ["Illizi Takhamalt Airport", "Illizi", "DZ", true], - "VXC": ["Lichinga Airport", "Lichinga", "MZ", true], - "VXE": ["Cesaria Evora International Airport", "São Pedro", "CV", true], - "VXO": ["Växjö Kronoberg Airport", "Växjö", "SE", true], - "VYD": ["Vryheid Airport", "Vryheid", "ZA", false], - "VYI": ["Vilyuisk Airport", "Vilyuisk", "RU", true], - "VYS": ["Illinois Valley Regional Airport Walter A Duncan Field", "Peru", "US", false], - "WAA": ["Wales Airport", "Wales", "US", true], - "WAD": ["Andriamena Airport", "Andriamena", "MG", false], - "WAE": ["Wadi Al Dawasir Domestic Airport", "Wadi Al Dawasir", "SA", true], - "WAF": ["Wana Airport", "Waana", "PK", false], - "WAG": ["Wanganui Airport", "Wanganui", "NZ", true], - "WAH": ["Harry Stern Airport", "Wahpeton", "US", false], - "WAI": ["Ambalabe Airport", "Antsohihy", "MG", false], - "WAJ": ["Wawoi Falls Airport", "Wavoi Falls", "PG", false], - "WAK": ["Ankazoabo Airport", "Ankazoabo", "MG", false], - "WAL": ["Wallops Flight Facility Airport", "Wallops Island", "US", false], - "WAM": ["Ambatondrazaka Airport", "Ambatondrazaka", "MG", false], - "WAO": ["Wabo Airport", "Wabo", "PG", false], - "WAP": ["Alto Palena Airport", "Palena", "CL", false], - "WAQ": ["Antsalova Airport", "Antsalova", "MG", false], - "WAT": ["Waterford Airport", "Waterford", "IE", false], - "WAU": ["Wauchope Airport", "Wauchope", "AU", false], - "WAV": ["Wave Hill Airport", "", "AU", false], - "WAW": ["Warsaw Chopin Airport", "Warsaw", "PL", true], - "WAX": ["Zuwarah International Airport", "Zuwarah", "LY", false], - "WAY": ["Greene County Airport", "Waynesburg", "US", false], - "WAZ": ["Warwick Airport", "", "AU", false], - "WBA": ["Wahai Airport", "Wahai", "ID", false], - "WBB": ["Stebbins Airport", "Stebbins", "US", true], - "WBC": ["Wapolu Airport", "Wapolu", "PG", false], - "WBD": ["Avaratra Airport", "Befandriana", "MG", false], - "WBE": ["Ankaizina Airport", "Bealanana", "MG", false], - "WBG": ["Schleswig Air Base", "Jagel", "DE", false], - "WBK": ["West Branch Community Airport", "West Branch", "US", false], - "WBM": ["Wapenamanda Airport", "Wapenamanda", "PG", true], - "WBO": ["Antsoa Airport", "Beroroha", "MG", false], - "WBQ": ["Beaver Airport", "Beaver", "US", true], - "WBR": ["Roben Hood Airport", "Big Rapids", "US", false], - "WBU": ["Boulder Municipal Airport", "Boulder", "US", false], - "WBW": ["Wilkes Barre Wyoming Valley Airport", "Wilkes-Barre", "US", false], - "WCA": ["Gamboa Airport", "Castro", "CL", false], - "WCD": ["Carosue Dam Airport", "Cundeelee", "AU", false], - "WCH": ["Nuevo Chaitén Airport", "Chaitén", "CL", false], - "WCR": ["Chandalar Lake Airport", "Chandalar Lake", "US", false], - "WDA": ["Al Ain Airport Ain District, Shabwah Governorate, Yemen", "Ain District", "YE", false], - "WDB": ["Weda Bay Airport", "Weda", "ID", false], - "WDG": ["Enid Woodring Regional Airport", "Enid", "US", false], - "WDH": ["Hosea Kutako International Airport", "Windhoek", "NA", true], - "WDI": ["Wondai Airport", "", "AU", false], - "WDN": ["Waldron Airstrip", "Eastsound", "US", true], - "WDR": ["Barrow County Airport", "Winder", "US", false], - "WDS": ["Shiyan Wudangshan Airport", "Shiyan (Maojian)", "CN", true], - "WEA": ["Parker County Airport", "Weatherford", "US", false], - "WED": ["Wedau Airport", "Wedau", "PG", false], - "WEF": ["Weifang Nanyuan Airport", "Weifang (Kuiwen)", "CN", true], - "WEH": ["Weihai Dashuibo Airport", "Weihai", "CN", true], - "WEI": ["Weipa Airport", "Weipa", "AU", true], - "WEL": ["Welkom Airport", "Welkom", "ZA", false], - "WEP": ["Weam Airport", "Weam", "PG", false], - "WET": ["Deiyai / Waghete Baru Airport", "Tigi", "ID", false], - "WEW": ["Wee Waa Airport", "", "AU", false], - "WFB": ["Ketchikan Harbor Seaplane Base", "Ketchikan", "US", true], - "WFI": ["Fianarantsoa Airport", "Fianarantsoa", "MG", false], - "WFK": ["Northern Aroostook Regional Airport", "Frenchville", "US", false], - "WGA": ["Wagga Wagga Airport", "Forest Hill", "AU", true], - "WGB": ["Bahawalnagar Airport", "Bahawalnagar", "PK", false], - "WGC": ["Warangal Airport", "Warangal", "IN", false], - "WGE": ["Walgett Airport", "", "AU", true], - "WGN": ["Shaoyang Wugang Airport", "Shaoyang (Wugang)", "CN", true], - "WGO": ["Winchester Regional Airport", "Winchester", "US", false], - "WGP": ["Umbu Mehang Kunda Airport", "Waingapu-Sumba Island", "ID", true], - "WGT": ["Wangaratta Airport", "Laceby", "AU", false], - "WHA": ["Wuhu Xuanzhou Airport", "Wuhu", "CN", true], - "WHB": ["Eliwana", "", "AU", false], - "WHD": ["Hyder Seaplane Base", "Hyder", "US", true], - "WHF": ["Wadi Halfa Airport", "Wadi Halfa", "SD", false], - "WHK": ["Whakatāne Airport", "Whakatāne", "NZ", true], - "WHL": ["Welshpool Airport", "Welshpool", "AU", false], - "WHN": ["Wuhan Hannan Municipal Airport", "Wuhan (Hannan)", "CN", false], - "WHO": ["Franz Josef Aerodrome", "", "NZ", false], - "WHP": ["Whiteman Airport", "Pacoima", "US", false], - "WHS": ["Whalsay Airstrip", "Whalsay, Shetlands", "GB", false], - "WHT": ["Wharton Regional Airport", "Wharton", "US", false], - "WHU": ["Wuhu Wanli Airport / Wuhu Air Base", "Wuhu", "CN", false], - "WIB": ["Wilbarger County Airport", "Vernon", "US", false], - "WIC": ["Wick John O'Groats Airport", "Wick", "GB", true], - "WIE": ["Wiesbaden Army Airfield", "Wiesbaden", "DE", false], - "WIK": ["Waiheke Island Aerodrome", "Waiheke Island", "NZ", false], - "WIL": ["Nairobi Wilson Airport", "Nairobi", "KE", true], - "WIN": ["Winton Airport", "", "AU", true], - "WIO": ["Wilcannia Airport", "", "AU", false], - "WIR": ["Wairoa Airport", "Wairoa", "NZ", false], - "WIU": ["Witu Airport", "Garove Island", "PG", false], - "WJA": ["Woja Airport", "Woja", "MH", false], - "WJF": ["General William J Fox Airfield", "Lancaster", "US", false], - "WJR": ["Wajir Airport", "Wajir", "KE", true], - "WJU": ["Wonju Airport / Hoengseong Air Base (K-38/K-46)", "Wonju", "KR", true], - "WKA": ["Wanaka Airport", "Wanaka", "NZ", true], - "WKB": ["Warracknabeal Airport", "", "AU", false], - "WKF": ["Waterkloof Air Force Base", "Pretoria", "ZA", false], - "WKI": ["Hwange (Town) Airport", "Hwange", "ZW", false], - "WKJ": ["Wakkanai Airport", "Wakkanai", "JP", true], - "WKK": ["Aleknagik / New Airport", "Aleknagik", "US", true], - "WKL": ["Waikoloa Heliport", "Waikoloa Village", "US", false], - "WKN": ["Wakunai Airport", "Wakunai", "PG", false], - "WKR": ["Walkers Cay Airport", "Walkers Cay", "BS", false], - "WLA": ["Wallal Downs Airport", "Eighty Mile Beach", "AU", false], - "WLC": ["Walcha Airport", "", "AU", false], - "WLD": ["Strother Field", "Winfield / Arkansas City", "US", false], - "WLE": ["Miles Airport", "", "AU", false], - "WLG": ["Wellington International Airport", "Wellington", "NZ", true], - "WLH": ["Walaha Airport", "Walaha", "VU", true], - "WLK": ["Selawik Airport", "Selawik", "US", true], - "WLL": ["Wollogorang Airport", "", "AU", false], - "WLO": ["Waterloo Airport", "", "AU", false], - "WLP": ["West Angelas Airport", "", "AU", false], - "WLR": ["Loring Seaplane Base", "Loring", "US", false], - "WLS": ["Hihifo Airport", "Wallis Island", "WF", true], - "WLU": ["Walluanda Airport", "Puri", "PG", false], - "WLW": ["Willows Glenn County Airport", "Willows", "US", false], - "WMA": ["Mandritsara Airport", "Mandritsara", "MG", false], - "WMB": ["Warrnambool Airport", "", "AU", false], - "WMC": ["Winnemucca Municipal Airport", "Winnemucca", "US", false], - "WMD": ["Mandabe Airport", "Mandabe", "MG", false], - "WME": ["Mount Keith Airport", "", "AU", false], - "WMH": ["Ozark Regional Airport", "Mountain Home", "US", false], - "WMI": ["Warsaw Modlin Airport", "Nowy Dwór Mazowiecki", "PL", true], - "WMK": ["Meyers Chuck Seaplane Base", "Meyers Chuck", "US", false], - "WML": ["Malaimbandy Airport", "Malaimbandy", "MG", false], - "WMN": ["Maroantsetra Airport", "Maroantsetra", "MG", true], - "WMO": ["White Mountain Airport", "White Mountain", "US", true], - "WMP": ["Mampikony Airport", "Mampikony", "MG", false], - "WMR": ["Mananara Nord Airport", "Mananara Nord", "MG", false], - "WMT": ["Zunyi Maotai Airport", "Zunyi", "CN", true], - "WMX": ["Wamena Airport", "Wamena", "ID", true], - "WNA": ["Napakiak Airport", "Napakiak", "US", true], - "WND": ["Windarra Airport", "Laverton", "AU", false], - "WNH": ["Wenshan Puzhehei Airport", "Wenshan", "CN", true], - "WNI": ["Matahora Airport", "Wangi-wangi Island", "ID", true], - "WNN": ["Wunnumin Lake Airport", "Wunnumin Lake", "CA", true], - "WNP": ["Naga Airport", "Naga", "PH", true], - "WNR": ["Windorah Airport", "Windorah", "AU", true], - "WNS": ["Shaheed Benazirabad Airport", "Nawabashah", "PK", true], - "WNU": ["Wanuma Airport", "Wanuma", "PG", false], - "WNZ": ["Wenzhou Longwan International Airport", "Wenzhou (Longwan)", "CN", true], - "WOA": ["Wonenara Airport", "Wonenara", "PG", false], - "WOE": ["Woensdrecht Air Base", "Hoogerheide", "NL", false], - "WOK": ["Uon Quen Airport", "Uonquén", "VE", false], - "WOL": ["Shellharbour Airport", "Albion Park Rail", "AU", false], - "WON": ["Wondoola Airport", "Wondoola", "AU", false], - "WOS": ["Wonsan Kalma Airport", "Wonsan", "KP", true], - "WOT": ["Wang'an Airport", "Wang'an", "TW", false], - "WOW": ["Willow Airport", "Willow", "US", false], - "WPA": ["Cabo Juan Román Airfield", "Puerto Aysén", "CL", false], - "WPB": ["Port Bergé Airport", "Port Bergé", "MG", false], - "WPC": ["Pincher Creek Airport", "Pincher Creek", "CA", false], - "WPK": ["Wrotham Park Airport", "Wrotham Park", "AU", false], - "WPL": ["Powell Lake Seaplane Base", "Powell River", "CA", true], - "WPM": ["Wipim Airport", "Wipim", "PG", false], - "WPO": ["North Fork Valley Airport", "Paonia", "US", false], - "WPR": ["Captain Fuentes Martinez Airport", "Porvenir", "CL", false], - "WPU": ["Guardia Marina Zañartu Airport", "Puerto Williams", "CL", false], - "WRA": ["Warder Airport", "Warder", "ET", false], - "WRB": ["Robins Air Force Base", "Warner Robins", "US", false], - "WRE": ["Whangarei Airport", "Whangarei", "NZ", true], - "WRG": ["Wrangell Airport", "Wrangell", "US", true], - "WRH": ["Wuerhe Airport", "Orku", "CN", false], - "WRI": ["Mc Guire Air Force Base", "Wrightstown", "US", false], - "WRL": ["Worland Municipal Airport", "Worland", "US", false], - "WRN": ["Windarling Airport", "Windarling Mine", "AU", false], - "WRO": ["Copernicus Wrocław Airport", "Wrocław", "PL", true], - "WRT": ["Warton Aerodrome", "Warton", "GB", false], - "WRW": ["Warrawagine Airport", "", "AU", false], - "WRY": ["Westray Airport", "Westray, Orkney Islands", "GB", true], - "WRZ": ["Weerawila Airport", "Weerawila", "LK", false], - "WSA": ["Wasua Airport", "Wasua", "PG", false], - "WSB": ["Steamboat Bay Seaplane Base", "Steamboat Bay", "US", false], - "WSF": ["Cape Sarichef Airport", "Cape Sarichef", "US", false], - "WSG": ["Washington County Airport", "Washington", "US", false], - "WSH": ["Brookhaven Calabro Airport", "Shirley", "US", false], - "WSI": ["Western Sydney International (Nancy-Bird Walton) Airport", "Sydney", "AU", false], - "WSK": ["Chongqing Wushan Airport", "Wushan", "CN", true], - "WSM": ["Wiseman Airport", "Wiseman", "US", false], - "WSN": ["South Naknek Number 2 Airport", "South Naknek", "US", true], - "WSO": ["Washabo Airport", "Washabo", "SR", false], - "WSP": ["Waspam Airport", "Waspam", "NI", false], - "WSR": ["Wasior Airport", "Wasior", "ID", false], - "WST": ["Westerly State Airport", "Westerly", "US", true], - "WSU": ["Wasu Airport", "Wasu", "PG", false], - "WSX": ["Westsound/WSX Seaplane Base", "West Sound", "US", true], - "WSY": ["Shute Harbour/Whitsunday Airport", "Shute Harbour", "AU", false], - "WSZ": ["Westport Airport", "Westport", "NZ", true], - "WTA": ["Tambohorano Airport", "Tambohorano", "MG", true], - "WTB": ["Toowoomba Wellcamp Airport", "Toowoomba", "AU", true], - "WTD": ["West End Airport", "West End", "BS", false], - "WTE": ["Wotje Airport", "Wotje", "MH", true], - "WTK": ["Noatak Airport", "Noatak", "US", true], - "WTL": ["Tuntutuliak Airport", "Tuntutuliak", "US", true], - "WTN": ["RAF Waddington", "Lincoln, Lincolnshire", "GB", false], - "WTO": ["Wotho Island Airport", "Wotho Island", "MH", true], - "WTP": ["Woitape Airport", "Fatima Mission", "PG", false], - "WTR": ["Whiteriver Airport", "Whiteriver", "US", false], - "WTS": ["Tsiroanomandidy Airport", "Tsiroanomandidy", "MG", false], - "WTT": ["Wantoat Airport", "Wantoat", "PG", false], - "WTZ": ["Whitianga Airport", "Whitianga", "NZ", false], - "WUA": ["Wuhai Airport", "Wuhai", "CN", true], - "WUD": ["Wudinna Airport", "", "AU", false], - "WUG": ["Wau Airport", "Wau", "PG", false], - "WUH": ["Wuhan Tianhe International Airport", "Wuhan (Huangpi)", "CN", true], - "WUI": ["Murrin Murrin Airport", "Murrin Murrin", "AU", false], - "WUM": ["Wasum Airport", "Wasum", "PG", false], - "WUN": ["Wiluna Airport", "", "AU", true], - "WUS": ["Nanping Wuyishan Airport", "Wuyishan", "CN", true], - "WUT": ["Xinzhou Wutaishan Airport", "Xinzhou", "CN", true], - "WUU": ["Wau Airport", "Wau", "SS", true], - "WUV": ["Wuvulu Island Airport", "Wuvulu Island", "PG", false], - "WUX": ["Sunan Shuofang International Airport", "Wuxi", "CN", true], - "WUZ": ["Wuzhou Xijiang Airport", "Tangbu", "CN", true], - "WVB": ["Walvis Bay International Airport", "Walvis Bay(Rooikop)", "NA", true], - "WVI": ["Watsonville Municipal Airport", "Watsonville", "US", false], - "WVK": ["Manakara Airport", "Manakara", "MG", false], - "WVL": ["Waterville Robert Lafleur Airport", "Waterville", "US", false], - "WVN": ["JadeWeser Airport", "Wilhelmshaven", "DE", false], - "WWA": ["Wasilla Airport", "Wasilla", "US", false], - "WWD": ["Cape May County Airport", "Wildwood", "US", false], - "WWI": ["Woodie Woodie Airport", "Woodie Woodie", "AU", false], - "WWK": ["Wewak International Airport", "Wewak", "PG", true], - "WWP": ["Whale Pass Seaplane Float Harbor Facility", "Whale Pass", "US", true], - "WWR": ["West Woodward Airport", "Woodward", "US", false], - "WWT": ["Mertarvik Airport", "Mertarvik", "US", true], - "WWY": ["West Wyalong Airport", "West Wyalong", "AU", false], - "WXN": ["Wanzhou Wuqiao Airport", "Wanzhou", "CN", true], - "WYA": ["Whyalla Airport", "Whyalla", "AU", true], - "WYB": ["Yes Bay Lodge Seaplane Base", "Yes Bay", "US", false], - "WYE": ["Yengema Airport", "Yengema", "SL", false], - "WYN": ["Wyndham Airport", "", "AU", false], - "WYS": ["Yellowstone Airport", "West Yellowstone", "US", true], - "WZA": ["Wa Airport", "Wa", "GH", false], - "WZQ": ["Urad Middle Banner Airport", "Urad Middle Banner", "CN", false], - "XAI": ["Xinyang Minggang Airport", "Xinyang", "CN", true], - "XAL": ["Álamos Airport", "Álamos", "MX", false], - "XAP": ["Serafin Enoss Bertaso Airport", "Chapecó", "BR", true], - "XAR": ["Aribinda Airport", "Aribinda", "BF", false], - "XAU": ["Saül Airport", "Saül", "GF", false], - "XBB": ["Blubber Bay Seaplane Base", "Blubber Bay", "CA", false], - "XBE": ["Bearskin Lake Airport", "Bearskin Lake", "CA", true], - "XBG": ["Bogande Airport", "Bogande", "BF", false], - "XBJ": ["Birjand International Airport", "Birjand", "IR", true], - "XBN": ["Biniguni Airport", "Biniguni", "PG", false], - "XBO": ["Boulsa Airport", "Boulsa", "BF", false], - "XBR": ["Brockville - Thousand Islands Regional Tackaberry Airport", "Brockville", "CA", false], - "XCH": ["Christmas Island International Airport", "Flying Fish Cove", "CX", true], - "XCL": ["Cluff Lake Airport", "Cluff Lake", "CA", false], - "XCM": ["Chatham Kent Airport", "Chatham-Kent", "CA", false], - "XCO": ["Colac Airport", "Colac Otway Shire", "AU", false], - "XCR": ["Chalons Vatry airport", "Chalons en Champagne", "FR", true], - "XDE": ["Diebougou Airport", "Diebougou", "BF", false], - "XDJ": ["Djibo Airport", "Djibo", "BF", false], - "XEN": ["Xingcheng Air Base", "Huludao (Xingcheng)", "CN", false], - "XEQ": ["Tasiusaq Heliport", "Tasiusak", "GL", true], - "XES": ["Grand Geneva Resort Airport", "Lake Geneva", "US", false], - "XFN": ["Xiangyang Liuji Airport", "Xiangyang (Xiangzhou)", "CN", true], - "XFT": ["Big Pine Sands STOLGround", "Big Pine Key, Florida", "US", false], - "XFW": ["Hamburg-Finkenwerder Airport", "Hamburg", "DE", false], - "XGA": ["Gaoua Airport", "Gaoua", "BF", false], - "XGG": ["Gorom-Gorom Airport", "Gorom-Gorom", "BF", false], - "XGN": ["Xangongo Airport", "Xangongo", "AO", false], - "XGR": ["Kangiqsualujjuaq (Georges River) Airport", "Kangiqsualujjuaq", "CA", true], - "XIC": ["Xichang Qingshan Airport", "Liangshan (Xichang)", "CN", true], - "XIE": ["Xienglom Airport", "Xienglom", "LA", false], - "XIG": ["Xinguara Municipal Airport", "Xinguara", "BR", false], - "XIJ": ["Ahmed Al Jaber Air Base", "Ahmed Al Jaber AB", "KW", false], - "XIL": ["Xilinhot Airport", "Xilinhot", "CN", true], - "XIN": ["Xingning Air Base", "Meizhou (Xingning)", "CN", false], - "XIQ": ["Ilimanaq Heliport", "Ilimanaq", "GL", true], - "XIY": ["Xi'an Xianyang International Airport", "Xi'an", "CN", true], - "XJD": ["Al Udeid Air Base", "Ar Rayyan", "QA", false], - "XJM": ["Mangla Airport", "Mangla", "PK", false], - "XKA": ["Kantchari Airport", "Kantchari", "BF", false], - "XKH": ["Xieng Khouang Airport", "Xieng Khouang", "LA", true], - "XKS": ["Kasabonika Airport", "Kasabonika", "CA", true], - "XKY": ["Kaya Airport", "Kaya", "BF", false], - "XLB": ["Lac Brochet Airport", "Lac Brochet", "CA", true], - "XLS": ["Saint Louis Airport", "Saint Louis", "SN", false], - "XLU": ["Leo Airport", "Leo", "BF", false], - "XMA": ["Maramag Airport", "Maramag", "PH", false], - "XMC": ["Mallacoota Airport", "", "AU", false], - "XMD": ["Madison Municipal Airport", "Madison", "US", false], - "XMG": ["Mahendranagar Airport", "Mahendranagar", "NP", false], - "XMH": ["Manihi Airport", "", "PF", true], - "XMI": ["Masasi Airport", "Masasi", "TZ", false], - "XML": ["Minlaton Airport", "", "AU", false], - "XMN": ["Xiamen Gaoqi International Airport", "Xiamen", "CN", true], - "XMP": ["Macmillan Pass Airport", "Macmillan Pass", "CA", false], - "XMS": ["Coronel E Carvajal Airport", "Macas", "EC", true], - "XMY": ["Yam Island Airport", "Yam Island", "AU", true], - "XNA": ["Northwest Arkansas National Airport", "Fayetteville/Springdale/Rogers", "US", true], - "XNH": ["Ali Air Base", "Nasiriyah", "IQ", false], - "XNN": ["Xining Caojiabao International Airport", "Haidong (Huzhu Tu Autonomous County)", "CN", true], - "XNT": ["Xingtai Dalian Airport", "Xingtai", "CN", false], - "XNU": ["Nouna Airport", "Nouna", "BF", false], - "XPA": ["Pama Airport", "Pama", "BF", false], - "XPK": ["Pukatawagan Airport", "Pukatawagan", "CA", true], - "XPL": ["Palmerola International Airport", "Palmerola", "HN", true], - "XPP": ["Poplar River Airport", "Poplar River", "CA", false], - "XPR": ["Pine Ridge Airport", "Pine Ridge", "US", false], - "XQP": ["Quepos Managua Airport", "Quepos", "CR", true], - "XQU": ["Qualicum Beach Airport", "Qualicum Beach", "CA", true], - "XRH": ["RAAF Base Richmond", "Richmond", "AU", false], - "XRQ": ["New Barag Right Banner Baogede Airport", "New Barag Right Banner", "CN", false], - "XRR": ["Ross River Airport", "Ross River", "CA", false], - "XRY": ["Jerez Airport", "Jerez de la Frontera", "ES", true], - "XSB": ["Sir Bani Yas Airport", "Sir Bani Yas", "AE", false], - "XSC": ["South Caicos Airport", "South Caicos", "TC", true], - "XSD": ["Tonopah Test Range Airport", "Tonopah", "US", false], - "XSE": ["Sebba Airport", "Sebba", "BF", false], - "XSI": ["South Indian Lake Airport", "South Indian Lake", "CA", true], - "XSO": ["Siocon Airport", "Siocon", "PH", false], - "XSP": ["Seletar Airport", "Seletar", "SG", true], - "XTG": ["Thargomindah Airport", "Thargomindah", "AU", true], - "XTL": ["Tadoule Lake Airport", "Tadoule Lake", "CA", true], - "XTO": ["Taroom Airport", "", "AU", false], - "XTR": ["Tara Airport", "", "AU", false], - "XUZ": ["Xuzhou Guanyin International Airport", "Xuzhou", "CN", true], - "XWA": ["Williston Basin International Airport", "Williston", "US", true], - "XYA": ["Yandina Airport", "Yandina", "SB", true], - "XYE": ["Ye Airport", "Ye", "MM", false], - "XYR": ["Edwaki Airport", "Yellow River Mission", "PG", false], - "XZA": ["Zabré Airport", "Zabré", "BF", false], - "XZM": ["Macau Heliport at Outer Harbour Ferry Terminal", "Sé", "MO", false], - "YAA": ["Anahim Lake Airport", "Anahim Lake", "CA", true], - "YAB": ["Arctic Bay Airport", "Arctic Bay", "CA", true], - "YAC": ["Cat Lake Airport", "Cat Lake", "CA", true], - "YAE": ["Yartsevo Airport", "Yartsevo", "RU", false], - "YAG": ["Fort Frances Municipal Airport", "Fort Frances", "CA", true], - "YAH": ["La Grande-4 Airport", "La Grande-4", "CA", false], - "YAI": ["Gral. Bernardo O´Higgins Airport", "Chillan", "CL", false], - "YAJ": ["Lyall Harbour Seaplane Base", "Saturna Island", "CA", true], - "YAK": ["Yakutat Airport", "Yakutat", "US", true], - "YAL": ["Alert Bay Airport", "Alert Bay", "CA", true], - "YAM": ["Sault Ste Marie Airport", "Sault Ste Marie", "CA", true], - "YAN": ["Yangambi Airport", "Yangambi", "CD", false], - "YAO": ["Yaoundé Ville Airport", "Yaoundé", "CM", false], - "YAP": ["Yap International Airport", "Yap Island", "FM", true], - "YAQ": ["Maple Bay Seaplane Base", "Maple Bay", "CA", true], - "YAR": ["La Grande-3 Airport", "La Grande-3", "CA", false], - "YAS": ["Yasawa Island Airport", "Yasawa Island", "FJ", true], - "YAT": ["Attawapiskat Airport", "Attawapiskat", "CA", true], - "YAU": ["Donaldson Airport", "Kattiniq", "CA", false], - "YAV": ["Mayne Island Seaplane Base", "Miners Bay", "CA", true], - "YAX": ["Wapekeka Airport", "Angling Lake", "CA", true], - "YAY": ["St. Anthony Airport", "St. Anthony", "CA", true], - "YAZ": ["Tofino / Long Beach Airport", "Tofino", "CA", true], - "YBA": ["Banff Airport", "Banff", "CA", false], - "YBB": ["Kugaaruk Airport", "Kugaaruk", "CA", true], - "YBC": ["Baie-Comeau Airport", "Baie-Comeau", "CA", true], - "YBE": ["Uranium City Airport", "Uranium City", "CA", true], - "YBF": ["Bamfield Seaplane Base", "Bamfield", "CA", true], - "YBG": ["Saguenay-Bagotville Airport", "Saguenay", "CA", true], - "YBH": ["Bull Harbour Water Aerodrome", "Bull Harbour", "CA", false], - "YBI": ["Black Tickle Airport", "Black Tickle", "CA", true], - "YBJ": ["Baie-Johan-Beetz Water Aerodrome", "Baie-Johan-Beetz", "CA", false], - "YBK": ["Baker Lake Airport", "Baker Lake", "CA", true], - "YBL": ["Campbell River Airport", "Campbell River", "CA", true], - "YBO": ["Bob Quinn Lake Airport", "Bob Quinn Lake", "CA", false], - "YBP": ["Yibin Wuliangye Airport", "Yibin (Cuiping)", "CN", true], - "YBQ": ["Telegraph Harbour Seaplane Base", "Thetis Island", "CA", true], - "YBR": ["Brandon Municipal Airport", "Brandon", "CA", true], - "YBS": ["Opapimiskan Lake Airport", "Opapimiskan Lake", "CA", false], - "YBT": ["Brochet Airport", "Brochet", "CA", true], - "YBV": ["Berens River Airport", "Berens River", "CA", true], - "YBW": ["Bedwell Harbour Seaplane Base", "Bedwell Harbour", "CA", true], - "YBX": ["Lourdes-de-Blanc-Sablon Airport", "Blanc-Sablon", "CA", true], - "YBY": ["Bonnyville Airport", "Bonnyville", "CA", true], - "YCA": ["Courtenay Airpark", "Courtenay", "CA", false], - "YCB": ["Cambridge Bay Airport", "Cambridge Bay", "CA", true], - "YCC": ["Cornwall Regional Airport", "Cornwall", "CA", false], - "YCD": ["Nanaimo Airport", "Nanaimo", "CA", true], - "YCE": ["Centralia / James T. Field Memorial Aerodrome", "Huron Park", "CA", false], - "YCF": ["Cortes Bay Water Aerodrome", "Cortes Bay", "CA", false], - "YCG": ["Castlegar/West Kootenay Regional Airport", "Castlegar", "CA", true], - "YCH": ["Miramichi Airport", "Miramichi", "CA", false], - "YCK": ["Tommy Kochon Airport", "Colville Lake", "CA", true], - "YCL": ["Charlo Airport", "Charlo", "CA", false], - "YCM": ["Niagara District Airport", "Niagara-on-the-Lake", "CA", true], - "YCN": ["Cochrane Airport", "Cochrane", "CA", false], - "YCO": ["Kugluktuk Airport", "Kugluktuk", "CA", true], - "YCQ": ["Chetwynd Airport", "Chetwynd", "CA", false], - "YCR": ["Cross Lake (Charlie Sinclair Memorial) Airport", "Cross Lake", "CA", true], - "YCS": ["Chesterfield Inlet Airport", "Chesterfield Inlet", "CA", true], - "YCT": ["Coronation Airport", "Coronation", "CA", false], - "YCU": ["Yuncheng Yanhu International Airport", "Yuncheng (Yanhu)", "CN", true], - "YCW": ["Chilliwack Airport", "Chilliwack", "CA", false], - "YCY": ["Clyde River Airport", "Clyde River", "CA", true], - "YCZ": ["Fairmont Hot Springs Airport", "Fairmont Hot Springs", "CA", false], - "YDA": ["Dawson City Airport", "Dawson City", "CA", true], - "YDB": ["Burwash Airport", "Burwash Landing", "CA", false], - "YDC": ["Drayton Valley Industrial Airport", "Drayton Valley", "CA", false], - "YDF": ["Deer Lake Airport", "Deer Lake", "CA", true], - "YDG": ["Digby / Annapolis Regional Airport", "Digby", "CA", false], - "YDJ": ["Hatchet Lake Airport", "Hatchet Lake", "CA", false], - "YDL": ["Dease Lake Airport", "Dease Lake", "CA", true], - "YDN": ["Dauphin Barker Airport", "Dauphin", "CA", true], - "YDO": ["Dolbeau-Saint-Felicien Airport", "Dolbeau-Saint-Felicien", "CA", false], - "YDP": ["Nain Airport", "Nain", "CA", true], - "YDQ": ["Dawson Creek Airport", "Dawson Creek", "CA", false], - "YDT": ["Boundary Bay Airport", "Delta", "CA", false], - "YDU": ["Kasba Lake Airport", "Kasba Lake", "CA", false], - "YDV": ["Bloodvein River Airport", "Bloodvein River", "CA", true], - "YDW": ["North of Sixty Airport", "Obre Lake", "CA", false], - "YEB": ["Bar River Airport", "Echo Bay", "CA", false], - "YEC": ["Yecheon Airbase", "Yecheon-ri", "KR", false], - "YEG": ["Edmonton International Airport", "Edmonton", "CA", true], - "YEH": ["Yinchuan Yueyahu Airport", "Yinchuan", "CN", false], - "YEI": ["Bursa Yenişehir Airport", "Yenişehir", "TR", true], - "YEK": ["Arviat Airport", "Arviat", "CA", true], - "YEL": ["Elliot Lake Municipal Airport", "Elliot Lake", "CA", false], - "YEM": ["Manitoulin East Municipal Airport", "Sheguiandah", "CA", false], - "YEN": ["Estevan Airport", "Estevan", "CA", false], - "YEO": ["RNAS Yeovilton", "Yeovil, Somerset", "GB", false], - "YEQ": ["Yenkis(Yankisa) Airport", "", "PG", false], - "YER": ["Fort Severn Airport", "Fort Severn", "CA", true], - "YES": ["Yasuj Airport", "Yasuj", "IR", false], - "YET": ["Edson Airport", "Edson", "CA", false], - "YEU": ["Eureka Airport", "Eureka", "CA", false], - "YEV": ["Inuvik Mike Zubko Airport", "Inuvik", "CA", true], - "YEY": ["Amos/Magny Airport", "Amos", "CA", false], - "YFA": ["Fort Albany Airport", "Fort Albany", "CA", true], - "YFB": ["Iqaluit Airport", "Iqaluit", "CA", true], - "YFC": ["Fredericton International Airport", "Fredericton", "CA", true], - "YFE": ["Forestville Airport", "Forestville", "CA", false], - "YFG": ["Fontanges Airport", "Fontanges", "CA", false], - "YFH": ["Fort Hope Airport", "Fort Hope", "CA", true], - "YFI": ["Fort Mackay / Firebag", "Suncor Energy Site", "CA", false], - "YFJ": ["Wekweètì Airport", "Wekweètì", "CA", true], - "YFL": ["Fort Reliance Seaplane Base", "Fort Reliance", "CA", false], - "YFO": ["Flin Flon Airport", "Flin Flon", "CA", true], - "YFR": ["Fort Resolution Airport", "Fort Resolution", "CA", false], - "YFS": ["Fort Simpson Airport", "Fort Simpson", "CA", true], - "YFX": ["St. Lewis (Fox Harbour) Airport", "St. Lewis", "CA", true], - "YGA": ["Yongchuan Da'an General Airport", "Chongqing", "CN", false], - "YGB": ["Texada Gillies Bay Airport", "Texada", "CA", false], - "YGE": ["Gorge Harbour Seaplane Base", "Gorge Harbour", "CA", false], - "YGG": ["Ganges Seaplane Base", "Salt Spring Island", "CA", true], - "YGH": ["Fort Good Hope Airport", "Fort Good Hope", "CA", true], - "YGJ": ["Yonago Kitaro Airport / JASDF Miho Air Base", "Yonago", "JP", true], - "YGK": ["Kingston Norman Rogers Airport", "Kingston", "CA", false], - "YGL": ["La Grande Rivière Airport", "La Grande Rivière", "CA", true], - "YGM": ["Gimli Industrial Park Airport", "Gimli", "CA", false], - "YGN": ["Greenway Sound Seaplane Base", "Broughton Island", "CA", true], - "YGO": ["Gods Lake Narrows Airport", "Gods Lake Narrows", "CA", true], - "YGP": ["Michel-Pouliot Gaspé Airport", "Gaspé", "CA", true], - "YGQ": ["Geraldton Greenstone Regional Airport", "Geraldton", "CA", false], - "YGR": ["Îles-de-la-Madeleine Airport", "Les Îles-de-la-Madeleine", "CA", true], - "YGT": ["Igloolik Airport", "Igloolik", "CA", true], - "YGV": ["Havre-Saint-Pierre Airport", "Havre-Saint-Pierre", "CA", true], - "YGW": ["Kuujjuarapik Airport", "Kuujjuarapik", "CA", true], - "YGX": ["Gillam Airport", "Gillam", "CA", true], - "YGZ": ["Grise Fiord Airport", "Grise Fiord", "CA", true], - "YHA": ["Port Hope Simpson Airport", "Port Hope Simpson", "CA", true], - "YHB": ["Hudson Bay Airport", "Hudson Bay", "CA", false], - "YHC": ["Hakai Passage Water Aerodrome", "Hecate Island", "CA", false], - "YHD": ["Dryden Regional Airport", "Dryden", "CA", false], - "YHE": ["Hope Airport / FVRD Regional Airpark", "Hope", "CA", false], - "YHF": ["Hearst René Fontaine Municipal Airport", "Hearst", "CA", false], - "YHG": ["Charlottetown Airport", "Charlottetown", "CA", true], - "YHH": ["Campbell River Seaplane Base", "Campbell River", "CA", true], - "YHI": ["Ulukhaktok Holman Airport", "Ulukhaktok", "CA", true], - "YHJ": ["Nanchang Yaohu Airport", "Nanchang", "CN", false], - "YHK": ["Gjoa Haven Airport", "Gjoa Haven", "CA", true], - "YHM": ["John C. Munro Hamilton International Airport", "Hamilton", "CA", true], - "YHN": ["Hornepayne Municipal Airport", "Hornepayne", "CA", false], - "YHO": ["Hopedale Airport", "Hopedale", "CA", true], - "YHP": ["Poplar Hill Airport", "Poplar Hill", "CA", true], - "YHR": ["Chevery Airport", "Chevery", "CA", true], - "YHS": ["Sechelt-Gibsons Airport", "Sechelt", "CA", false], - "YHT": ["Haines Junction Airport", "Haines Junction", "CA", false], - "YHU": ["Montréal / Saint-Hubert Metropolitan Airport", "Montréal", "CA", true], - "YHY": ["Hay River / Merlyn Carter Airport", "Hay River", "CA", true], - "YHZ": ["Halifax / Stanfield International Airport", "Halifax", "CA", true], - "YIA": ["Yogyakarta International Airport", "Yogyakarta", "ID", true], - "YIB": ["Atikokan Municipal Airport", "Atikokan", "CA", false], - "YIC": ["Yichun Mingyueshan Airport", "Yichun", "CN", true], - "YIE": ["Arxan Yi'ershi Airport", "Arxan", "CN", true], - "YIF": ["St Augustin Airport", "St-Augustin", "CA", true], - "YIG": ["Big Bay Seaplane Base", "Stuart Island", "CA", true], - "YIH": ["Yichang Sanxia Airport", "Yichang (Xiaoting)", "CN", true], - "YIK": ["Ivujivik Airport", "Ivujivik", "CA", true], - "YIN": ["Ili Yining International Airport", "Ili (Yining / Ghulja)", "CN", true], - "YIO": ["Pond Inlet Airport", "Pond Inlet", "CA", true], - "YIP": ["Willow Run Airport", "Detroit", "US", false], - "YIV": ["Island Lake Airport", "Island Lake", "CA", true], - "YIW": ["Yiwu Airport", "Yiwu/Jinhua", "CN", true], - "YJA": ["Jasper Airport", "Jasper", "CA", false], - "YJF": ["Fort Liard Airport", "Fort Liard", "CA", false], - "YJN": ["St Jean Airport", "St Jean", "CA", false], - "YJP": ["Jasper-Hinton Airport", "Hinton", "CA", false], - "YJS": ["Samjiyŏn Airport", "Samjiyŏn", "KP", false], - "YKA": ["Kamloops John Moose Fulton Field Regional Airport", "Kamloops", "CA", true], - "YKC": ["Collins Bay Airport", "Collins Bay", "CA", false], - "YKD": ["Kincardine Municipal Airport", "Kincardine", "CA", false], - "YKE": ["Knee Lake Airport", "Knee Lake", "CA", false], - "YKF": ["Region of Waterloo International Airport", "Breslau", "CA", true], - "YKG": ["Kangirsuk Airport", "Kangirsuk", "CA", true], - "YKH": ["Yingkou Lanqi Airport", "Yingkou (Laobian)", "CN", true], - "YKJ": ["Key Lake Airport", "Key Lake", "CA", false], - "YKK": ["Kitkatla Seaplane Base", "Kitkatla", "CA", false], - "YKL": ["Schefferville Airport", "Schefferville", "CA", true], - "YKM": ["Yakima Air Terminal McAllister Field", "Yakima", "US", true], - "YKN": ["Chan Gurney Municipal Airport", "Yankton", "US", false], - "YKO": ["Hakkari Yüksekova Airport", "Hakkari", "TR", true], - "YKQ": ["Waskaganish Airport", "Waskaganish", "CA", true], - "YKS": ["Platon Oyunsky Yakutsk International Airport", "Yakutsk", "RU", true], - "YKT": ["Klemtu Water Aerodrome", "Klemtu", "CA", false], - "YKU": ["Chisasibi Airport", "Chisasibi", "CA", true], - "YKX": ["Kirkland Lake Airport", "Kirkland Lake", "CA", false], - "YKY": ["Kindersley Airport", "Kindersley", "CA", false], - "YLB": ["Lac La Biche Airport", "Lac La Biche", "CA", false], - "YLC": ["Kimmirut Airport", "Kimmirut", "CA", true], - "YLD": ["Chapleau Airport", "Chapleau", "CA", false], - "YLE": ["Whatì Airport", "Whatì", "CA", true], - "YLG": ["Yalgoo Airport", "Yalgoo", "AU", false], - "YLH": ["Lansdowne House Airport", "Lansdowne House", "CA", true], - "YLI": ["Ylivieska Airfield", "Ylivieska", "FI", false], - "YLJ": ["Meadow Lake Airport", "Meadow Lake", "CA", false], - "YLK": ["Barrie-Lake Simcoe Regional Airport", "Barrie", "CA", true], - "YLL": ["Lloydminster Airport", "Lloydminster", "CA", true], - "YLP": ["Mingan Airport", "Longue-Pointe-de-Mingan", "CA", false], - "YLQ": ["La Tuque Airport", "La Tuque", "CA", false], - "YLR": ["Leaf Rapids Airport", "Leaf Rapids", "CA", false], - "YLS": ["Lebel-sur-Quevillon Airport", "Lebel-sur-Quévillon", "CA", false], - "YLT": ["Alert Airport", "Alert", "CA", false], - "YLV": ["Yevlakh Airport", "Yevlakh", "AZ", false], - "YLW": ["Kelowna International Airport", "Kelowna", "CA", true], - "YLX": ["Yulin Fumian Airport", "Yulin", "CN", true], - "YLY": ["Langley Airport", "Langley", "CA", false], - "YMA": ["Mayo Airport", "Mayo", "CA", false], - "YMB": ["Merritt Airport", "Merritt", "CA", false], - "YME": ["Matane Airport", "Matane", "CA", false], - "YMF": ["Montague Harbour Seaplane Base", "Galiano Island", "CA", true], - "YMG": ["Manitouwadge Airport", "Manitouwadge", "CA", false], - "YMH": ["Mary's Harbour Airport", "Mary's Harbour", "CA", true], - "YMJ": ["Moose Jaw Air Vice Marshal C. M. McEwen Airport", "Moose Jaw", "CA", false], - "YMK": ["Mys Kamenny Airport", "Mys Kamennyi", "RU", false], - "YML": ["Charlevoix Airport", "Charlevoix", "CA", false], - "YMM": ["Fort McMurray International Airport", "Fort McMurray", "CA", true], - "YMN": ["Makkovik Airport", "Makkovik", "CA", true], - "YMO": ["Moosonee Airport", "Moosonee", "CA", true], - "YMP": ["Port McNeill Airport", "Port McNeill", "CA", true], - "YMS": ["Moises Benzaquen Rengifo Airport", "Yurimaguas", "PE", true], - "YMT": ["Chapais Airport", "Chibougamau", "CA", true], - "YMU": ["Mansons Landing Seaplane Base", "Mansons Landing", "CA", false], - "YMV": ["Mary River Aerodrome", "Mary River", "CA", false], - "YMW": ["Maniwaki Airport", "Messines", "CA", false], - "YMX": ["Montreal Mirabel International Airport", "Montréal", "CA", true], - "YNA": ["Natashquan Airport", "Natashquan", "CA", true], - "YNB": ["Prince Abdulmohsen Bin Abdulaziz International Airport", "Yanbu", "SA", true], - "YNC": ["Wemindji Airport", "Wemindji", "CA", true], - "YND": ["Ottawa / Gatineau Airport", "Gatineau", "CA", true], - "YNE": ["Norway House Airport", "Norway House", "CA", true], - "YNG": ["Youngstown Warren Regional Airport", "Youngstown/Warren", "US", false], - "YNH": ["Hudson's Hope Airport", "Hudson's Hope", "CA", false], - "YNJ": ["Yanji Chaoyangchuan Airport", "Yanji", "CN", true], - "YNL": ["Points North Landing Airport", "Points North Landing", "CA", true], - "YNM": ["Matagami Airport", "Matagami", "CA", false], - "YNO": ["North Spirit Lake Airport", "North Spirit Lake", "CA", true], - "YNP": ["Natuashish Airport", "Natuashish", "CA", true], - "YNS": ["Nemiscau Airport", "Nemiscau", "CA", true], - "YNT": ["Yantai Penglai International Airport", "Yantai", "CN", true], - "YNX": ["Snap Lake Airport", "Snap Lake Mine", "CA", false], - "YNY": ["Yangyang International Airport", "Gonghang-ro", "KR", true], - "YNZ": ["Yancheng Nanyang International Airport", "Yancheng (Tinghu)", "CN", true], - "YOA": ["Ekati Airport", "Ekati", "CA", false], - "YOC": ["Old Crow Airport", "Old Crow", "CA", true], - "YOD": ["CFB Cold Lake", "Cold Lake", "CA", false], - "YOE": ["Donnelly Airport", "Donnelly", "CA", false], - "YOG": ["Ogoki Post Airport", "Ogoki Post", "CA", true], - "YOH": ["Oxford House Airport", "Oxford House", "CA", true], - "YOI": ["Opinaca Aerodrome", "Éléonore Mine", "CA", false], - "YOJ": ["High Level Airport", "High Level", "CA", true], - "YOL": ["Yola Airport", "Yola", "NG", true], - "YON": ["Yongphulla Airport", "Yongphulla", "BT", true], - "YOO": ["Oshawa Executive Airport", "Oshawa", "CA", false], - "YOP": ["Rainbow Lake Airport", "Rainbow Lake", "CA", false], - "YOS": ["Owen Sound / Billy Bishop Regional Airport", "Owen Sound", "CA", false], - "YOW": ["Ottawa Macdonald-Cartier International Airport", "Ottawa", "CA", true], - "YPA": ["Prince Albert Glass Field", "Prince Albert", "CA", true], - "YPB": ["Alberni Valley Regional Airport", "Port Alberni", "CA", false], - "YPC": ["Paulatuk (Nora Aliqatchialuk Ruben) Airport", "Paulatuk", "CA", true], - "YPD": ["Parry Sound Area Municipal Airport", "Parry Sound", "CA", false], - "YPE": ["Peace River Airport", "Peace River", "CA", true], - "YPG": ["Portage-la-Prairie / Southport Airport", "Portage la Prairie", "CA", false], - "YPH": ["Inukjuak Airport", "Inukjuak", "CA", true], - "YPJ": ["Aupaluk Airport", "Aupaluk", "CA", true], - "YPL": ["Pickle Lake Airport", "Pickle Lake", "CA", true], - "YPM": ["Pikangikum Airport", "Pikangikum", "CA", true], - "YPN": ["Port-Menier Airport", "Port-Menier", "CA", true], - "YPO": ["Peawanuck Airport", "Peawanuck", "CA", true], - "YPQ": ["Peterborough Regional Airport", "Peterborough", "CA", true], - "YPR": ["Prince Rupert Airport", "Prince Rupert", "CA", true], - "YPS": ["Port Hawkesbury Airport", "Port Hawkesbury", "CA", false], - "YPT": ["Pender Harbour Seaplane Base", "Sunshine Coast", "CA", false], - "YPW": ["Powell River Airport", "Powell River", "CA", true], - "YPX": ["Puvirnituq Airport", "Puvirnituq", "CA", true], - "YPY": ["Fort Chipewyan Airport", "Fort Chipewyan", "CA", true], - "YPZ": ["Burns Lake Airport", "Burns Lake", "CA", true], - "YQA": ["Muskoka Airport", "Gravenhurst", "CA", true], - "YQB": ["Quebec Jean Lesage International Airport", "Quebec", "CA", true], - "YQC": ["Quaqtaq Airport", "Quaqtaq", "CA", true], - "YQD": ["The Pas Airport", "The Pas", "CA", true], - "YQF": ["Red Deer Regional Airport", "Springbrook", "CA", false], - "YQG": ["Windsor International Airport", "Windsor", "CA", true], - "YQH": ["Watson Lake Airport", "Watson Lake", "CA", true], - "YQI": ["Yarmouth Airport", "Yarmouth", "CA", false], - "YQJ": ["April Point Seaplane Base", "Quadra Island", "CA", false], - "YQK": ["Kenora Airport", "Kenora", "CA", true], - "YQL": ["Lethbridge County Airport", "Lethbridge", "CA", true], - "YQM": ["Greater Moncton Roméo LeBlanc International Airport", "Moncton", "CA", true], - "YQN": ["Nakina Airport", "Nakina", "CA", true], - "YQQ": ["Comox Valley International Airport / CFB Comox", "Comox", "CA", true], - "YQR": ["Regina International Airport", "Regina", "CA", true], - "YQS": ["St Thomas Municipal Airport", "St Thomas", "CA", false], - "YQT": ["Thunder Bay International Airport", "Thunder Bay", "CA", true], - "YQU": ["Grande Prairie Airport", "Grande Prairie", "CA", true], - "YQV": ["Yorkton Municipal Airport", "Yorkton", "CA", false], - "YQW": ["North Battleford Airport", "North Battleford", "CA", false], - "YQX": ["Gander International Airport", "Gander", "CA", true], - "YQY": ["Sydney / J.A. Douglas McCurdy Airport", "Sydney", "CA", true], - "YQZ": ["Quesnel Airport", "Quesnel", "CA", true], - "YRA": ["Rae Lakes Airport", "Gamètì", "CA", true], - "YRB": ["Resolute Bay Airport", "Resolute Bay", "CA", true], - "YRC": ["Refuge Cove Seaplane Base", "Desolation Sound", "CA", false], - "YRD": ["Dean River Airport", "Kimsquit Valley", "CA", false], - "YRF": ["Cartwright Airport", "Cartwright", "CA", true], - "YRG": ["Rigolet Airport", "Rigolet", "CA", true], - "YRI": ["Rivière-du-Loup Airport", "Rivière-du-Loup", "CA", false], - "YRJ": ["Roberval Airport", "Roberval", "CA", true], - "YRL": ["Red Lake Airport", "Red Lake", "CA", true], - "YRM": ["Rocky Mountain House Airport", "Rocky Mountain House", "CA", false], - "YRN": ["Rivers Inlet Seaplane Base", "Rivers Inlet", "CA", false], - "YRO": ["Ottawa / Rockcliffe Airport", "Ottawa", "CA", true], - "YRQ": ["Trois-Rivières Airport", "Trois-Rivières", "CA", false], - "YRR": ["Stuart Island Airstrip", "Big Bay", "CA", false], - "YRS": ["Red Sucker Lake Airport", "Red Sucker Lake", "CA", true], - "YRT": ["Rankin Inlet Airport", "Rankin Inlet", "CA", true], - "YRV": ["Revelstoke Airport", "Revelstoke", "CA", false], - "YSA": ["Sable Island Airport.", "Sable Island", "CA", false], - "YSB": ["Sudbury Airport", "Sudbury", "CA", true], - "YSC": ["Sherbrooke Airport", "Sherbrooke", "CA", false], - "YSD": ["Canadian Forces Base Suffield Heliport", "Ralston", "CA", false], - "YSE": ["Squamish Airport", "Squamish", "CA", false], - "YSF": ["Stony Rapids Airport", "Stony Rapids", "CA", true], - "YSG": ["Lutselk'e Airport", "Lutselk'e", "CA", true], - "YSH": ["Smiths Falls-Montague (Russ Beach) Airport", "Smiths Falls", "CA", false], - "YSI": ["Parry Sound/Frying Pan Island-Sans Souci Seaplane Base", "Frying Pan Island", "CA", false], - "YSJ": ["Saint John Airport", "Saint John", "CA", true], - "YSK": ["Sanikiluaq Airport", "Sanikiluaq", "CA", true], - "YSL": ["Saint-Léonard Airport", "Saint-Léonard", "CA", false], - "YSM": ["Fort Smith Airport", "Fort Smith", "CA", true], - "YSN": ["Shuswap Regional Airport", "Salmon Arm", "CA", false], - "YSO": ["Postville Airport", "Postville", "CA", true], - "YSP": ["Marathon Airport", "Marathon", "CA", false], - "YSQ": ["Songyuan Chaganhu Airport", "Qian Gorlos Mongol Autonomous County", "CN", true], - "YST": ["St. Theresa Point Airport", "St. Theresa Point", "CA", true], - "YSU": ["Summerside Airport", "Slemon Park", "CA", false], - "YSV": ["Saglek Airport", "Saglek", "CA", false], - "YSX": ["Bella Bella/Shearwater Seaplane Base", "Bella Bella", "CA", false], - "YSY": ["Sachs Harbour (David Nasogaluak Jr. Saaryuaq) Airport", "Sachs Harbour", "CA", true], - "YTA": ["Pembroke Airport", "Pembroke", "CA", false], - "YTB": ["Hartley Bay Seaplane Base", "Hartley Bay", "CA", false], - "YTD": ["Thicket Portage Airport", "Thicket Portage", "CA", false], - "YTE": ["Cape Dorset Airport", "Kinngait", "CA", true], - "YTF": ["Alma Airport", "Alma", "CA", false], - "YTG": ["Sullivan Bay Seaplane Base", "Sullivan Bay", "CA", true], - "YTH": ["Thompson Airport", "Thompson", "CA", true], - "YTL": ["Big Trout Lake Airport", "Big Trout Lake", "CA", true], - "YTM": ["Mont-Tremblant International Airport", "La Macaza", "CA", false], - "YTP": ["Tofino Harbour Seaplane Base", "Tofino", "CA", true], - "YTQ": ["Tasiujaq Airport", "Tasiujaq", "CA", true], - "YTR": ["CFB Trenton", "Trenton", "CA", false], - "YTS": ["Timmins/Victor M. Power", "Timmins", "CA", true], - "YTT": ["Tisdale Airport", "Tisdale", "CA", false], - "YTW": ["Yutian Wanfang Airport", "Hotan (Yutian)", "CN", true], - "YTY": ["Yangzhou Taizhou Airport", "Yangzhou", "CN", true], - "YTZ": ["Billy Bishop Toronto City Airport", "Toronto", "CA", true], - "YUA": ["Yuanmou Air Base", "Chuxiong (Yuanmou)", "CN", false], - "YUB": ["Tuktoyaktuk / James Gruben Airport", "Tuktoyaktuk", "CA", false], - "YUD": ["Umiujaq Airport", "Umiujaq", "CA", true], - "YUE": ["Yuendumu Airport", "", "AU", false], - "YUK": ["Syunik Airport", "Kapan", "AM", false], - "YUL": ["Montreal / Pierre Elliott Trudeau International Airport", "Montréal", "CA", true], - "YUM": ["Yuma International Airport / Marine Corps Air Station Yuma", "Yuma", "US", true], - "YUS": ["Yushu Batang Airport", "Yushu (Batang)", "CN", true], - "YUT": ["Naujaat Airport", "Repulse Bay", "CA", true], - "YUX": ["Hall Beach Airport", "Sanirajak", "CA", true], - "YUY": ["Rouyn Noranda Airport", "Rouyn-Noranda", "CA", true], - "YVB": ["Bonaventure Airport", "Bonaventure", "CA", true], - "YVC": ["La Ronge Airport", "La Ronge", "CA", true], - "YVD": ["Yeva Airport", "Yeva", "PG", false], - "YVE": ["Vernon Regional Airport", "Vernon", "CA", false], - "YVG": ["Vermilion Airport", "Vermilion", "CA", false], - "YVM": ["Qikiqtarjuaq Airport", "Qikiqtarjuaq", "CA", true], - "YVO": ["Val-d'Or Airport", "Val-d'Or", "CA", true], - "YVP": ["Kuujjuaq Airport", "Kuujjuaq", "CA", true], - "YVQ": ["Norman Wells Airport", "Norman Wells", "CA", true], - "YVR": ["Vancouver International Airport", "Vancouver", "CA", true], - "YVT": ["Buffalo Narrows Airport", "Buffalo Narrows", "CA", false], - "YVV": ["Wiarton Airport", "Wiarton", "CA", true], - "YVZ": ["Deer Lake Airport", "Deer Lake", "CA", true], - "YWA": ["Petawawa Heliport", "Petawawa", "CA", false], - "YWB": ["Kangiqsujuaq (Wakeham Bay) Airport", "Kangiqsujuaq", "CA", true], - "YWG": ["Winnipeg / James Armstrong Richardson International Airport", "Winnipeg", "CA", true], - "YWH": ["Victoria Harbour Seaplane Base", "Victoria", "CA", true], - "YWJ": ["Déline Airport", "Déline", "CA", true], - "YWK": ["Wabush Airport", "Wabush", "CA", true], - "YWL": ["Williams Lake Airport", "Williams Lake", "CA", true], - "YWM": ["Williams Harbour Airport", "Williams Harbour", "CA", true], - "YWP": ["Webequie Airport", "Webequie", "CA", true], - "YWQ": ["Chute-Des-Passes/Lac Margane Seaplane Base", "Chutes-des-Passes", "CA", false], - "YWR": ["White River Seaplane Base", "White River", "CA", false], - "YWS": ["Whistler/Green Lake Water Aerodrome", "Whistler", "CA", true], - "YWY": ["Wrigley Airport", "Wrigley", "CA", false], - "YXC": ["Cranbrook/Canadian Rockies International Airport", "Cranbrook", "CA", true], - "YXE": ["Saskatoon John G. Diefenbaker International Airport", "Saskatoon", "CA", true], - "YXH": ["Medicine Hat Regional Airport", "Medicine Hat", "CA", true], - "YXJ": ["Fort St John / North Peace Regional Airport", "Fort Saint John", "CA", true], - "YXK": ["Rimouski Airport", "Rimouski", "CA", false], - "YXL": ["Sioux Lookout Airport", "Sioux Lookout", "CA", true], - "YXN": ["Whale Cove Airport", "Whale Cove", "CA", true], - "YXP": ["Pangnirtung Airport", "Pangnirtung", "CA", true], - "YXQ": ["Beaver Creek Airport", "Beaver Creek", "CA", false], - "YXR": ["Earlton (Timiskaming Regional) Airport", "Earlton", "CA", false], - "YXS": ["Prince George (International) Airport", "Prince George", "CA", true], - "YXT": ["Northwest Regional Airport Terrace-Kitimat", "Terrace", "CA", true], - "YXU": ["London International Airport", "London", "CA", true], - "YXX": ["Abbotsford International Airport", "Abbotsford", "CA", true], - "YXY": ["Whitehorse / Erik Nielsen International Airport", "Whitehorse", "CA", true], - "YXZ": ["Wawa Airport", "Wawa", "CA", false], - "YYA": ["Yueyang Sanhe Airport", "Yueyang (Yueyanglou)", "CN", true], - "YYB": ["North Bay Jack Garland Airport", "North Bay", "CA", true], - "YYC": ["Calgary International Airport", "Calgary", "CA", true], - "YYD": ["Smithers Airport", "Smithers", "CA", true], - "YYE": ["Fort Nelson Airport", "Fort Nelson", "CA", true], - "YYF": ["Penticton Airport", "Penticton", "CA", true], - "YYG": ["Charlottetown Airport", "Charlottetown", "CA", true], - "YYH": ["Taloyoak Airport", "Taloyoak", "CA", true], - "YYJ": ["Victoria International Airport", "Victoria", "CA", true], - "YYL": ["Lynn Lake Airport", "Lynn Lake", "CA", true], - "YYM": ["Cowley Airport", "Cowley", "CA", false], - "YYN": ["Swift Current Airport", "Swift Current", "CA", false], - "YYQ": ["Churchill Airport", "Churchill", "CA", true], - "YYR": ["Goose Bay Airport", "Goose Bay", "CA", true], - "YYT": ["St. John's International Airport", "St. John's", "CA", true], - "YYU": ["Kapuskasing Airport", "Kapuskasing", "CA", false], - "YYW": ["Armstrong Airport", "Armstrong", "CA", false], - "YYY": ["Mont Joli Airport", "Mont-Joli", "CA", true], - "YYZ": ["Toronto Pearson International Airport", "Toronto", "CA", true], - "YZA": ["Cache Creek-Ashcroft Regional Airport", "Cache Creek", "CA", false], - "YZE": ["Gore Bay Manitoulin Airport", "Gore Bay", "CA", false], - "YZF": ["Yellowknife International Airport", "Yellowknife", "CA", true], - "YZG": ["Salluit Airport", "Salluit", "CA", true], - "YZH": ["Slave Lake Airport", "Slave Lake", "CA", false], - "YZP": ["Sandspit Airport", "Sandspit", "CA", true], - "YZR": ["Chris Hadfield Airport", "Sarnia", "CA", false], - "YZS": ["Coral Harbour Airport", "Coral Harbour", "CA", true], - "YZT": ["Port Hardy Airport", "Port Hardy", "CA", true], - "YZU": ["Whitecourt Airport", "Whitecourt", "CA", true], - "YZV": ["Sept-Îles Airport", "Sept-Îles", "CA", true], - "YZW": ["Teslin Airport", "Teslin", "CA", false], - "YZX": ["CFB Greenwood", "Greenwood", "CA", false], - "YZY": ["Zhangye Ganzhou Airport", "Zhangye (Ganzhou)", "CN", true], - "YZZ": ["Trail Regional Airport", "Trail", "CA", true], - "ZAC": ["York Landing Airport", "York Landing", "CA", false], - "ZAD": ["Zadar Airport", "Zadar", "HR", true], - "ZAG": ["Zagreb Franjo Tuđman International Airport", "Velika Gorica", "HR", true], - "ZAH": ["Zahedan International Airport", "Zahedan", "IR", true], - "ZAL": ["Pichoy Airport", "Valdivia", "CL", true], - "ZAM": ["Zamboanga International Airport", "Zamboanga", "PH", true], - "ZAO": ["Cahors Lalbenque airport", "Cahors", "FR", false], - "ZAR": ["Zaria Airport", "Zaria", "NG", false], - "ZAT": ["Zhaotong Zhaoyang Airport", "Zhaotong", "CN", true], - "ZAZ": ["Zaragoza Airport", "Zaragoza", "ES", true], - "ZBE": ["Zábřeh Dolní Benešov Airfield", "Dolní Benešov", "CZ", false], - "ZBF": ["Bathurst Airport", "South Tetagouche", "CA", true], - "ZBL": ["Biloela Airport", "Biloela", "AU", false], - "ZBM": ["Bromont (Roland Désourdy) Airport", "Bromont", "CA", false], - "ZBO": ["Bowen Airport", "Bowen", "AU", false], - "ZBR": ["Chabahar Konarak International Airport", "Konarak", "IR", true], - "ZBY": ["Sayaboury Airport", "Sainyabuli", "LA", false], - "ZCL": ["General Leobardo C. Ruiz International Airport", "Zacatecas", "MX", true], - "ZCO": ["La Araucanía International Airport", "Temuco", "CL", true], - "ZDM": ["Ramallah Heliport", "Ramallah", "PS", false], - "ZDY": ["Delma Airport", "Delma Island", "AE", true], - "ZEC": ["Secunda Airport", "Secunda", "ZA", false], - "ZEG": ["Senggo Airport", "Senggo-Papua Island", "ID", false], - "ZEL": ["Bella Bella (Campbell Island) Airport", "Bella Bella", "CA", true], - "ZEM": ["Eastmain River Airport", "Eastmain River", "CA", true], - "ZEN": ["Zenag Airport", "Zenag", "PG", false], - "ZER": ["Ziro Airport", "Ziro", "IN", false], - "ZFA": ["Faro Airport", "Faro", "CA", false], - "ZFD": ["Fond-du-Lac Airport", "Fond-du-Lac", "CA", true], - "ZFL": ["Zhaosu Tianma Airport", "Zhaosu", "CN", true], - "ZFM": ["Fort Mcpherson Airport", "Fort Mcpherson", "CA", true], - "ZFN": ["Tulita Airport", "Tulita", "CA", true], - "ZFU": ["Unifly Heliport", "Arujá", "BR", false], - "ZFW": ["Fairview Airport", "Fairview", "CA", false], - "ZGF": ["Grand Forks Airport", "Grand Forks", "CA", false], - "ZGI": ["Gods River Airport", "Gods River", "CA", true], - "ZGL": ["South Galway Airport", "", "AU", false], - "ZGM": ["Ngoma Airport", "Ngoma", "ZM", false], - "ZGN": ["Zhongshan Ferry Port", "Zhongshan", "CN", false], - "ZGR": ["Little Grand Rapids Airport", "Little Grand Rapids", "CA", false], - "ZGS": ["La Romaine Airport", "Le Golfe-du-Saint-Laurent", "CA", true], - "ZGU": ["Gaua Island Airport", "Gaua Island", "VU", false], - "ZHA": ["Zhanjiang Wuchuan International Airport", "Zhanjiang", "CN", true], - "ZHM": ["Shamshernagar Airport", "Shamshernagar", "BD", false], - "ZHP": ["High Prairie Airport", "High Prairie", "CA", false], - "ZHY": ["Zhongwei Shapotou Airport", "Zhongwei (Shapotou)", "CN", true], - "ZIA": ["Zhukovsky International Airport", "Moscow", "RU", true], - "ZIC": ["Victoria Airport", "Victoria", "CL", false], - "ZIG": ["Ziguinchor Airport", "Ziguinchor", "SN", true], - "ZIH": ["Ixtapa-Zihuatanejo International Airport", "Ixtapa", "MX", true], - "ZIS": ["Alzintan Airport", "Zintan", "LY", false], - "ZIX": ["Zhigansk Airport", "Zhigansk", "RU", true], - "ZIZ": ["Zamzama Heliport", "Zamzama Gas Field", "PK", false], - "ZJG": ["Jenpeg Airport", "Jenpeg", "CA", false], - "ZJN": ["Swan River Airport", "Swan River", "CA", false], - "ZKB": ["Kasaba Bay Airport", "Kasaba Bay", "ZM", false], - "ZKE": ["Kashechewan Airport", "Kashechewan", "CA", true], - "ZKG": ["Kégashka Airport", "Côte-Nord-du-Golfe-du-Saint-Laurent", "CA", false], - "ZKL": ["Zigong Fengming Airport", "Zigong (Gongjing)", "CN", false], - "ZKP": ["Zyryanka Airport", "Zyryanka", "RU", true], - "ZLO": ["Playa de Oro International Airport", "Manzanillo", "MX", true], - "ZLR": ["Municipal de Linares Airport", "Linares", "CL", false], - "ZLT": ["La Tabatière Airport", "La Tabatière", "CA", true], - "ZLX": ["Zalingei Airport", "Zalingei", "SD", false], - "ZMD": ["Sena Madureira Airport", "Sena Madureira", "BR", false], - "ZMH": ["South Cariboo Region / 108 Mile Airport", "108 Mile", "CA", false], - "ZMM": ["Zamora Airport", "Zamora", "MX", false], - "ZMT": ["Masset Airport", "Masset", "CA", true], - "ZNA": ["Nanaimo Harbour Water Aerodrome", "Nanaimo", "CA", true], - "ZNC": ["Nyac Airport", "Nyac", "US", false], - "ZND": ["Zinder Airport", "Zinder", "NE", true], - "ZNE": ["Newman Airport", "Newman", "AU", true], - "ZNU": ["Namu Water Aerodrome", "Namu", "CA", false], - "ZNZ": ["Abeid Amani Karume International Airport", "Zanzibar", "TZ", true], - "ZOE": ["Ludwigshafen Accident Hospital Heliport", "Ludwigshafen am Rhein", "DE", false], - "ZOF": ["Ocean Falls Seaplane Base", "Ocean Falls", "CA", false], - "ZOS": ["Cañal Bajo Carlos Hott Siebert Airport", "Osorno", "CL", true], - "ZPB": ["Sachigo Lake Airport", "Sachigo Lake", "CA", true], - "ZPC": ["Pucón Airport", "Pucon", "CL", true], - "ZPH": ["Zephyrhills Municipal Airport", "Zephyrhills", "US", false], - "ZPO": ["Pinehouse Lake Airport", "Pinehouse Lake", "CA", false], - "ZQN": ["Queenstown Airport", "Queenstown", "NZ", true], - "ZQS": ["Queen Charlotte City Seaplane Base", "Queen Charlotte", "CA", false], - "ZQW": ["Zweibrücken Airport", "Zweibrücken", "DE", false], - "ZQZ": ["Zhangjiakou Ningyuan Airport", "Zhangjiakou", "CN", true], - "ZRH": ["Zürich Airport", "Zurich", "CH", true], - "ZRI": ["Stevanus Rumbewas Airport", "Serui", "ID", false], - "ZRJ": ["Round Lake (Weagamow Lake) Airport", "Round Lake", "CA", true], - "ZRM": ["Mararena Sarmi Airport", "Sarmi", "ID", false], - "ZSA": ["San Salvador International Airport", "San Salvador", "BS", true], - "ZSE": ["Saint-Pierre Pierrefonds Airport", "Saint-Pierre", "RE", true], - "ZSJ": ["Sandy Lake Airport", "Sandy Lake", "CA", true], - "ZSS": ["Sassandra Airport", "Sassandra", "CI", false], - "ZST": ["Stewart Airport", "Stewart", "CA", false], - "ZSW": ["Prince Rupert/Seal Cove Seaplane Base", "Prince Rupert", "CA", false], - "ZTA": ["Tureia Airport", "Tureia", "PF", false], - "ZTB": ["Tête-à-la-Baleine Airport", "Tête-à-la-Baleine", "CA", true], - "ZTH": ["Zakynthos International Airport Dionysios Solomos", "Zakynthos", "GR", true], - "ZTM": ["Shamattawa Airport", "Shamattawa", "CA", true], - "ZTR": ["Zhytomyr Airport", "Zhytomyr", "UA", false], - "ZTS": ["Tahsis Seaplane Base", "Tahsis", "CA", false], - "ZTU": ["Zaqatala International Airport", "Zaqatala", "AZ", false], - "ZUC": ["Ignace Municipal Airport", "Ignace", "CA", false], - "ZUD": ["Pupelde Airport", "Ancud", "CL", false], - "ZUH": ["Zhuhai Jinwan Airport", "Zhuhai (Jinwan)", "CN", true], - "ZUL": ["Zilfi Airport", "Zilfi", "SA", false], - "ZUM": ["Churchill Falls Airport", "Churchill Falls", "CA", true], - "ZVA": ["Miandrivazo Airport", "Miandrivazo", "MG", false], - "ZVG": ["Springvale Airport", "Springvale", "AU", false], - "ZVK": ["Savannakhet Airport", "", "LA", false], - "ZWA": ["Andapa Airport", "", "MG", false], - "ZWL": ["Wollaston Lake Airport", "Wollaston Lake", "CA", true], - "ZXT": ["Zabrat Airport", "Zabrat", "AZ", false], - "ZYI": ["Zunyi Xinzhou Airport", "Zunyi", "CN", true], - "ZYL": ["Osmany International Airport", "Sylhet", "BD", true], - "ZZE": ["Zangilan International Airport", "Zangilan", "AZ", false], - "ZZO": ["Zonalnoye Airport", "Tymovskoye", "RU", false], - "ZZU": ["Mzuzu Airport", "Mzuzu", "MW", false], - "ZZV": ["Zanesville Municipal Airport", "Zanesville", "US", false] -} diff --git a/agenda/airport_lookup.py b/agenda/airport_lookup.py deleted file mode 100644 index 1d6f3dd..0000000 --- a/agenda/airport_lookup.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Local airport names from OurAirports' public-domain data (1 October 2026).""" - -import json -import typing -import unicodedata -from functools import lru_cache -from pathlib import Path - -import pycountry -import yaml - -from agenda.types import StrDict - - -def normalized(value: str) -> str: - """Match names regardless of case or accents.""" - return "".join( - c - for c in unicodedata.normalize("NFKD", value.casefold()) - if not unicodedata.combining(c) - ).strip() - - -@lru_cache(maxsize=1) -def airports() -> dict[str, list[typing.Any]]: - """Load the bundled index once; no network access during autocomplete.""" - return typing.cast( - dict[str, list[typing.Any]], - json.loads(Path(__file__).with_name("airport_index.json").read_text()), - ) - - -@lru_cache(maxsize=1) -def airport_groups() -> dict[tuple[str, str], list[str]]: - """Named cross-border groups from the conference location mappings.""" - mapping: dict[str, str | list[str]] = json.loads( - Path(__file__).with_name("conference_airports.json").read_text() - ) - return { - (country.upper(), normalized(city)): codes - for key, codes in mapping.items() - for country, city in [key.split(":", 1)] - if isinstance(codes, list) - } - - -def code_list(query: str) -> tuple[str, ...]: - """Recognize one or more exact IATA codes, without fuzzy matching.""" - codes = tuple(dict.fromkeys(part.strip().upper() for part in query.split(","))) - return ( - codes - if all(len(code) == 3 and code.isascii() and code.isalpha() for code in codes) - else () - ) - - -def preferred_names(personal_data: str | None) -> dict[str, str]: - """Read personal airport names afresh so edits appear without a reload.""" - if personal_data is None: - return {} - path = Path(personal_data) / "airports.yaml" - if not path.exists(): - return {} - records: dict[str, StrDict] = yaml.safe_load(path.read_text()) or {} - names = {} - for key, record in records.items(): - code = str(record.get("iata") or key).upper() - name = record.get("name") - if isinstance(name, str) and name.strip(): - names[code] = name.strip() - return names - - -def group_match( - codes: tuple[str, ...], preferred: dict[str, str] | None = None -) -> StrDict | None: - """Represent multiple airports as one selectable destination.""" - names = [] - for code in codes: - record = airports().get(code) - if code == "LON": - names.append("London (all airports)") - elif record: - names.append((preferred or {}).get(code, str(record[0]))) - else: - return None - return {"code": ",".join(codes), "name": " / ".join(names)} - - -def matches(query: str, personal_data: str | None = None) -> list[StrDict]: - """Prefer scheduled airports serving an exact city; accept country suffixes.""" - query = query.strip() - preferred = preferred_names(personal_data) - codes = code_list(query) - if codes: - match = group_match(codes, preferred) - return [match] if match else [] - city, _, suffix = query.partition(",") - needle = normalized(city) - if not needle: - return [] - country: str | None = None - if suffix.strip(): - try: - subdivision = typing.cast(typing.Any, pycountry.subdivisions).get( - code="US-" + suffix.strip().upper() - ) - country = ( - "US" - if subdivision - else str(pycountry.countries.lookup(suffix.strip()).alpha_2) - ) - except LookupError: - pass - for (group_country, group_city), group_codes in airport_groups().items(): - if needle == group_city and country in (None, group_country): - match = group_match(tuple(group_codes), preferred) - if match: - return [match] - candidates: list[tuple[tuple[int, int, int, str], StrDict]] = [] - for code, record in airports().items(): - name, municipality, iso_country, scheduled = record - if country and iso_country != country: - continue - name_text, city_text = normalized(name), normalized(municipality) - display_name = preferred.get(code, str(name)) - preferred_text = normalized(display_name) - if ( - needle not in name_text - and needle not in city_text - and needle not in preferred_text - ): - continue - rank = ( - int(not scheduled), - int(city_text != needle), - int( - not (name_text.startswith(needle) or preferred_text.startswith(needle)) - ), - code, - ) - candidates.append((rank, {"code": code, "name": display_name})) - candidates.sort(key=lambda item: item[0]) - result = [record for _, record in candidates[:10]] - if needle == "london" and country in (None, "GB"): - result.insert(0, {"code": "LON", "name": "London (all airports)"}) - return result[:10] diff --git a/agenda/birthday.py b/agenda/birthday.py index 6fdfd21..fdebd5d 100644 --- a/agenda/birthday.py +++ b/agenda/birthday.py @@ -4,7 +4,7 @@ from datetime import date import yaml -from .event import Event +from .types import Event YEAR_NOT_KNOWN = 1900 @@ -42,7 +42,7 @@ def get_birthdays(from_date: date, filepath: str) -> list[Event]: Event( date=bday.replace(year=bday.year + offset), name="birthday", - title=f'{entity["label"]} ({display_age})', + title=f'🎈 {entity["label"]} ({display_age})', ) ) diff --git a/agenda/bristol_waste.py b/agenda/bristol_waste.py deleted file mode 100644 index db8f80f..0000000 --- a/agenda/bristol_waste.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Waste collection schedules.""" - -import json -import os -import typing -from collections import defaultdict -from datetime import date, datetime, timedelta - -import httpx - -from .event import Event -from .utils import make_waste_dir - -ttl_hours = 12 - - -BristolSchedule = list[dict[str, typing.Any]] - - -async def get(start_date: date, data_dir: str, uprn: str, cache: str) -> list[Event]: - """Get waste collection schedule from Bristol City Council.""" - by_date: defaultdict[date, list[str]] = defaultdict(list) - for item in await get_data(data_dir, uprn, cache): - service = get_service(item) - for d in collections(item): - if d < start_date and service not in by_date[d]: - by_date[d].append(service) - - return [ - Event(name="waste_schedule", date=d, title="Bristol: " + ", ".join(services)) - for d, services in by_date.items() - ] - - -async def get_data(data_dir: str, uprn: str, cache: str) -> BristolSchedule: - """Get Bristol Waste schedule, with cache.""" - now = datetime.now() - waste_dir = os.path.join(data_dir, "waste") - - make_waste_dir(data_dir) - - existing_data = os.listdir(waste_dir) - existing = [f for f in existing_data if f.endswith(f"_{uprn}.json")] - if existing: - recent_filename = max(existing) - recent = datetime.strptime(recent_filename, f"%Y-%m-%d_%H:%M_{uprn}.json") - delta = now - recent - - def get_from_recent() -> BristolSchedule: - json_data = json.load(open(os.path.join(waste_dir, recent_filename))) - return typing.cast(BristolSchedule, json_data["data"]) - - if ( - cache != "refresh" - and existing - and (cache == "force" or delta < timedelta(hours=ttl_hours)) - ): - return get_from_recent() - - try: - r = await get_web_data(uprn) - except httpx.ReadTimeout: - return get_from_recent() - - with open(f'{waste_dir}/{now.strftime("%Y-%m-%d_%H:%M")}_{uprn}.json', "wb") as out: - out.write(r.content) - - return typing.cast(BristolSchedule, r.json()["data"]) - - -async def get_web_data(uprn: str) -> httpx.Response: - """Get JSON from Bristol City Council.""" - UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" - HEADERS = { - "Accept": "*/*", - "Accept-Language": "en-GB,en;q=0.9", - "Connection": "keep-alive", - "Ocp-Apim-Subscription-Key": "47ffd667d69c4a858f92fc38dc24b150", - "Ocp-Apim-Trace": "true", - "Origin": "https://bristolcouncil.powerappsportals.com", - "Referer": "https://bristolcouncil.powerappsportals.com/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "cross-site", - "Sec-GPC": "1", - "User-Agent": UA, - } - - _uprn = str(uprn).zfill(12) - - async with httpx.AsyncClient(timeout=20) as client: - # Initialise form - payload = {"servicetypeid": "7dce896c-b3ba-ea11-a812-000d3a7f1cdc"} - response = await client.get( - "https://bristolcouncil.powerappsportals.com/completedynamicformunauth/", - headers=HEADERS, - params=payload, - ) - - host = "bcprdapidyna002.azure-api.net" - - # Set the search criteria - payload = {"Uprn": "UPRN" + _uprn} - response = await client.post( - f"https://{host}/bcprdfundyna001-llpg/DetailedLLPG", - headers=HEADERS, - json=payload, - ) - - # Retrieve the schedule - payload = {"uprn": _uprn} - response = await client.post( - f"https://{host}/bcprdfundyna001-alloy/NextCollectionDates", - headers=HEADERS, - json=payload, - ) - - return response - - -def get_service(item: dict[str, typing.Any]) -> str: - """Bristol waste service name.""" - service: str = item["containerName"] - return "Recycling" if "Recycling" in service else service.partition(" ")[2] - - -def collections(item: dict[str, typing.Any]) -> typing.Iterable[date]: - """Bristol dates from collections.""" - for collection in item["collection"]: - for collection_date_key in ["nextCollectionDate", "lastCollectionDate"]: - yield date.fromisoformat(collection[collection_date_key][:10]) diff --git a/agenda/build_place_yaml.py b/agenda/build_place_yaml.py deleted file mode 100644 index bc346f4..0000000 --- a/agenda/build_place_yaml.py +++ /dev/null @@ -1,257 +0,0 @@ -"""Build airport and station YAML entries from Wikidata.""" - -import argparse -import typing -from pathlib import Path - -import requests -import yaml - -API_URL = "https://www.wikidata.org/w/api.php" -PERSONAL_DATA_DIR = Path("~/src/personal-data").expanduser() -USER_AGENT = "agenda-build-place-yaml/0.1" - -Entity = dict[str, typing.Any] -Entities = dict[str, Entity] - - -class WikidataClient: - """Small Wikidata API client for place lookups.""" - - def __init__(self) -> None: - """Create a Wikidata API session.""" - self.session = requests.Session() - self.session.headers.update({"User-Agent": USER_AGENT}) - - def get_json(self, params: dict[str, str]) -> dict[str, typing.Any]: - """Fetch JSON from Wikidata.""" - response = self.session.get(API_URL, params=params) - response.raise_for_status() - return typing.cast(dict[str, typing.Any], response.json()) - - def get_alpha2_country_code(self, qid: str) -> str: - """Query the Wikidata API for alpha-2 country code.""" - data = self.get_json( - { - "action": "wbgetclaims", - "entity": qid, - "property": "P297", - "format": "json", - } - ) - p297 = data["claims"]["P297"] - return typing.cast(str, p297[0]["mainsnak"]["datavalue"]["value"]).lower() - - def search_entities(self, query: str) -> Entities: - """Search Wikidata and return detailed entities.""" - search_data = self.get_json( - { - "action": "query", - "list": "search", - "format": "json", - "srsearch": query, - } - ) - search_results = search_data["query"]["search"] - if not search_results: - return {} - - ids = [result["title"] for result in search_results] - entity_data = self.get_json( - { - "action": "wbgetentities", - "format": "json", - "ids": "|".join(ids), - } - ) - return typing.cast(Entities, entity_data["entities"]) - - -def entity_names(entity: Entity) -> set[str]: - """Return labels and aliases for a Wikidata entity.""" - names: set[str] = {lang["value"] for lang in entity.get("labels", {}).values()} - for alias_list in entity.get("aliases", {}).values(): - for alias in alias_list: - names.add(alias["value"]) - return names - - -def exact_station_match(station_name: str, entity: Entity) -> bool: - """Return whether the entity names match the requested station name.""" - names = entity_names(entity) - return station_name in names or f"{station_name} station" in names - - -def entity_claim_value( - entity: Entity, property_id: str, default: typing.Any = None -) -> typing.Any: - """Return the first Wikidata claim value for a property.""" - claims = entity.get("claims", {}) - if property_id not in claims: - return default - return claims[property_id][0]["mainsnak"]["datavalue"]["value"] - - -def build_station_info( - client: WikidataClient, station_name: str, entity_id: str, entity: Entity -) -> dict[str, typing.Any]: - """Build a stations.yaml entry.""" - coords = entity_claim_value(entity, "P625") - country_value = entity_claim_value(entity, "P17") - uic = entity_claim_value(entity, "P722") - uk_station_code = entity_claim_value(entity, "P4755") - - station_info: dict[str, typing.Any] = { - "name": station_name, - "latitude": coords["latitude"], - "longitude": coords["longitude"], - "country": client.get_alpha2_country_code(country_value["id"]), - "wikidata": entity_id, - "routes": {}, - } - - if uic is not None: - station_info["uic"] = uic - if uk_station_code is not None: - station_info["alpha3"] = uk_station_code - - return station_info - - -def search_for_station( - client: WikidataClient, station_name: str -) -> dict[str, typing.Any]: - """Search for a station and return a stations.yaml entry.""" - haswbstatement = "P31=Q55488|P31=Q18543139|P31=Q1147171" - entities = client.search_entities(f"{station_name} haswbstatement:{haswbstatement}") - - for entity_id, entity in entities.items(): - if exact_station_match(station_name, entity): - return build_station_info(client, station_name, entity_id, entity) - - if entities: - entity_id, entity = next(iter(entities.items())) - return build_station_info(client, station_name, entity_id, entity) - - raise ValueError(f"No Wikidata station found for {station_name!r}") - - -def build_airport_info( - client: WikidataClient, iata: str, entity_id: str, entity: Entity -) -> dict[str, typing.Any]: - """Build an airports.yaml entry.""" - label = entity["labels"]["en"]["value"] - claims = entity["claims"] - coords = claims["P625"][0]["mainsnak"]["datavalue"]["value"] - country_qid = claims["P17"][0]["mainsnak"]["datavalue"]["value"]["id"] - - info: dict[str, typing.Any] = { - "iata": iata, - "name": label, - "city": label, - "country": client.get_alpha2_country_code(country_qid), - "latitude": coords["latitude"], - "longitude": coords["longitude"], - "qid": entity_id, - } - - website = entity_claim_value(entity, "P856") - if website is not None: - info["website"] = website - - return info - - -def search_for_airport(client: WikidataClient, iata: str) -> dict[str, typing.Any]: - """Search for an airport by IATA code and return an airports.yaml entry.""" - entities = client.search_entities(f"haswbstatement:P238={iata.upper()}") - if not entities: - raise ValueError(f"No Wikidata airport found for IATA code {iata!r}") - - entity_id, entity = next(iter(entities.items())) - return build_airport_info(client, iata.upper(), entity_id, entity) - - -def load_yaml(path: Path) -> typing.Any: - """Load a YAML file.""" - return yaml.safe_load(path.read_text()) - - -def dump_yaml(data: typing.Any) -> str: - """Dump YAML using the local personal-data style.""" - return yaml.dump(data, sort_keys=False, allow_unicode=True) - - -def dump_yaml_list_with_blank_lines(items: list[dict[str, typing.Any]]) -> str: - """Dump a YAML list with a blank line between top-level items.""" - text = dump_yaml(items).lstrip() - return text.replace("\n- ", "\n\n- ") - - -def upsert_station(data_dir: Path, station_info: dict[str, typing.Any]) -> bool: - """Add or replace a station entry. Return True when an existing entry changed.""" - path = data_dir / "stations.yaml" - stations = typing.cast(list[dict[str, typing.Any]], load_yaml(path)) - - for index, station in enumerate(stations): - if station.get("name") == station_info["name"]: - stations[index] = station_info - path.write_text(dump_yaml_list_with_blank_lines(stations)) - return True - - stations.append(station_info) - path.write_text(dump_yaml_list_with_blank_lines(stations)) - return False - - -def upsert_airport(data_dir: Path, airport_info: dict[str, typing.Any]) -> bool: - """Add or replace an airport entry. Return True when an existing entry changed.""" - path = data_dir / "airports.yaml" - airports = typing.cast(dict[str, dict[str, typing.Any]], load_yaml(path)) - iata = typing.cast(str, airport_info["iata"]) - existed = iata in airports - airports[iata] = airport_info - path.write_text(dump_yaml(airports)) - return existed - - -def station_main(argv: list[str] | None = None) -> int: - """CLI entrypoint for building and importing a station YAML entry.""" - parser = argparse.ArgumentParser( - description="Add or update a station in personal-data/stations.yaml." - ) - parser.add_argument("station_name") - parser.add_argument("--data-dir", default=str(PERSONAL_DATA_DIR)) - parser.add_argument("--print-only", action="store_true") - args = parser.parse_args(argv) - - station_info = search_for_station(WikidataClient(), args.station_name) - if args.print_only: - print(dump_yaml_list_with_blank_lines([station_info]).strip()) - return 0 - - replaced = upsert_station(Path(args.data_dir), station_info) - action = "Updated" if replaced else "Added" - print(f"{action} station {station_info['name']} in {args.data_dir}/stations.yaml") - return 0 - - -def airport_main(argv: list[str] | None = None) -> int: - """CLI entrypoint for building and importing an airport YAML entry.""" - parser = argparse.ArgumentParser( - description="Add or update an airport in personal-data/airports.yaml." - ) - parser.add_argument("iata") - parser.add_argument("--data-dir", default=str(PERSONAL_DATA_DIR)) - parser.add_argument("--print-only", action="store_true") - args = parser.parse_args(argv) - - airport_info = search_for_airport(WikidataClient(), args.iata) - if args.print_only: - print(dump_yaml({airport_info["iata"]: airport_info}).strip()) - return 0 - - replaced = upsert_airport(Path(args.data_dir), airport_info) - action = "Updated" if replaced else "Added" - print(f"{action} airport {airport_info['iata']} in {args.data_dir}/airports.yaml") - return 0 diff --git a/agenda/busy.py b/agenda/busy.py deleted file mode 100644 index 7dc744c..0000000 --- a/agenda/busy.py +++ /dev/null @@ -1,668 +0,0 @@ -"""Identify busy events and gaps when nothing is scheduled.""" - -import itertools -import typing -from datetime import date, datetime, timedelta, timezone - -import flask -import pycountry - -from . import events_yaml, get_country, travel -from .event import Event -from .types import StrDict, Trip - - -def busy_event(e: Event) -> bool: - """Busy.""" - if e.name not in { - "event", - "accommodation", - "conference", - "transport", - "meetup", - "party", - "trip", - "hackathon", - }: - return False - - if e.title in ("IA UK board meeting", "Mill Road Winter Fair"): - return False - - if e.name == "conference" and not e.going: - return False - if not e.title: - return True - if e.title == "LHG Run Club" or "Third Thursday Social" in e.title: - return False - - lc_title = e.title.lower() - return ( - "rebels" not in lc_title - and "south west data social" not in lc_title - and "dorkbot" not in lc_title - ) - - -def get_busy_events( - start: date, config: flask.config.Config, trips: list[Trip] -) -> list[Event]: - """Find busy events from a year ago to two years in the future.""" - last_year = start - timedelta(days=365) - next_year = start + timedelta(days=2 * 365) - - my_data = config["PERSONAL_DATA"] - events = events_yaml.read(my_data, last_year, next_year, skip_trips=True) - - for trip in trips: - event_type = "trip" - if trip.events and not trip.conferences: - event_type = trip.events[0]["name"] - elif len(trip.conferences) == 1 and trip.conferences[0].get("hackathon"): - event_type = "hackathon" - events.append( - Event( - name=event_type, - title=trip.title + " " + trip.country_flags, - date=trip.start, - end_date=trip.end, - url=flask.url_for("trip_page", start=trip.start.isoformat()), - ) - ) - - busy_events = [ - e - for e in sorted(events, key=lambda e: e.as_date) - if (e.as_date >= start or (e.end_date and e.end_as_date >= start)) - and e.as_date < next_year - and busy_event(e) - ] - - return busy_events - - -def _parse_datetime_field(datetime_obj: datetime | date | str) -> tuple[datetime, date]: - """Parse a datetime field that could be datetime object or string.""" - if isinstance(datetime_obj, datetime): - dt = datetime_obj - elif isinstance(datetime_obj, date): - dt = datetime.combine(datetime_obj, datetime.min.time(), tzinfo=timezone.utc) - elif isinstance(datetime_obj, str): - dt = datetime.fromisoformat(datetime_obj.replace("Z", "+00:00")) - else: - raise ValueError(f"Invalid datetime format: {datetime_obj}") - - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - else: - dt = dt.astimezone(timezone.utc) - - return dt, dt.date() - - -def _get_accommodation_location( - acc: StrDict, on_trip: bool = False -) -> tuple[str | None, pycountry.db.Country]: - """Get location from accommodation data.""" - c = get_country(acc["country"]) - assert c - assert isinstance(acc["location"], str) - return (acc["location"] if on_trip else None, c) - - -def _find_most_recent_travel_within_trip( - trip: Trip, - target_date: date, -) -> tuple[str | None, pycountry.db.Country | None] | None: - """Find the most recent travel location within a trip.""" - uk_airports = {"LHR", "LGW", "STN", "LTN", "BRS", "BHX", "MAN", "EDI", "GLA"} - - trip_most_recent_date: date | None = None - trip_most_recent_location: tuple[str | None, pycountry.db.Country | None] | None = ( - None - ) - trip_most_recent_datetime: datetime | None = None - - # Check flights within trip period - for travel_item in trip.travel: - if travel_item["type"] == "flight" and "arrive" in travel_item: - arrive_datetime, arrive_date = _parse_datetime_field(travel_item["arrive"]) - - # Only consider flights within this trip and before target date - if not (trip.start <= arrive_date <= target_date): - continue - # Compare both date and time to handle same-day flights correctly - if ( - trip_most_recent_date is None - or arrive_date > trip_most_recent_date - or ( - arrive_date == trip_most_recent_date - and ( - trip_most_recent_datetime is None - or arrive_datetime > trip_most_recent_datetime - ) - ) - ): - trip_most_recent_date = arrive_date - trip_most_recent_datetime = arrive_datetime - destination_airport = travel_item["to"] - assert "to_airport" in travel_item - airport_info = travel_item["to_airport"] - airport_country = airport_info["country"] - if airport_country == "gb": - if destination_airport in uk_airports: - # UK airport while on trip - show actual location - location_name = airport_info.get( - "city", airport_info.get("name", "London") - ) - trip_most_recent_location = ( - location_name, - get_country("gb"), - ) - else: - trip_most_recent_location = (None, get_country("gb")) - else: - location_name = airport_info.get( - "city", airport_info.get("name", destination_airport) - ) - trip_most_recent_location = ( - location_name, - get_country(airport_country), - ) - - # Check accommodations within trip period - for acc in trip.accommodation: - if "from" in acc: - try: - _, acc_date = _parse_datetime_field(acc["from"]) - except ValueError: - continue - - # Only consider accommodations within this trip and before/on target date - if trip.start <= acc_date <= target_date: - # Accommodation takes precedence over flights on the same date - # or if it's genuinely more recent - if ( - trip_most_recent_date is None - or acc_date > trip_most_recent_date - or acc_date == trip_most_recent_date - ): - trip_most_recent_date = acc_date - trip_most_recent_location = _get_accommodation_location( - acc, on_trip=True - ) - - # Check trains within trip period - for travel_item in trip.travel: - if travel_item["type"] == "train": - for leg in travel_item.get("legs", []): - if "arrive" in leg: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - leg["arrive"] - ) - except ValueError: - continue - - # Only consider trains within this trip and before target date - if trip.start <= arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - trip_most_recent_date is None - or arrive_date > trip_most_recent_date - or ( - arrive_date == trip_most_recent_date - and ( - trip_most_recent_datetime is None - or arrive_datetime > trip_most_recent_datetime - ) - ) - ): - trip_most_recent_date = arrive_date - trip_most_recent_datetime = arrive_datetime - # For trains, we can get station info from to_station if available - destination = leg.get("to") - assert "to_station" in leg - station_info = leg["to_station"] - station_country = station_info["country"] - if station_country == "gb": - trip_most_recent_location = ( - destination, - get_country("gb"), - ) - else: - trip_most_recent_location = ( - destination, - get_country(station_country), - ) - - # Check ferries within trip period - for travel_item in trip.travel: - if travel_item["type"] == "ferry" and "arrive" in travel_item: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - travel_item["arrive"] - ) - except ValueError: - continue - - # Only consider ferries within this trip and before target date - if trip.start <= arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - trip_most_recent_date is None - or arrive_date > trip_most_recent_date - or ( - arrive_date == trip_most_recent_date - and ( - trip_most_recent_datetime is None - or arrive_datetime > trip_most_recent_datetime - ) - ) - ): - trip_most_recent_date = arrive_date - trip_most_recent_datetime = arrive_datetime - # For ferries, we can get terminal info from to_terminal if available - destination = travel_item.get("to") - assert "to_terminal" in travel_item - terminal_info = travel_item["to_terminal"] - terminal_country = terminal_info.get("country", "gb") - terminal_city = terminal_info.get("city", destination) - if terminal_country == "gb": - trip_most_recent_location = ( - terminal_city, - get_country("gb"), - ) - else: - trip_most_recent_location = ( - terminal_city, - get_country(terminal_country), - ) - - return trip_most_recent_location - - -def _get_trip_location_by_progression( - trip: Trip, target_date: date -) -> tuple[str | None, pycountry.db.Country | None] | None: - """Determine location based on trip progression and date.""" - locations = trip.locations() - if not locations: - return None - - # If only one location, use it (when on a trip, always show the location) - if len(locations) == 1: - city, country = locations[0] - return (city, country) - - # Multiple locations: use progression through the trip - if not trip.end: - city, country = locations[-1] - return (city, country) - trip_duration = (trip.end - trip.start).days + 1 - days_into_trip = (target_date - trip.start).days - - # Simple progression: first half at first location, second half at last location - if days_into_trip <= trip_duration // 2: - city, country = locations[0] - else: - city, country = locations[-1] - - return (city, country) - - -def _find_most_recent_travel_before_date( - target_date: date, - trips: list[Trip], -) -> tuple[str | None, pycountry.db.Country | None] | None: - """Find the most recent travel location before a given date.""" - uk_airports = {"LHR", "LGW", "STN", "LTN", "BRS", "BHX", "MAN", "EDI", "GLA"} - - most_recent_location: tuple[str | None, pycountry.db.Country | None] | None = None - most_recent_date: date | None = None - most_recent_datetime: datetime | None = None - - # Check all travel across all trips - for trip in trips: - # Check flights - for travel_item in trip.travel: - if travel_item["type"] == "flight" and "arrive" in travel_item: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - travel_item["arrive"] - ) - except ValueError: - continue - - if arrive_date <= target_date: - # Compare both date and time to handle same-day flights correctly - if ( - most_recent_date is None - or arrive_date > most_recent_date - or ( - arrive_date == most_recent_date - and ( - most_recent_datetime is None - or arrive_datetime > most_recent_datetime - ) - ) - ): - most_recent_date = arrive_date - most_recent_datetime = arrive_datetime - destination_airport = travel_item["to"] - # For flights, determine if we're "on trip" based on whether this is within any trip period - on_trip = any( - t.start <= arrive_date <= (t.end or t.start) for t in trips - ) - - if "to_airport" in travel_item: - airport_info = travel_item["to_airport"] - airport_country = airport_info.get("country", "gb") - if airport_country == "gb": - if not on_trip: - # When not on a trip, UK airports mean home - most_recent_location = (None, get_country("gb")) - else: - # When on a trip, show the actual location even for UK airports - location_name = airport_info.get( - "city", airport_info.get("name", "London") - ) - most_recent_location = ( - location_name, - get_country("gb"), - ) - else: - location_name = airport_info.get( - "city", - airport_info.get("name", destination_airport), - ) - most_recent_location = ( - location_name, - get_country(airport_country), - ) - else: - most_recent_location = ( - destination_airport, - get_country("gb"), - ) - - # Check trains - elif travel_item["type"] == "train": - for leg in travel_item.get("legs", []): - if "arrive" in leg: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - leg["arrive"] - ) - except ValueError: - continue - - if arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - most_recent_date is None - or arrive_date > most_recent_date - or ( - arrive_date == most_recent_date - and ( - most_recent_datetime is None - or arrive_datetime > most_recent_datetime - ) - ) - ): - most_recent_date = arrive_date - most_recent_datetime = arrive_datetime - destination = leg.get("to") - on_trip = any( - t.start <= arrive_date <= (t.end or t.start) - for t in trips - ) - - if "to_station" in leg: - station_info = leg["to_station"] - station_country = station_info.get("country", "gb") - if station_country == "gb": - if not on_trip: - most_recent_location = ( - None, - get_country("gb"), - ) - else: - most_recent_location = ( - destination, - get_country("gb"), - ) - else: - most_recent_location = ( - destination, - get_country(station_country), - ) - else: - most_recent_location = ( - destination, - get_country("gb"), - ) - - # Check ferries - elif travel_item["type"] == "ferry" and "arrive" in travel_item: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - travel_item["arrive"] - ) - except ValueError: - continue - - if arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - most_recent_date is None - or arrive_date > most_recent_date - or ( - arrive_date == most_recent_date - and ( - most_recent_datetime is None - or arrive_datetime > most_recent_datetime - ) - ) - ): - most_recent_date = arrive_date - most_recent_datetime = arrive_datetime - destination = travel_item.get("to") - on_trip = any( - t.start <= arrive_date <= (t.end or t.start) for t in trips - ) - - if "to_terminal" in travel_item: - terminal_info = travel_item["to_terminal"] - terminal_country = terminal_info.get("country", "gb") - terminal_city = terminal_info.get("city", destination) - if terminal_country == "gb": - if not on_trip: - most_recent_location = (None, get_country("gb")) - else: - most_recent_location = ( - terminal_city, - get_country("gb"), - ) - else: - most_recent_location = ( - terminal_city, - get_country(terminal_country), - ) - else: - most_recent_location = (destination, get_country("gb")) - - # Check accommodation - only override if accommodation is more recent - for acc in trip.accommodation: - if "from" in acc: - try: - _, acc_date = _parse_datetime_field(acc["from"]) - except ValueError: - continue - - if acc_date <= target_date: - # Only update if this accommodation is more recent than existing result - if most_recent_date is None or acc_date > most_recent_date: - most_recent_date = acc_date - on_trip = any( - t.start <= acc_date <= (t.end or t.start) for t in trips - ) - most_recent_location = _get_accommodation_location( - acc, on_trip=on_trip - ) - - return most_recent_location - - -def _check_return_home_heuristic( - target_date: date, trips: list[Trip] -) -> tuple[str | None, pycountry.db.Country | None] | None: - """Check if should return home based on recent trips that have ended.""" - for trip in trips: - if trip.end and trip.end < target_date: - locations = trip.locations() - if locations: - final_city, final_country = locations[-1] - final_alpha_2 = final_country.alpha_2 - days_since_trip = (target_date - trip.end).days - - # If trip ended in UK, you should be home now - if hasattr(final_country, "alpha_2") and final_country.alpha_2 == "GB": - return (None, get_country("gb")) - - return None - - -def get_location_for_date( - target_date: date, - trips: list[Trip], -) -> tuple[str | None, pycountry.db.Country | None]: - """Get location (city, country) for a specific date using travel history.""" - # First check if currently on a trip - for trip in trips: - if not (trip.start <= target_date <= (trip.end or trip.start)): - continue - # For trips, find the most recent travel within the trip period - trip_location = _find_most_recent_travel_within_trip( - trip, - target_date, - ) - if trip_location: - return trip_location - - # Fallback: determine location based on trip progression and date - progression_location = _get_trip_location_by_progression(trip, target_date) - if progression_location: - return progression_location - - # Find most recent travel before this date - recent_travel = _find_most_recent_travel_before_date(target_date, trips) - - # Check for recent trips that have ended - prioritize this over individual travel data - # This handles cases where you're traveling home after a trip (e.g. stopovers, connections) - return_home = _check_return_home_heuristic(target_date, trips) - if return_home: - return return_home - - # Return most recent location or default to home - if recent_travel: - return recent_travel - - return (None, get_country("gb")) - - -def weekends( - start: date, busy_events: list[Event], trips: list[Trip], data_dir: str -) -> typing.Sequence[StrDict]: - """Next ten weekends.""" - weekday = start.weekday() - - # Calculate the difference to the next or previous Saturday - if weekday == 6: # Sunday - start_date = start - timedelta(days=1) - else: - start_date = start + timedelta(days=(5 - weekday)) - - weekends_info = [] - for i in range(52): - saturday = start_date + timedelta(weeks=i) - sunday = saturday + timedelta(days=1) - - saturday_events = [ - event - for event in busy_events - if event.end_date and event.as_date <= saturday <= event.end_as_date - ] - sunday_events = [ - event - for event in busy_events - if event.end_date and event.as_date <= sunday <= event.end_as_date - ] - - saturday_location = get_location_for_date( - saturday, - trips, - ) - sunday_location = get_location_for_date( - sunday, - trips, - ) - - weekends_info.append( - { - "date": saturday, - "saturday": saturday_events, - "sunday": sunday_events, - "saturday_location": saturday_location, - "sunday_location": sunday_location, - } - ) - - return weekends_info - - -def find_gaps(events: list[Event], min_gap_days: int = 3) -> list[StrDict]: - """Gaps of at least `min_gap_days` between events in a list of events.""" - # Sort events by start date - - gaps: list[tuple[date, date]] = [] - previous_event_end = None - - by_start_date = { - d: list(on_day) - for d, on_day in itertools.groupby(events, key=lambda e: e.as_date) - } - - by_end_date = { - d: list(on_day) - for d, on_day in itertools.groupby(events, key=lambda e: e.end_as_date) - } - - for event in events: - # Use start date for current event - start_date = event.as_date - - # If previous event exists, calculate the gap - if previous_event_end: - gap_days = (start_date - previous_event_end).days - if gap_days >= (min_gap_days + 2): - start_end = ( - previous_event_end + timedelta(days=1), - start_date - timedelta(days=1), - ) - gaps.append(start_end) - - # Update previous event end date - end = event.end_as_date - if not previous_event_end or end > previous_event_end: - previous_event_end = end - - return [ - { - "start": gap_start, - "end": gap_end, - "after": by_start_date[gap_end + timedelta(days=1)], - "before": by_end_date[gap_start - timedelta(days=1)], - } - for gap_start, gap_end in gaps - ] diff --git a/agenda/calendar.py b/agenda/calendar.py index 0a66b6c..4d2a8d8 100644 --- a/agenda/calendar.py +++ b/agenda/calendar.py @@ -1,115 +1,79 @@ """Calendar.""" import typing -import uuid from datetime import timedelta -from .event import Event +from .types import Event -# A map to associate event types with a specific calendar ID -event_type_calendar_map = { - "bank_holiday": "uk_holidays", - "conference": "conferences", - "us_holiday": "us_holidays", - "birthday": "birthdays", - "waste_schedule": "home", - "accommodation": "travel", - "market": "markets", +event_type_color_map = { + "bank_holiday": "success-subtle", + "conference": "primary-subtle", + "us_holiday": "secondary-subtle", + "birthday": "info-subtle", + "waste_schedule": "danger-subtle", } -# Define the calendars (categories) for TOAST UI -# These will be passed to the frontend to configure colors and names -toastui_calendars = [ - {"id": "default", "name": "General", "backgroundColor": "#00a9ff"}, - {"id": "uk_holidays", "name": "UK Bank Holiday", "backgroundColor": "#28a745"}, - {"id": "us_holidays", "name": "US Holiday", "backgroundColor": "#6c757d"}, - {"id": "conferences", "name": "Conference", "backgroundColor": "#007bff"}, - {"id": "birthdays", "name": "Birthday", "backgroundColor": "#17a2b8"}, - {"id": "home", "name": "Home", "backgroundColor": "#dc3545"}, - {"id": "travel", "name": "Travel", "backgroundColor": "#ffc107", "color": "#000"}, - {"id": "markets", "name": "Markets", "backgroundColor": "#e2e3e5", "color": "#000"}, -] +colors = { + "primary-subtle": "#cfe2ff", + "secondary-subtle": "#e2e3e5", + "success-subtle": "#d1e7dd", + "info-subtle": "#cff4fc", + "warning-subtle": "#fff3cd", + "danger-subtle": "#f8d7da", +} -def build_toastui_events(events: list[Event]) -> list[dict[str, typing.Any]]: - """Build a list of event objects for TOAST UI Calendar.""" +def build_events(events: list[Event]) -> list[dict[str, typing.Any]]: + """Build list of events for FullCalendar.""" items: list[dict[str, typing.Any]] = [] + one_day = timedelta(days=1) for e in events: if e.name == "today": continue - - # Determine the calendar ID for the event, defaulting if not mapped - calendar_id = event_type_calendar_map.get(e.name, "default") - - # Handle special case for 'accommodation' if e.name == "accommodation": assert e.title and e.end_date - # All-day event for the duration of the stay - items.append( - { - "id": str(uuid.uuid4()), - "calendarId": calendar_id, - "title": e.title_with_emoji, - "start": e.as_date.isoformat(), - "end": (e.end_as_date + one_day).isoformat(), - "isAllday": True, - "raw": {"url": e.url}, - } - ) - # Timed event for check-in - items.append( - { - "id": str(uuid.uuid4()), - "calendarId": calendar_id, - "title": f"Check-in: {e.title}", - "start": e.date.isoformat(), - "end": (e.date + timedelta(hours=1)).isoformat(), - "isAllday": False, - "raw": {"url": e.url}, - } - ) - # Timed event for check-out - items.append( - { - "id": str(uuid.uuid4()), - "calendarId": calendar_id, - "title": f"Checkout: {e.title}", - "start": e.end_date.isoformat(), - "end": (e.end_date + timedelta(hours=1)).isoformat(), - "isAllday": False, - "raw": {"url": e.url}, - } - ) + item = { + "allDay": True, + "title": e.display_title, + "start": e.as_date.isoformat(), + "end": (e.end_as_date + one_day).isoformat(), + "url": e.url, + } + items.append(item) + + item = { + "allDay": False, + "title": "checkin: " + e.title, + "start": e.date.isoformat(), + "url": e.url, + } + items.append(item) + item = { + "allDay": False, + "title": "checkout: " + e.title, + "start": e.end_date.isoformat(), + "url": e.url, + } + items.append(item) + continue - # Handle all other events - start_iso = e.date.isoformat() if e.has_time: - end_iso = (e.end_date or e.date + timedelta(minutes=30)).isoformat() + end = e.end_date or e.date + timedelta(hours=1) else: - # For all-day events, the end date is exclusive. - # For a single-day event, the end date should be the same as the start date. - # For a multi-day event, a day is added to make the range inclusive. - if e.end_date: - # This is a multi-day event, so add one day to the end. - end_date = e.end_as_date + one_day - else: - # This is a single-day event. The end date is the same as the start date. - end_date = e.as_date - end_iso = end_date.isoformat() - - item: dict[str, typing.Any] = { - "id": str(uuid.uuid4()), - "calendarId": calendar_id, - "title": e.title_with_emoji, - "start": start_iso, - "end": end_iso, - "isAllday": not e.has_time, + end = (e.end_as_date if e.end_date else e.as_date) + one_day + item = { + "allDay": not e.has_time, + "title": e.display_title, + "start": e.date.isoformat(), + "end": end.isoformat(), } + if e.name in event_type_color_map: + item["color"] = colors[event_type_color_map[e.name]] + item["textColor"] = "black" if e.url: - item["raw"] = {"url": e.url} + item["url"] = e.url items.append(item) - return items diff --git a/agenda/car_journey_timeline.py b/agenda/car_journey_timeline.py deleted file mode 100644 index d3518ee..0000000 --- a/agenda/car_journey_timeline.py +++ /dev/null @@ -1,474 +0,0 @@ -"""Import recorded car journeys from a Google Timeline export.""" - -from __future__ import annotations - -import argparse -import json -import typing -from collections import defaultdict -from dataclasses import dataclass -from datetime import date, datetime, timedelta -from pathlib import Path - -from geopy.distance import geodesic # type: ignore - -from . import car_journey_yaml, trip as trip_module, utils - -PERSONAL_DATA_DIR = Path("~/src/personal-data").expanduser() -VEHICLE_ACTIVITY_TYPES = frozenset({"IN_PASSENGER_VEHICLE", "IN_VEHICLE"}) -ACTIVITY_SPLIT_GAP = timedelta(hours=2) - -LatLon = tuple[float, float] -LonLat = tuple[float, float] -StrDict = dict[str, typing.Any] - - -@dataclass(frozen=True) -class TimelinePoint: - """A single Google Timeline breadcrumb point.""" - - time: datetime - location: LatLon - - @property - def lonlat(self) -> LonLat: - """Return point as GeoJSON lon/lat.""" - return (self.location[1], self.location[0]) - - -@dataclass(frozen=True) -class VehicleActivity: - """A vehicle activity segment plus its breadcrumb points.""" - - index: int - mode: str - start_time: datetime - end_time: datetime - start: LatLon - end: LatLon - points: tuple[TimelinePoint, ...] - - @property - def path(self) -> tuple[LatLon, ...]: - """Return a deduplicated path.""" - coords = [point.location for point in self.points] - if not coords: - coords = [self.start, self.end] - elif coords[0] != self.start: - coords.insert(0, self.start) - if coords[-1] != self.end: - coords.append(self.end) - return tuple(dedupe_consecutive(coords)) - - @property - def depart_time(self) -> datetime: - """Prefer the first breadcrumb time when available.""" - return self.points[0].time if self.points else self.start_time - - @property - def arrive_time(self) -> datetime: - """Prefer the last breadcrumb time when available.""" - return self.points[-1].time if self.points else self.end_time - - @property - def leading_path(self) -> tuple[LatLon, ...]: - """Return the first few coordinates for start matching.""" - return self.path[: min(3, len(self.path))] - - @property - def trailing_path(self) -> tuple[LatLon, ...]: - """Return the last few coordinates for end matching.""" - return self.path[-min(3, len(self.path)) :] - - -@dataclass(frozen=True) -class TimelineCarJourney: - """A YAML-ready car journey row with a GeoJSON path.""" - - trip: date - depart: datetime - arrive: datetime - route: str - from_label: str - to_label: str - path: tuple[LatLon, ...] - mode: str - - def as_yaml_item(self) -> StrDict: - """Return the YAML mapping.""" - return { - "trip": self.trip, - "depart": self.depart, - "arrive": self.arrive, - "route": self.route, - "from": self.from_label, - "to": self.to_label, - } - - def as_geojson(self) -> StrDict: - """Return a GeoJSON feature for this journey.""" - return { - "type": "Feature", - "properties": { - "source": "google_timeline", - "mode": self.mode, - "start_time": self.depart.isoformat(), - "end_time": self.arrive.isoformat(), - }, - "geometry": { - "type": "LineString", - "coordinates": [[lon, lat] for lat, lon in self.path], - }, - } - - -def parse_latlng(value: str) -> LatLon: - """Parse Google's ``lat°, lon°`` format.""" - latitude, longitude = value.replace("°", "").split(",", 1) - return (float(latitude.strip()), float(longitude.strip())) - - -def days_spanned(start: datetime, end: datetime) -> list[date]: - """Return all dates touched by a datetime range.""" - day = start.date() - days = [day] - while day < end.date(): - day += timedelta(days=1) - days.append(day) - return days - - -def dedupe_consecutive(coords: list[LatLon]) -> list[LatLon]: - """Drop repeated adjacent coordinates.""" - deduped: list[LatLon] = [] - for coord in coords: - if deduped and deduped[-1] == coord: - continue - deduped.append(coord) - return deduped - - -def load_timeline_path_points(data: StrDict) -> dict[date, list[TimelinePoint]]: - """Group all timelinePath points by local date.""" - points_by_day: dict[date, list[TimelinePoint]] = defaultdict(list) - for segment in data.get("semanticSegments", []): - timeline_path = segment.get("timelinePath") - if not isinstance(timeline_path, list): - continue - for item in timeline_path: - if not isinstance(item, dict): - continue - point = item.get("point") - time_value = item.get("time") - if not isinstance(point, str) or not isinstance(time_value, str): - continue - breadcrumb = TimelinePoint( - time=datetime.fromisoformat(time_value), - location=parse_latlng(point), - ) - points_by_day[breadcrumb.time.date()].append(breadcrumb) - for breadcrumbs in points_by_day.values(): - breadcrumbs.sort(key=lambda item: item.time) - return points_by_day - - -def activity_points( - start: datetime, - end: datetime, - points_by_day: dict[date, list[TimelinePoint]], -) -> tuple[TimelinePoint, ...]: - """Return breadcrumb points within an activity window.""" - selected: list[TimelinePoint] = [] - for day in days_spanned(start, end): - for point in points_by_day.get(day, []): - if start <= point.time <= end: - selected.append(point) - selected.sort(key=lambda item: item.time) - return tuple(selected) - - -def load_vehicle_activities(timeline_path: Path) -> list[VehicleActivity]: - """Load vehicle activities and attach breadcrumb points.""" - data = typing.cast(StrDict, json.loads(timeline_path.read_text())) - points_by_day = load_timeline_path_points(data) - activities: list[VehicleActivity] = [] - next_index = 0 - for index, segment in enumerate(data.get("semanticSegments", [])): - activity = segment.get("activity") - if not isinstance(activity, dict): - continue - mode = activity.get("topCandidate", {}).get("type") - start_location = activity.get("start", {}).get("latLng") - end_location = activity.get("end", {}).get("latLng") - start_time = segment.get("startTime") - end_time = segment.get("endTime") - if ( - mode not in VEHICLE_ACTIVITY_TYPES - or not isinstance(start_location, str) - or not isinstance(end_location, str) - or not isinstance(start_time, str) - or not isinstance(end_time, str) - ): - continue - - depart = datetime.fromisoformat(start_time) - arrive = datetime.fromisoformat(end_time) - start_latlon = parse_latlng(start_location) - end_latlon = parse_latlng(end_location) - activity = VehicleActivity( - index=index, - mode=typing.cast(str, mode), - start_time=depart, - end_time=arrive, - start=start_latlon, - end=end_latlon, - points=activity_points( - depart, - arrive, - points_by_day, - ), - ) - for split_activity in split_activity_on_gaps(activity): - activities.append( - VehicleActivity( - index=next_index, - mode=split_activity.mode, - start_time=split_activity.start_time, - end_time=split_activity.end_time, - start=split_activity.start, - end=split_activity.end, - points=split_activity.points, - ) - ) - next_index += 1 - return activities - - -def activity_matches_date(activity: VehicleActivity, target: date) -> bool: - """Return whether an activity overlaps a date.""" - return target in days_spanned(activity.start_time, activity.end_time) - - -def split_activity_on_gaps(activity: VehicleActivity) -> list[VehicleActivity]: - """Split an activity when breadcrumbs contain long pauses.""" - if len(activity.points) < 2: - return [activity] - - chunks: list[list[TimelinePoint]] = [[]] - for point in activity.points: - current = chunks[-1] - if current and point.time - current[-1].time > ACTIVITY_SPLIT_GAP: - chunks.append([]) - current = chunks[-1] - current.append(point) - - if len(chunks) == 1: - return [activity] - - split: list[VehicleActivity] = [] - for chunk in chunks: - split.append( - VehicleActivity( - index=activity.index, - mode=activity.mode, - start_time=chunk[0].time, - end_time=chunk[-1].time, - start=chunk[0].location, - end=chunk[-1].location, - points=tuple(chunk), - ) - ) - return split - - -def distance_to_coords_km(coords: tuple[LatLon, ...], target: LatLon) -> float: - """Return the closest coordinate distance to a target coordinate.""" - return min(float(geodesic(coord, target).km) for coord in coords) - - -def select_activity_slice( - activities: list[VehicleActivity], - start_target: LatLon, - end_target: LatLon, -) -> list[VehicleActivity]: - """Select the contiguous activity slice that best matches a planned leg.""" - if not activities: - raise ValueError("no vehicle activities available for route matching") - - best_slice: tuple[float, int, int] | None = None - for start_index, activity in enumerate(activities): - start_distance = distance_to_coords_km(activity.leading_path, start_target) - for end_index in range(start_index, len(activities)): - end_distance = distance_to_coords_km( - activities[end_index].trailing_path, - end_target, - ) - score = start_distance + end_distance - if ( - best_slice is None - or score < best_slice[0] - or ( - score == best_slice[0] - and end_index - start_index < best_slice[2] - best_slice[1] - ) - ): - best_slice = (score, start_index, end_index) - - assert best_slice is not None - return activities[best_slice[1] : best_slice[2] + 1] - - -def stop_labels( - from_label: str, - to_label: str, - count: int, -) -> list[tuple[str, str]]: - """Return start/end labels for a split route.""" - if count == 1: - return [(from_label, to_label)] - labels: list[tuple[str, str]] = [] - for index in range(count): - start = from_label if index == 0 else f"Drive stop {index}" - end = to_label if index == count - 1 else f"Drive stop {index + 1}" - labels.append((start, end)) - return labels - - -def route_name(activity: VehicleActivity) -> str: - """Return a stable route filename stem for an imported activity.""" - return "timeline_" + activity.depart_time.strftime("%Y%m%d_%H%M%S") - - -def build_timeline_journeys( - timeline_path: Path, - data_dir: Path, - today: date | None = None, -) -> list[TimelineCarJourney]: - """Generate split past car journeys from timeline data.""" - if today is None: - today = date.today() - - activities = load_vehicle_activities(timeline_path) - existing = car_journey_yaml.load_yaml_list(data_dir / "car_journeys.yaml") - generated: list[TimelineCarJourney] = [] - - for item in existing: - trip_date = utils.as_date(item["trip"]) - if trip_date >= today: - continue - - route = item.get("route") - if not isinstance(route, str): - raise ValueError(f"car journey route must be a string: {item!r}") - - route_path = car_journey_yaml.car_route_path(data_dir, route) - existing_geojson = car_journey_yaml.read_geojson(route_path) - endpoints = trip_module.geojson_route_endpoints(existing_geojson) - if endpoints is None: - raise ValueError(f"could not read route endpoints from {route_path}") - - from_label, to_label = trip_module.car_route_labels(item) - if not isinstance(from_label, str) or not isinstance(to_label, str): - raise ValueError(f"could not determine car route labels for {item!r}") - - journey_date = utils.as_date(item["depart"]) - matches = [ - activity - for activity in activities - if activity_matches_date(activity, journey_date) - ] - selected = select_activity_slice(matches, endpoints[0], endpoints[1]) - labels = stop_labels(from_label, to_label, len(selected)) - for activity, (start_label, end_label) in zip(selected, labels, strict=True): - generated.append( - TimelineCarJourney( - trip=trip_date, - depart=activity.depart_time, - arrive=activity.arrive_time, - route=route_name(activity), - from_label=start_label, - to_label=end_label, - path=activity.path, - mode=activity.mode, - ) - ) - - return generated - - -def write_timeline_routes( - data_dir: Path, - journeys: list[TimelineCarJourney], -) -> int: - """Write GeoJSON files for generated journeys.""" - route_dir = data_dir / "car_routes" - route_dir.mkdir(parents=True, exist_ok=True) - written = 0 - for journey in journeys: - path = route_dir / f"{journey.route}.geojson" - path.write_text(json.dumps(journey.as_geojson(), separators=(",", ":")) + "\n") - written += 1 - return written - - -def rewrite_car_journeys_yaml( - data_dir: Path, - journeys: list[TimelineCarJourney], - today: date | None = None, -) -> tuple[int, int]: - """Replace past YAML rows with timeline-derived journeys.""" - if today is None: - today = date.today() - - path = data_dir / "car_journeys.yaml" - existing = car_journey_yaml.load_yaml_list(path) - future_rows = [item for item in existing if utils.as_date(item["trip"]) >= today] - generated_rows = [journey.as_yaml_item() for journey in journeys] - combined = generated_rows + future_rows - combined.sort(key=car_journey_yaml.journey_key) - path.write_text(car_journey_yaml.dump_yaml_list_with_blank_lines(combined)) - replaced = len(existing) - len(future_rows) - return (replaced, len(generated_rows)) - - -def import_from_timeline( - timeline_path: Path, - data_dir: Path = PERSONAL_DATA_DIR, - today: date | None = None, -) -> tuple[int, int, int]: - """Write route files and replace past YAML rows.""" - journeys = build_timeline_journeys(timeline_path, data_dir, today=today) - routes_written = write_timeline_routes(data_dir, journeys) - rows_replaced, rows_written = rewrite_car_journeys_yaml( - data_dir, journeys, today=today - ) - return (routes_written, rows_replaced, rows_written) - - -def parse_date(value: str) -> date: - """Parse an ISO date.""" - return date.fromisoformat(value) - - -def main(argv: list[str] | None = None) -> int: - """CLI entrypoint.""" - parser = argparse.ArgumentParser( - description=( - "Replace past car_journeys.yaml rows with routes split from a " - "Google Timeline export." - ) - ) - parser.add_argument("timeline", type=Path) - parser.add_argument("--data-dir", type=Path, default=PERSONAL_DATA_DIR) - parser.add_argument("--today", type=parse_date, default=None) - args = parser.parse_args(argv) - - routes_written, rows_replaced, rows_written = import_from_timeline( - args.timeline, - data_dir=args.data_dir, - today=args.today, - ) - print(f"Wrote {routes_written} route GeoJSON file(s)") - print(f"Replaced {rows_replaced} past car journey row(s)") - print(f"Wrote {rows_written} split timeline journey row(s)") - return 0 diff --git a/agenda/car_journey_yaml.py b/agenda/car_journey_yaml.py deleted file mode 100644 index e8cb1f9..0000000 --- a/agenda/car_journey_yaml.py +++ /dev/null @@ -1,762 +0,0 @@ -"""Generate car journey YAML and routes for trips.""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import typing -import unicodedata -from dataclasses import dataclass -from datetime import date, datetime -from pathlib import Path - -import yaml - -try: - import openrouteservice # type: ignore[import-not-found] -except ImportError: # pragma: no cover - optional dependency in tests - openrouteservice = None - -from . import trip as trip_module -from . import utils - -PERSONAL_DATA_DIR = Path("~/src/personal-data").expanduser() -DEFAULT_HOME_LABEL = "PCH" -DEFAULT_PROFILE = "driving-car" -DEFAULT_SNAP_RADIUS_METRES = 5000 - -LatLon = tuple[float, float] -LonLat = tuple[float, float] -GeoJSON = dict[str, typing.Any] -StrDict = dict[str, typing.Any] -RouteFetcher = typing.Callable[[LonLat, LonLat], GeoJSON] - - -class NoAliasDumper(yaml.SafeDumper): - """YAML dumper that avoids anchors for repeated scalar values.""" - - def ignore_aliases(self, data: typing.Any) -> bool: - """Disable aliases for all values.""" - return True - - -class TripLike(typing.Protocol): - """Trip fields needed for car journey generation.""" - - start: date - accommodation: list[StrDict] - travel: list[StrDict] - - -@dataclass(frozen=True) -class CarJourney: - """A car journey ready to write to YAML and GeoJSON.""" - - trip: date - depart: date - arrive: date - route: str - start: LonLat - end: LonLat - - def as_yaml_item(self) -> StrDict: - """Return the YAML mapping for this journey.""" - return { - "trip": self.trip, - "depart": self.depart, - "arrive": self.arrive, - "route": self.route, - } - - -def latlon_to_lonlat(coord: LatLon) -> LonLat: - """Convert a lat/lon pair to lon/lat for routing APIs.""" - return (coord[1], coord[0]) - - -def route_label(value: str) -> str: - """Return a filesystem-safe route label.""" - ascii_value = ( - unicodedata.normalize( - "NFKD", - value.strip() - .replace("æ", "ae") - .replace("Æ", "AE") - .replace("ð", "d") - .replace("Ð", "D") - .replace("þ", "th") - .replace("Þ", "Th"), - ) - .encode("ascii", "ignore") - .decode("ascii") - ) - label = re.sub(r"[^A-Za-z0-9]+", "_", ascii_value).strip("_") - if not label: - raise ValueError(f"empty route label from {value!r}") - return label - - -def load_yaml_list(path: Path) -> list[StrDict]: - """Load a YAML list, returning an empty list when the file does not exist.""" - if not path.exists(): - return [] - loaded = yaml.safe_load(path.read_text()) - if loaded is None: - return [] - if isinstance(loaded, list) and all(isinstance(item, dict) for item in loaded): - return typing.cast(list[StrDict], loaded) - raise ValueError(f"{path} must contain a YAML list") - - -def dump_yaml_list_with_blank_lines(items: list[StrDict]) -> str: - """Dump a top-level YAML list using the personal-data spacing style.""" - if not items: - return "---\n" - text = yaml.dump( - items, sort_keys=False, allow_unicode=True, Dumper=NoAliasDumper - ).lstrip() - return "---\n" + text.replace("\n- ", "\n\n- ") - - -def car_route_path(data_dir: Path, route: str) -> Path: - """Return the GeoJSON path for a car route name.""" - route_filename = trip_module.route_filename_without_extension(route) - return data_dir / "car_routes" / f"{route_filename}.geojson" - - -def read_geojson(path: Path) -> GeoJSON: - """Read a GeoJSON file.""" - return typing.cast(GeoJSON, json.loads(path.read_text())) - - -def home_latlon_from_existing_routes(data_dir: Path, home_label: str) -> LatLon: - """Find the home coordinate from existing car routes.""" - route_dir = data_dir / "car_routes" - if not route_dir.exists(): - raise ValueError(f"missing car route directory: {route_dir}") - - home_route_prefix = f"{home_label}_to_" - home_route_suffix = f"_to_{home_label}" - - for path in sorted(route_dir.glob("*.geojson")): - geojson_data = read_geojson(path) - endpoints = trip_module.geojson_route_endpoints(geojson_data) - if endpoints is None: - continue - route_name = path.stem - if route_name.startswith(home_route_prefix): - return endpoints[0] - if route_name.endswith(home_route_suffix): - return endpoints[1] - - raise ValueError( - f"could not find home coordinate for {home_label!r} in {route_dir}" - ) - - -def trip_for_date(data_dir: Path, trip_date: date) -> TripLike: - """Return the trip with the given start date.""" - trips = typing.cast( - list[TripLike], trip_module.build_trip_list(data_dir=str(data_dir)) - ) - for item in trips: - if item.start == trip_date: - return item - raise ValueError(f"could not find trip starting {trip_date}") - - -def primary_accommodation(trip: TripLike) -> StrDict: - """Return the first accommodation for a trip.""" - if not trip.accommodation: - raise ValueError(f"trip {trip.start} has no accommodation") - return min(trip.accommodation, key=lambda item: utils.as_datetime(item["from"])) - - -def accommodation_label(accommodation: StrDict) -> str: - """Return the destination label for an accommodation.""" - label = accommodation.get("location") or accommodation.get("name") - if not isinstance(label, str): - raise ValueError("accommodation has no location or name") - return route_label(label) - - -def accommodation_latlon(accommodation: StrDict) -> LatLon: - """Return accommodation coordinates.""" - latitude = accommodation.get("latitude") - longitude = accommodation.get("longitude") - if not isinstance(latitude, (int, float)) or not isinstance( - longitude, (int, float) - ): - raise ValueError("accommodation is missing latitude/longitude") - return (float(latitude), float(longitude)) - - -def sorted_accommodation(trip: TripLike) -> list[StrDict]: - """Return trip accommodation sorted by check-in time.""" - return sorted(trip.accommodation, key=lambda item: utils.as_datetime(item["from"])) - - -def airport_latlon(airport: StrDict) -> LatLon: - """Return airport coordinates.""" - latitude = airport.get("latitude") - longitude = airport.get("longitude") - if not isinstance(latitude, (int, float)) or not isinstance( - longitude, (int, float) - ): - raise ValueError("airport is missing latitude/longitude") - return (float(latitude), float(longitude)) - - -def flight_departure_datetime(item: StrDict) -> typing.Any: - """Return flight departure value for sorting.""" - return item["depart"] - - -def trip_flights(trip: TripLike) -> list[StrDict]: - """Return trip flights sorted by departure time.""" - flights = [item for item in trip.travel if item.get("type") == "flight"] - if not flights: - raise ValueError(f"trip {trip.start} has no flights") - return sorted( - flights, key=lambda item: utils.as_datetime(flight_departure_datetime(item)) - ) - - -def trip_ferries(trip: TripLike) -> list[StrDict]: - """Return trip ferries sorted by departure time.""" - ferries = [item for item in trip.travel if item.get("type") == "ferry"] - if not ferries: - raise ValueError(f"trip {trip.start} has no ferries") - return sorted(ferries, key=lambda item: utils.as_datetime(item["depart"])) - - -def location_latlon(location: StrDict, label: str) -> LatLon: - """Return coordinates from a loaded travel location.""" - latitude = location.get("latitude") - longitude = location.get("longitude") - if not isinstance(latitude, (int, float)) or not isinstance( - longitude, (int, float) - ): - raise ValueError(f"{label} is missing latitude/longitude") - return (float(latitude), float(longitude)) - - -def location_name(location: StrDict, fallback: str) -> str: - """Return a route label source for a loaded travel location.""" - name = location.get("name") or fallback - if not isinstance(name, str): - raise ValueError(f"location name must be a string: {location!r}") - return name - - -def car_journeys_for_accommodation( - trip: TripLike, - data_dir: Path, - home_label: str = DEFAULT_HOME_LABEL, - destination_label: str | None = None, -) -> list[CarJourney]: - """Build outbound and return car journeys for a trip's primary accommodation.""" - accommodation = primary_accommodation(trip) - home = latlon_to_lonlat(home_latlon_from_existing_routes(data_dir, home_label)) - destination = latlon_to_lonlat(accommodation_latlon(accommodation)) - destination_route_label = destination_label or accommodation_label(accommodation) - outbound_route = f"{route_label(home_label)}_to_{destination_route_label}" - return_route = f"{destination_route_label}_to_{route_label(home_label)}" - - check_in = utils.as_date(accommodation["from"]) - check_out = utils.as_date(accommodation["to"]) - - return [ - CarJourney( - trip=trip.start, - depart=check_in, - arrive=check_in, - route=outbound_route, - start=home, - end=destination, - ), - CarJourney( - trip=trip.start, - depart=check_out, - arrive=check_out, - route=return_route, - start=destination, - end=home, - ), - ] - - -def car_journeys_for_ferry( - trip: TripLike, - data_dir: Path, - home_label: str = DEFAULT_HOME_LABEL, - destination_label: str | None = None, -) -> list[CarJourney]: - """Build car journeys around an outbound and return car ferry crossing.""" - ferries = trip_ferries(trip) - outbound_ferry = ferries[0] - return_ferry = ferries[-1] - accommodation = primary_accommodation(trip) - - home = latlon_to_lonlat(home_latlon_from_existing_routes(data_dir, home_label)) - accommodation_coord = latlon_to_lonlat(accommodation_latlon(accommodation)) - accommodation_route_label = destination_label or accommodation_label(accommodation) - - outbound_from_terminal = outbound_ferry.get("from_terminal") - outbound_to_terminal = outbound_ferry.get("to_terminal") - return_from_terminal = return_ferry.get("from_terminal") - return_to_terminal = return_ferry.get("to_terminal") - terminals = ( - outbound_from_terminal, - outbound_to_terminal, - return_from_terminal, - return_to_terminal, - ) - if not all(isinstance(terminal, dict) for terminal in terminals): - raise ValueError("ferry is missing terminal details") - - outbound_from = typing.cast(StrDict, outbound_from_terminal) - outbound_to = typing.cast(StrDict, outbound_to_terminal) - return_from = typing.cast(StrDict, return_from_terminal) - return_to = typing.cast(StrDict, return_to_terminal) - - home_route_label = route_label(home_label) - outbound_from_label = route_label( - location_name(outbound_from, typing.cast(str, outbound_ferry.get("from", ""))) - ) - outbound_to_label = route_label( - location_name(outbound_to, typing.cast(str, outbound_ferry.get("to", ""))) - ) - return_from_label = route_label( - location_name(return_from, typing.cast(str, return_ferry.get("from", ""))) - ) - return_to_label = route_label( - location_name(return_to, typing.cast(str, return_ferry.get("to", ""))) - ) - - outbound_from_coord = latlon_to_lonlat( - location_latlon(outbound_from, outbound_from_label) - ) - outbound_to_coord = latlon_to_lonlat( - location_latlon(outbound_to, outbound_to_label) - ) - return_from_coord = latlon_to_lonlat( - location_latlon(return_from, return_from_label) - ) - return_to_coord = latlon_to_lonlat(location_latlon(return_to, return_to_label)) - - return [ - CarJourney( - trip=trip.start, - depart=utils.as_date(outbound_ferry["depart"]), - arrive=utils.as_date(outbound_ferry["depart"]), - route=f"{home_route_label}_to_{outbound_from_label}", - start=home, - end=outbound_from_coord, - ), - CarJourney( - trip=trip.start, - depart=utils.as_date(outbound_ferry["arrive"]), - arrive=utils.as_date(outbound_ferry["arrive"]), - route=f"{outbound_to_label}_to_{accommodation_route_label}", - start=outbound_to_coord, - end=accommodation_coord, - ), - CarJourney( - trip=trip.start, - depart=utils.as_date(return_ferry["depart"]), - arrive=utils.as_date(return_ferry["depart"]), - route=f"{accommodation_route_label}_to_{return_from_label}", - start=accommodation_coord, - end=return_from_coord, - ), - CarJourney( - trip=trip.start, - depart=utils.as_date(return_ferry["arrive"]), - arrive=utils.as_date(return_ferry["arrive"]), - route=f"{return_to_label}_to_{home_route_label}", - start=return_to_coord, - end=home, - ), - ] - - -def car_journeys_for_airport( - trip: TripLike, - data_dir: Path, - home_label: str = DEFAULT_HOME_LABEL, -) -> list[CarJourney]: - """Build outbound and return car journeys for the trip's UK airport.""" - flights = trip_flights(trip) - outbound_flight = flights[0] - return_flight = flights[-1] - outbound_airport = outbound_flight.get("from_airport") - return_airport = return_flight.get("to_airport") - if not isinstance(outbound_airport, dict) or not isinstance(return_airport, dict): - raise ValueError("flight is missing airport details") - - outbound_iata = outbound_airport.get("iata") - return_iata = return_airport.get("iata") - if not isinstance(outbound_iata, str) or not isinstance(return_iata, str): - raise ValueError("flight airport is missing IATA code") - - home = latlon_to_lonlat(home_latlon_from_existing_routes(data_dir, home_label)) - outbound_destination = latlon_to_lonlat(airport_latlon(outbound_airport)) - return_start = latlon_to_lonlat(airport_latlon(return_airport)) - home_route_label = route_label(home_label) - outbound_route_label = route_label(outbound_iata) - return_route_label = route_label(return_iata) - - return [ - CarJourney( - trip=trip.start, - depart=utils.as_date(outbound_flight["depart"]), - arrive=utils.as_date(outbound_flight["depart"]), - route=f"{home_route_label}_to_{outbound_route_label}", - start=home, - end=outbound_destination, - ), - CarJourney( - trip=trip.start, - depart=utils.as_date(return_flight["arrive"]), - arrive=utils.as_date(return_flight["arrive"]), - route=f"{return_route_label}_to_{home_route_label}", - start=return_start, - end=home, - ), - ] - - -def country_code(item: StrDict) -> str | None: - """Return a lower-case country code from a YAML item.""" - country = item.get("country") - return country.casefold() if isinstance(country, str) else None - - -def accommodation_route_label(accommodation: StrDict) -> str: - """Return the route label for an accommodation.""" - return accommodation_label(accommodation) - - -def airport_iata(airport: StrDict) -> str: - """Return an airport IATA code.""" - iata = airport.get("iata") - if not isinstance(iata, str): - raise ValueError(f"airport missing IATA code: {airport!r}") - return iata - - -def flight_airport(flight: StrDict, field: str) -> StrDict: - """Return airport detail for a flight field.""" - airport = flight.get(field) - if not isinstance(airport, dict): - raise ValueError(f"flight missing {field}") - return typing.cast(StrDict, airport) - - -def car_journeys_for_fly_drive( - trip: TripLike, - data_dir: Path, - home_label: str = DEFAULT_HOME_LABEL, -) -> list[CarJourney]: - """Build car journeys for home-airport-hotel plus destination rental car legs.""" - flights = trip_flights(trip) - accommodations = sorted_accommodation(trip) - if not accommodations: - raise ValueError(f"trip {trip.start} has no accommodation") - - outbound_flight = flights[0] - return_flight = flights[-1] - outbound_destination_airport = flight_airport(outbound_flight, "to_airport") - return_origin_airport = flight_airport(return_flight, "from_airport") - return_destination_airport = flight_airport(return_flight, "to_airport") - home = latlon_to_lonlat(home_latlon_from_existing_routes(data_dir, home_label)) - home_route_label = route_label(home_label) - - preflight_accommodation = next( - ( - accommodation - for accommodation in accommodations - if country_code(accommodation) == "gb" - and utils.as_date(accommodation["from"]) - <= utils.as_date(outbound_flight["depart"]) - ), - None, - ) - destination_country = country_code(outbound_destination_airport) - rental_accommodations = [ - accommodation - for accommodation in accommodations - if country_code(accommodation) == destination_country - ] - if not rental_accommodations: - raise ValueError("trip has no destination-country accommodation") - - journeys: list[CarJourney] = [] - if preflight_accommodation is not None: - preflight_label = accommodation_route_label(preflight_accommodation) - preflight_coord = latlon_to_lonlat( - accommodation_latlon(preflight_accommodation) - ) - journeys.append( - CarJourney( - trip=trip.start, - depart=utils.as_date(preflight_accommodation["from"]), - arrive=utils.as_date(preflight_accommodation["from"]), - route=f"{home_route_label}_to_{preflight_label}", - start=home, - end=preflight_coord, - ) - ) - - destination_airport_label = route_label(airport_iata(outbound_destination_airport)) - destination_airport_coord = latlon_to_lonlat( - airport_latlon(outbound_destination_airport) - ) - first_accommodation = rental_accommodations[0] - first_label = accommodation_route_label(first_accommodation) - first_coord = latlon_to_lonlat(accommodation_latlon(first_accommodation)) - journeys.append( - CarJourney( - trip=trip.start, - depart=utils.as_date(outbound_flight["arrive"]), - arrive=utils.as_date(outbound_flight["arrive"]), - route=f"{destination_airport_label}_to_{first_label}", - start=destination_airport_coord, - end=first_coord, - ) - ) - - for previous, current in zip(rental_accommodations, rental_accommodations[1:]): - previous_label = accommodation_route_label(previous) - current_label = accommodation_route_label(current) - journeys.append( - CarJourney( - trip=trip.start, - depart=utils.as_date(previous["to"]), - arrive=utils.as_date(previous["to"]), - route=f"{previous_label}_to_{current_label}", - start=latlon_to_lonlat(accommodation_latlon(previous)), - end=latlon_to_lonlat(accommodation_latlon(current)), - ) - ) - - last_accommodation = rental_accommodations[-1] - return_origin_airport_label = route_label(airport_iata(return_origin_airport)) - journeys.append( - CarJourney( - trip=trip.start, - depart=utils.as_date(return_flight["depart"]), - arrive=utils.as_date(return_flight["depart"]), - route=f"{accommodation_route_label(last_accommodation)}_to_{return_origin_airport_label}", - start=latlon_to_lonlat(accommodation_latlon(last_accommodation)), - end=latlon_to_lonlat(airport_latlon(return_origin_airport)), - ) - ) - - return_destination_airport_label = route_label( - airport_iata(return_destination_airport) - ) - journeys.append( - CarJourney( - trip=trip.start, - depart=utils.as_date(return_flight["arrive"]), - arrive=utils.as_date(return_flight["arrive"]), - route=f"{return_destination_airport_label}_to_{home_route_label}", - start=latlon_to_lonlat(airport_latlon(return_destination_airport)), - end=home, - ) - ) - - return journeys - - -def car_journeys_for_trip( - trip: TripLike, - data_dir: Path, - destination: str, - home_label: str = DEFAULT_HOME_LABEL, - destination_label: str | None = None, -) -> list[CarJourney]: - """Build outbound and return car journeys for a trip.""" - if destination == "accommodation": - return car_journeys_for_accommodation( - trip, - data_dir, - home_label=home_label, - destination_label=destination_label, - ) - if destination == "airport": - return car_journeys_for_airport(trip, data_dir, home_label=home_label) - if destination == "ferry": - return car_journeys_for_ferry( - trip, - data_dir, - home_label=home_label, - destination_label=destination_label, - ) - if destination == "fly-drive": - return car_journeys_for_fly_drive(trip, data_dir, home_label=home_label) - raise ValueError(f"unknown car journey destination {destination!r}") - - -def openrouteservice_fetcher(api_key: str) -> RouteFetcher: - """Return a route fetcher backed by openrouteservice.""" - if openrouteservice is None: - raise ValueError("openrouteservice must be installed") - client = openrouteservice.Client(key=api_key) - - def fetch(start: LonLat, end: LonLat) -> GeoJSON: - return typing.cast( - GeoJSON, - client.directions( - [start, end], - profile=DEFAULT_PROFILE, - format="geojson", - instructions=False, - radiuses=[DEFAULT_SNAP_RADIUS_METRES, DEFAULT_SNAP_RADIUS_METRES], - ), - ) - - return fetch - - -def write_route_files( - data_dir: Path, - journeys: list[CarJourney], - fetch_route: RouteFetcher, - overwrite_routes: bool = False, -) -> int: - """Write missing route GeoJSON files and return count written.""" - route_dir = data_dir / "car_routes" - route_dir.mkdir(parents=True, exist_ok=True) - written = 0 - - for journey in journeys: - path = car_route_path(data_dir, journey.route) - if path.exists() and not overwrite_routes: - continue - route_geojson = fetch_route(journey.start, journey.end) - path.write_text(json.dumps(route_geojson, separators=(",", ":")) + "\n") - written += 1 - - return written - - -def journey_key(item: StrDict) -> tuple[date, datetime, datetime, str]: - """Return a stable key for duplicate detection and sorting.""" - route = item.get("route") - if not isinstance(route, str): - raise ValueError(f"car journey route must be a string: {item!r}") - return ( - utils.as_date(item["trip"]), - utils.as_datetime(item["depart"]), - utils.as_datetime(item["arrive"]), - trip_module.route_filename_without_extension(route), - ) - - -def import_car_journeys(data_dir: Path, journeys: list[CarJourney]) -> int: - """Insert car journeys into car_journeys.yaml and return count added.""" - path = data_dir / "car_journeys.yaml" - existing = load_yaml_list(path) - existing_keys = {journey_key(item) for item in existing} - new_items = [ - journey.as_yaml_item() - for journey in journeys - if journey_key(journey.as_yaml_item()) not in existing_keys - ] - if not new_items: - return 0 - - combined = existing + new_items - combined.sort(key=journey_key) - path.write_text(dump_yaml_list_with_blank_lines(combined)) - return len(new_items) - - -def add_car_journeys_for_trip( - trip_date: date, - data_dir: Path = PERSONAL_DATA_DIR, - destination: str = "accommodation", - home_label: str = DEFAULT_HOME_LABEL, - destination_label: str | None = None, - fetch_route: RouteFetcher | None = None, - overwrite_routes: bool = False, - dry_run: bool = False, -) -> tuple[list[CarJourney], int, int]: - """Add car journeys and routes for a trip.""" - trip = trip_for_date(data_dir, trip_date) - journeys = car_journeys_for_trip( - trip, - data_dir, - destination=destination, - home_label=home_label, - destination_label=destination_label, - ) - - if dry_run: - return (journeys, 0, 0) - - if fetch_route is None: - api_key = os.environ.get("ORS_API_KEY") - if not api_key: - raise ValueError("ORS_API_KEY must be set") - fetch_route = openrouteservice_fetcher(api_key) - - routes_written = write_route_files( - data_dir, journeys, fetch_route, overwrite_routes=overwrite_routes - ) - journeys_added = import_car_journeys(data_dir, journeys) - return (journeys, routes_written, journeys_added) - - -def parse_date(value: str) -> date: - """Parse an ISO date.""" - return date.fromisoformat(value) - - -def main(argv: list[str] | None = None) -> int: - """CLI entrypoint.""" - parser = argparse.ArgumentParser( - description=( - "Add car journeys from home to trip accommodation and back, " - "using openrouteservice for GeoJSON routes." - ) - ) - parser.add_argument("trip_date", type=parse_date) - parser.add_argument( - "--destination", - choices=("accommodation", "airport", "ferry", "fly-drive"), - default="accommodation", - help=( - "Route to accommodation check-in/out, the trip airport, " - "around an outbound/return car ferry, or a fly-drive chain." - ), - ) - parser.add_argument("--data-dir", default=str(PERSONAL_DATA_DIR)) - parser.add_argument("--home-label", default=DEFAULT_HOME_LABEL) - parser.add_argument("--destination-label") - parser.add_argument("--overwrite-routes", action="store_true") - parser.add_argument("--dry-run", action="store_true") - args = parser.parse_args(argv) - - journeys, routes_written, journeys_added = add_car_journeys_for_trip( - args.trip_date, - data_dir=Path(args.data_dir).expanduser(), - destination=args.destination, - home_label=args.home_label, - destination_label=args.destination_label, - overwrite_routes=args.overwrite_routes, - dry_run=args.dry_run, - ) - - for journey in journeys: - print(f"{journey.depart}: {journey.route}") - if args.dry_run: - return 0 - print(f"Wrote {routes_written} route file(s)") - print(f"Added {journeys_added} car journey YAML item(s)") - return 0 diff --git a/agenda/carnival.py b/agenda/carnival.py deleted file mode 100644 index 7f6f2f4..0000000 --- a/agenda/carnival.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Calculate the date for carnival.""" - -from datetime import date, timedelta - -from dateutil.easter import easter - -from .event import Event - - -def rio_carnival_events(start_date: date, end_date: date) -> list[Event]: - """List of events for Rio Carnival for each year between start_date and end_date.""" - events = [] - for year in range(start_date.year, end_date.year + 1): - easter_date = easter(year) - carnival_start = easter_date - timedelta(days=51) - carnival_end = easter_date - timedelta(days=46) - - # Only include the carnival if it falls within the specified date range - if ( - start_date <= carnival_start <= end_date - or start_date <= carnival_end <= end_date - ): - events.append( - Event( - name="carnival", - title="Rio Carnival", - date=carnival_start, - end_date=carnival_end, - url="https://en.wikipedia.org/wiki/Rio_Carnival", - ) - ) - - return events diff --git a/agenda/conference.py b/agenda/conference.py index 1d90d96..da697fe 100644 --- a/agenda/conference.py +++ b/agenda/conference.py @@ -2,31 +2,11 @@ import dataclasses import decimal -import os -import typing from datetime import date, datetime import yaml -from . import utils -from .event import Event -from .types import DateOrDateTime, StrDict - -MAX_CONF_DAYS = 20 -DATE_STATUSES = {"exact", "approximate", "tentative"} -DATED_STATUSES = {"exact", "tentative"} - - -class ConferenceSeries(typing.TypedDict, total=False): - """Conference series metadata.""" - - name: str - topic: str - url: str - notes: str - cadence: str - usual_location: str - country: str +from .types import Event @dataclasses.dataclass @@ -36,11 +16,8 @@ class Conference: name: str topic: str location: str - series: str | None = None - start: date | datetime | None = None - end: date | datetime | None = None - trip: date | None = None - country: str | None = None + start: date | datetime + end: date | datetime venue: str | None = None address: str | None = None url: str | None = None @@ -52,19 +29,6 @@ class Conference: online: bool = False price: decimal.Decimal | None = None currency: str | None = None - latitude: float | None = None - longitude: float | None = None - attend_start: date | datetime | None = None - attend_end: date | datetime | None = None - cfp_end: date | None = None - cfp_url: str | None = None - free: bool | None = None - hackathon: bool | None = None - ticket_type: str | None = None - attendees: int | None = None - hashtag: str | None = None - description: str | None = None - dates: StrDict | None = None @property def display_name(self) -> str: @@ -76,157 +40,19 @@ class Conference: ) -def _date_range_label(start: date, end: date) -> str: - """Format conference date range for display.""" - if start == end: - return start.strftime("%a %-d %b %Y") - if start.year == end.year and start.month == end.month: - return f"{start.strftime('%a %-d')}-{end.strftime('%-d %b %Y')}" - return f"{start.strftime('%a %-d %b')}-{end.strftime('%-d %b %Y')}" - - -def _require_date_value(value: typing.Any, field_name: str) -> DateOrDateTime: - """Return date-like field value or raise ValueError.""" - if isinstance(value, (date, datetime)): - return value - raise ValueError(f"conference dates field {field_name!r} must be a date/datetime") - - -def _require_date_only(value: typing.Any, field_name: str) -> date: - """Return field value as a date or raise ValueError.""" - return utils.as_date(_require_date_value(value, field_name)) - - -def conference_date_fields(item: StrDict) -> StrDict: - """Return derived date fields for a conference YAML item.""" - raw_dates = item.get("dates") - if raw_dates is None: - status = typing.cast(str, item.get("date_status", "exact")) - if status not in DATE_STATUSES: - raise ValueError(f"unknown conference date status {status!r}") - start = _require_date_value(item.get("start"), "start") - end = _require_date_value(item.get("end", start), "end") - start_date = utils.as_date(start) - end_date = utils.as_date(end) - return { - "date_status": status, - "start": start, - "end": end, - "start_date": start_date, - "end_date": end_date, - "sort_date": start_date, - "latest_date": end_date, - "display_date": _date_range_label(start_date, end_date), - "has_exact_dates": status == "exact", - } - - if not isinstance(raw_dates, dict): - raise ValueError("conference dates must be a mapping") - - status_value = raw_dates.get("status", "exact") - if not isinstance(status_value, str) or status_value not in DATE_STATUSES: - raise ValueError(f"unknown conference date status {status_value!r}") - - if status_value in DATED_STATUSES: - start = _require_date_value(raw_dates.get("start", item.get("start")), "start") - end = _require_date_value(raw_dates.get("end", item.get("end", start)), "end") - start_date = utils.as_date(start) - end_date = utils.as_date(end) - label = raw_dates.get("label") - display_date = ( - label if isinstance(label, str) else _date_range_label(start_date, end_date) - ) - return { - "date_status": status_value, - "start": start, - "end": end, - "start_date": start_date, - "end_date": end_date, - "sort_date": start_date, - "latest_date": end_date, - "display_date": display_date, - "has_exact_dates": status_value == "exact", - } - - earliest = _require_date_only(raw_dates.get("earliest"), "earliest") - latest = _require_date_only(raw_dates.get("latest"), "latest") - label = raw_dates.get("label") - display_date = ( - label if isinstance(label, str) else _date_range_label(earliest, latest) - ) - return { - "date_status": status_value, - "start_date": earliest, - "end_date": latest, - "sort_date": earliest, - "latest_date": latest, - "display_date": display_date, - "has_exact_dates": False, - } - - -def validate_conference_date_fields(item: StrDict) -> StrDict: - """Validate conference date fields and return derived values.""" - fields = conference_date_fields(item) - if fields["start_date"] > fields["end_date"]: - raise ValueError("conference ends before it starts") - - if fields["date_status"] in DATED_STATUSES: - duration = (fields["end_date"] - fields["start_date"]).days - if duration >= MAX_CONF_DAYS: - raise ValueError( - f"conference is {duration} days; maximum is {MAX_CONF_DAYS - 1}" - ) - return fields - - -def load_series(data_dir: str) -> dict[str, ConferenceSeries]: - """Load conference series metadata.""" - filepath = os.path.join(data_dir, "conference_series.yaml") - if not os.path.exists(filepath): - return {} - - loaded = yaml.safe_load(open(filepath, "r")) - if loaded is None: - return {} - if not isinstance(loaded, dict): - raise ValueError("conference_series.yaml must be a mapping") - return typing.cast(dict[str, ConferenceSeries], loaded) - - def get_list(filepath: str) -> list[Event]: """Read conferences from a YAML file and return a list of Event objects.""" - events: list[Event] = [] - for item in yaml.safe_load(open(filepath, "r")): - try: - fields = validate_conference_date_fields(item) - except ValueError as exc: - raise AssertionError(str(exc)) from exc - normalized_item = dict(item) - if "start" in fields: - normalized_item["start"] = fields["start"] - normalized_item["end"] = fields["end"] - conf = Conference(**normalized_item) - - if fields["has_exact_dates"]: - assert conf.start is not None and conf.end is not None - event = Event( - name="conference", - date=conf.start, - end_date=conf.end, - title=conf.display_name, - url=conf.url, - going=conf.going, - ) - events.append(event) - if not conf.cfp_end: - continue - cfp_end_event = Event( - name="cfp_end", - date=conf.cfp_end, - title="CFP end: " + conf.display_name, - url=conf.cfp_url or conf.url, + return [ + Event( + name="conference", + date=conf.start, + end_date=conf.end, + title=f"🎤 {conf.display_name}", + url=conf.url, + going=conf.going, ) - events.append(cfp_end_event) - - return events + for conf in ( + Conference(**conf) + for conf in yaml.safe_load(open(filepath, "r"))["conferences"] + ) + ] diff --git a/agenda/conference_airports.json b/agenda/conference_airports.json deleted file mode 100644 index ed326a6..0000000 --- a/agenda/conference_airports.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "be:brussels": "BRU", - "de:berlin": "BER", - "de:hamburg": "HAM", - "de:heidelberg": "FRA", - "de:karlsruhe": "STR", - "de:chemnitz": "DRS", - "de:mildenberg": "BER", - "es:barcelona": "BCN", - "it:bologna": [ - "BLQ", - "VRN", - "VCE" - ], - "cy:limassol": "LCA", - "al:albania": "TIA", - "dk:funen": [ - "BLL", - "CPH" - ], - "ca:vancouver": "YVR", - "co:bogotá": "BOG", - "cl:santiago": "SCL", - "us:pasadena, california": "LAX", - "us:petaluma, california": "SFO", - "us:st. louis, mo": "STL", - "us:seattle": "SEA", - "us:doe bay resort & spa, orcas island, wa": "SEA", - "jp:asahikawa, hokkaido": "AKJ", - "se:malmö": [ - "MMX", - "CPH" - ], - "ch:bern": [ - "BRN", - "BSL" - ], - "gb:belfast": [ - "BFS", - "BHD" - ], - "de:bonn": [ - "CGN", - "DUS" - ] -} diff --git a/agenda/conference_detail.py b/agenda/conference_detail.py deleted file mode 100644 index 1215ad0..0000000 --- a/agenda/conference_detail.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Conference identities and airport suggestions, without network lookups.""" - -import json -import math -import re -import unicodedata -from pathlib import Path - -import yaml - -from agenda import airport_lookup -from agenda.types import StrDict - -EUROPE = frozenset( - "al ad at be ba bg hr cy cz dk ee fi fr de gr hu is ie it lv li lt lu mt " - "md mc me nl mk no pl pt ro sm rs sk si es se ch tr ua gb va".split() -) - - -def conference_slug(conf: StrDict) -> str: - """Use a readable title or series and location; the URL also includes a date.""" - title = str(conf.get("series") or conf["name"]) - ascii_title = ( - unicodedata.normalize("NFKD", title).encode("ascii", "ignore").decode() - ) - slug = re.sub(r"[^a-z0-9]+", "-", ascii_title.lower()).strip("-") or "conference" - location = ( - unicodedata.normalize("NFKD", str(conf.get("location", ""))) - .encode("ascii", "ignore") - .decode() - ) - location_slug = re.sub(r"[^a-z0-9]+", "-", location.lower()).strip("-") - return f"{slug}-{location_slug}" if location_slug else slug - - -def distance_km(lat: float, lon: float, other_lat: float, other_lon: float) -> float: - """Great-circle distance.""" - a, b = math.radians(lat), math.radians(other_lat) - dlat, dlon = b - a, math.radians(other_lon - lon) - hav = math.sin(dlat / 2) ** 2 + math.cos(a) * math.cos(b) * math.sin(dlon / 2) ** 2 - return 6371 * 2 * math.asin(math.sqrt(min(1, hav))) - - -def suggest_airport(conf: StrDict, personal_data: str) -> tuple[str | None, str]: - """Prefer explicit mappings, then city matches, then nearby known airports.""" - mapping_path = Path(__file__).with_name("conference_airports.json") - mapping: dict[str, str | list[str]] = json.loads(mapping_path.read_text()) - overrides = Path(personal_data) / "conference_airports.yaml" - if overrides.exists(): - mapping.update(yaml.safe_load(overrides.read_text()) or {}) - location = airport_lookup.normalized(str(conf.get("location", ""))) - country = str(conf.get("country", "")).lower() - mapped = { - airport_lookup.normalized(key): value for key, value in mapping.items() - }.get(f"{country}:{location}") - if mapped: - return ( - ",".join(mapped) if isinstance(mapped, list) else mapped - ), "location mapping" - airports_path = Path(personal_data) / "airports.yaml" - airports: dict[str, StrDict] = ( - yaml.safe_load(airports_path.read_text()) or {} - if airports_path.exists() - else {} - ) - candidates = { - code: airport - for code, airport in airports.items() - if not country or str(airport.get("country", "")).lower() == country - } - for code, airport in candidates.items(): - if location and location == airport_lookup.normalized( - str(airport.get("city", "")) - ): - return code, "airport city matches conference location" - if conf.get("latitude") is not None and conf.get("longitude") is not None: - distances = [ - ( - distance_km( - float(conf["latitude"]), - float(conf["longitude"]), - float(airport["latitude"]), - float(airport["longitude"]), - ), - code, - ) - for code, airport in candidates.items() - if airport.get("latitude") is not None - and airport.get("longitude") is not None - ] - if distances: - distance, code = min(distances) - if distance <= 150: - return ( - code, - f"nearest airport in your data ({distance:.0f} km from venue)", - ) - return None, "No airport mapping yet; enter a destination airport code." - - -def is_short_haul(conf: StrDict) -> bool: - """Treat known European destinations within 3,500 km as short haul.""" - if str(conf.get("country", "")).lower() not in EUROPE: - return False - if conf.get("latitude") is not None and conf.get("longitude") is not None: - return ( - distance_km(51.47, -0.45, float(conf["latitude"]), float(conf["longitude"])) - <= 3500 - ) - return True diff --git a/agenda/conference_flights.py b/agenda/conference_flights.py deleted file mode 100644 index 0d7d687..0000000 --- a/agenda/conference_flights.py +++ /dev/null @@ -1,343 +0,0 @@ -"""On-demand flight searches with shared disk caching and refresh throttling.""" - -import fcntl -import hashlib -import json -import logging -import os -import re -import tempfile -import typing -from datetime import date, datetime, timedelta, timezone -from pathlib import Path - -import flask - -from agenda import airport_lookup, flight_search_cache, google_flights -from agenda.types import StrDict - -LONDON_AIRPORTS = ("LHR", "LGW", "STN", "LTN", "LCY", "SEN") -CACHE_VERSION = 4 -logger = logging.getLogger(__name__) -REFRESH_INTERVAL = timedelta(hours=6) -ERROR_INTERVAL = timedelta(minutes=15) - - -def cache_path( - data_dir: str, start: date, end: date, destination: str, short_haul: bool -) -> Path: - """Cache by all search inputs, independently of conference names.""" - key = json.dumps( - [ - CACHE_VERSION, - start.isoformat(), - end.isoformat(), - destination, - short_haul, - "GBP", - "en-GB", - "GB", - ] - ) - return ( - Path(data_dir) - / "conference-flights" - / (hashlib.sha256(key.encode()).hexdigest() + ".json") - ) - - -def read_cache(path: Path) -> StrDict | None: - """Read cached results; a missing or damaged cache is a cache miss.""" - try: - value = json.loads(path.read_text()) - if not isinstance(value, dict) or not isinstance(value.get("updated_at"), str): - return None - for field in ("updated_at", "attempted_at"): - if field in value and datetime.fromisoformat(value[field]).tzinfo is None: - return None - if not isinstance(value.get("searches"), list): - return None - return typing.cast(StrDict, value) - except (OSError, ValueError, TypeError): - return None - - -def write_cache(path: Path, value: StrDict) -> None: - """Replace JSON atomically so concurrent readers never see partial results.""" - with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as output: - temporary = output.name - json.dump(value, output) - try: - os.replace(temporary, path) - finally: - if os.path.exists(temporary): - os.unlink(temporary) - - -def airport_matches(query: str) -> list[StrDict]: - """Look up locally, preferring airport names in the app's personal data.""" - personal_data = ( - str(flask.current_app.config["PERSONAL_DATA"]) - if flask.has_app_context() - else None - ) - return airport_lookup.matches(query, personal_data) - - -def resolve_airport(query: str) -> tuple[str, str]: - """Resolve a code or the best name match, and expose its name for confirmation.""" - codes = airport_lookup.code_list(query) - if len(codes) > 1: - for code in codes: - if not valid_airport(code): - raise ValueError(f"Unknown IATA airport code {code!r}.") - matches = airport_matches(",".join(codes)) - if not matches: - raise ValueError("Unknown airport in destination group") - return str(matches[0]["code"]), str(matches[0]["name"]) - code = query.strip().upper() - if valid_airport(code): - matches = airport_matches(code) - return code, str(matches[0]["name"]) if matches else code - if len(code) == 3 and code.isascii() and code.isalpha(): - raise ValueError( - f"Unknown IATA airport code {code!r}. Enter a valid code or a longer city/airport name." - ) - matches = airport_matches(query) - if not matches: - raise ValueError( - f"No airport found for {query!r}. Try a city, airport name or IATA code." - ) - return str(matches[0]["code"]), str(matches[0]["name"]) - - -def error_detail(exc: Exception) -> str: - """Describe retry wrappers and underlying causes without losing the useful error.""" - messages: list[str] = [] - current: BaseException | None = exc - seen: set[int] = set() - while current is not None and id(current) not in seen and len(messages) < 3: - seen.add(id(current)) - last_attempt = getattr(current, "last_attempt", None) - if last_attempt is not None: - underlying = last_attempt.exception() - if underlying is not None and id(underlying) not in seen: - current = underlying - continue - raw_message = str(current) - if "Browser logs:" in raw_message: - # Playwright repeats its long command before the useful stderr. - diagnostic = list(dict.fromkeys(re.findall(r"\[err\] (.+)", raw_message))) - if diagnostic: - raw_message = ( - raw_message.split("Browser logs:", 1)[0] - + " " - + " ".join(diagnostic) - ) - message = " ".join(raw_message.split())[:1000] - messages.append( - f"{type(current).__name__}: {message or 'No error message provided'}" - ) - current = current.__cause__ or current.__context__ - return " — ".join(messages) - - -def date_unavailable_message(exc: BaseException) -> str | None: - """Use Google's date-range rejection as the banner through route wrappers.""" - current: BaseException | None = exc - seen: set[int] = set() - while current is not None and id(current) not in seen: - seen.add(id(current)) - if isinstance(current, google_flights.FlightDateUnavailableError): - return str(current) - current = current.__cause__ or current.__context__ - return None - - -def flight_rank(result: StrDict) -> tuple[int, int]: - """Prefer fewer stops, then BA-operated legs, preserving Google's ranking.""" - ba_legs = [ - leg.get("operating_airline_code", leg.get("airline_code")) == "BA" - for leg in result["legs"] - ] - ba_preference = 0 if ba_legs and all(ba_legs) else 1 if any(ba_legs) else 2 - return int(result["stops"]), ba_preference - - -def search_day(origin: str, destination: str, day: date, direct: bool) -> list[StrDict]: - """Reuse a route/date cache before making any paced provider requests.""" - directory = flight_search_cache.search_directory.get() - if directory is None: - raise RuntimeError( - "Flight searches must run inside a cached conference lookup." - ) - return flight_search_cache.cached_day( - directory, - origin, - destination, - day.isoformat(), - direct, - lambda: fetch_day(origin, destination, day, direct, directory), - ) - - -def fetch_day( - origin: str, destination: str, day: date, direct: bool, directory: Path -) -> list[StrDict]: - """Read the browser search page, preferring direct and one-stop flights.""" - max_stops = 0 if direct else 1 - rows = google_flights.search(origin, destination, day, max_stops) - rows = [row for row in rows if row["stops"] <= max_stops] - if not rows and not direct: - rows = google_flights.search(origin, destination, day, 2) - rows = [row for row in rows if row["stops"] <= 2] - rows.sort(key=flight_rank) - return rows[:5] - - -def valid_airport(code: str) -> bool: - """Accept known IATA codes and London's metropolitan code.""" - return code == "LON" or code in airport_lookup.airports() - - -def search_origin( - origin: str, destination: str, start: date, end: date, flexible: bool -) -> StrDict: - """Expand each direction from the closest date, stopping when flights are found.""" - days = range(1, 5) if flexible else range(1, 3) - outbound: list[StrDict] = [] - inbound: list[StrDict] = [] - outbound_dates: list[str] = [] - inbound_dates: list[str] = [] - for offset in days: - departure = start - timedelta(days=offset if flexible else offset + 1) - if not outbound and departure >= date.today(): - outbound_dates.append(departure.isoformat()) - # Only stop expanding when flights arrive before the opening date. - outbound = [ - row - for row in search_day(origin, destination, departure, origin == "BRS") - if date.fromisoformat(row["arrival"][:10]) < start - ] - if not inbound and (flexible or offset == 1): - return_day = end + timedelta(days=offset) - inbound_dates.append(return_day.isoformat()) - inbound = search_day(destination, origin, return_day, origin == "BRS") - if outbound and inbound: - break - return { - "origin": origin, - "outbound": outbound, - "inbound": inbound, - "outbound_dates": outbound_dates, - "inbound_dates": inbound_dates, - } - - -def lookup( - path: Path, start: date, end: date, destination: str, short_haul: bool -) -> StrDict: - """Serialize searches across workers, reuse recent results, and cache errors.""" - path.parent.mkdir(parents=True, exist_ok=True) - # A shared lock limits all conference requests, not just identical searches. - with (path.parent / "search.lock").open( - "a+" - ) as lock, flight_search_cache.use_directory( - path.parent - ), google_flights.use_browser( - path.parent - ): - try: - fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError: - raise ValueError( - "Another flight lookup is running. Please try again shortly." - ) from None - cached = read_cache(path) - now = datetime.now(timezone.utc) - legacy_empty_or_failed = ( - cached - and cached.get("transport") != flight_search_cache.TRANSPORT - and ( - cached.get("error") - or not any( - search.get("outbound") or search.get("inbound") - for search in cached.get("searches", []) - ) - ) - ) - retry_old_parse_failure = ( - cached - and cached.get("error") - and "Unrecognized Google Flights result format" - in cached.get("error_details", "") - and cached.get("parser_version") != google_flights.PARSER_VERSION - ) - needs_earlier_departure = ( - cached - and not cached.get("error") - and any( - search.get("origin") == "LON" - and not search.get("outbound") - and "outbound_dates" not in search - for search in cached.get("searches", []) - ) - ) - if ( - cached - and not needs_earlier_departure - and not retry_old_parse_failure - and not legacy_empty_or_failed - and (not cached.get("error") or cached.get("error_details")) - ): - timestamp = str(cached.get("attempted_at", cached["updated_at"])) - interval = ERROR_INTERVAL if cached.get("error") else REFRESH_INTERVAL - if now - datetime.fromisoformat(timestamp) < interval: - return cached - try: - searches = [] - if short_haul: - bristol = search_origin("BRS", destination, start, end, flexible=True) - searches.append(bristol) - if not bristol["outbound"] or not bristol["inbound"]: - searches.append( - search_origin("LON", destination, start, end, flexible=False) - ) - else: - searches.append( - search_origin("LON", destination, start, end, flexible=False) - ) - value: StrDict = { - "updated_at": now.isoformat(), - "destination": destination, - "searches": searches, - "transport": flight_search_cache.TRANSPORT, - "parser_version": google_flights.PARSER_VERSION, - } - except Exception as exc: - logger.exception( - "Conference flight lookup failed for %s (%s to %s)", - destination, - start, - end, - ) - # Preserve previously successful data and its original timestamp. - value = dict( - cached - or { - "updated_at": now.isoformat(), - "destination": destination, - "searches": [], - } - ) - value.update( - error=flight_search_cache.cooldown_message(path.parent) - or date_unavailable_message(exc) - or "Flight lookup failed. Please try again in 15 minutes.", - error_details=error_detail(exc), - attempted_at=now.isoformat(), - transport=flight_search_cache.TRANSPORT, - parser_version=google_flights.PARSER_VERSION, - ) - write_cache(path, value) - return value diff --git a/agenda/conference_ical.py b/agenda/conference_ical.py deleted file mode 100644 index 4e4624a..0000000 --- a/agenda/conference_ical.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Generate iCalendar feeds for conferences.""" - -import hashlib -from datetime import datetime, timedelta, timezone - -import agenda -import agenda.utils -from agenda import ical -from agenda.types import StrDict - - -def _conference_uid(conf: StrDict) -> str: - """Generate deterministic UID for conference events.""" - start = agenda.utils.as_date(conf["start"]) - raw = f"conference|{start.isoformat()}|{conf.get('name','unknown')}" - digest = hashlib.sha1(raw.encode("utf-8")).hexdigest() - return f"conference-{digest}@agenda-codex" - - -def _conference_location(conf: StrDict) -> str | None: - """Build conference location string.""" - parts: list[str] = [] - venue = conf.get("venue") - location = conf.get("location") - if isinstance(venue, str) and venue.strip(): - parts.append(venue.strip()) - if isinstance(location, str) and location.strip(): - parts.append(location.strip()) - if country_code := conf.get("country"): - country = agenda.get_country(country_code) - if country: - parts.append(country.name) - return ", ".join(parts) if parts else None - - -def _conference_description(conf: StrDict) -> str: - """Build textual description for conferences.""" - lines: list[str] = [] - if topic := conf.get("topic"): - lines.append(f"Topic: {topic}") - if venue := conf.get("venue"): - lines.append(f"Venue: {venue}") - if address := conf.get("address"): - lines.append(f"Address: {address}") - if url := conf.get("url"): - lines.append(f"URL: {url}") - status_bits: list[str] = [] - if conf.get("going"): - status_bits.append("attending") - if conf.get("speaking"): - status_bits.append("speaking") - if status_bits: - lines.append(f"Status: {', '.join(status_bits)}") - return "\n".join(lines) if lines else "Conference" - - -def build_conference_ical(items: list[StrDict]) -> bytes: - """Build iCalendar feed for all conferences.""" - lines = [ - "BEGIN:VCALENDAR", - "VERSION:2.0", - "PRODID:-//Agenda Codex//Conferences//EN", - "CALSCALE:GREGORIAN", - "METHOD:PUBLISH", - "X-WR-CALNAME:Conferences", - ] - generated = datetime.now(tz=timezone.utc) - - for conf in items: - if not conf["has_exact_dates"]: - continue - start_date = agenda.utils.as_date(conf["start"]) - end_date = agenda.utils.as_date(conf["end"]) - end_exclusive = end_date + timedelta(days=1) - - lines.append("BEGIN:VEVENT") - ical.append_property(lines, "UID", _conference_uid(conf)) - ical.append_property(lines, "DTSTAMP", ical.format_datetime_utc(generated)) - ical.append_property(lines, "DTSTART;VALUE=DATE", ical.format_date(start_date)) - ical.append_property(lines, "DTEND;VALUE=DATE", ical.format_date(end_exclusive)) - ical.append_property(lines, "SUMMARY", ical.escape_text(conf["name"])) - description = ical.escape_text(_conference_description(conf)) - ical.append_property(lines, "DESCRIPTION", description) - if location := _conference_location(conf): - ical.append_property(lines, "LOCATION", ical.escape_text(location)) - lines.append("END:VEVENT") - - lines.append("END:VCALENDAR") - ical_text = "\r\n".join(lines) + "\r\n" - return ical_text.encode("utf-8") diff --git a/agenda/conference_list.py b/agenda/conference_list.py deleted file mode 100644 index 3055dbb..0000000 --- a/agenda/conference_list.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Prepare conference lists, country filters, series summaries, and timelines.""" - -import decimal -import hashlib -import os.path -import typing -from collections import defaultdict -from datetime import date, timedelta - -import yaml - -import agenda.conference -import agenda.conference_detail -from agenda.types import StrDict, Trip - - -def build_conference_list(data_dir: str, trips: list[Trip]) -> list[StrDict]: - """Build conference list.""" - filepath = os.path.join(data_dir, "conferences.yaml") - items: list[StrDict] = yaml.safe_load(open(filepath)) - series_lookup = agenda.conference.load_series(data_dir) - conference_trip_lookup = {} - - for trip in trips: - for trip_conf in trip.conferences: - key = (trip_conf["start"], trip_conf["name"]) - conference_trip_lookup[key] = trip - - for conf in items: - conf.update(agenda.conference.validate_conference_date_fields(conf)) - conf["slug"] = agenda.conference_detail.conference_slug(conf) - - price = conf.get("price") - if price: - conf["price"] = decimal.Decimal(price) - - series_id = conf.get("series") - if isinstance(series_id, str): - conf["series_detail"] = series_lookup.get(series_id) - - if "start" in conf: - key = (conf["start"], conf["name"]) - if this_trip := conference_trip_lookup.get(key): - conf["linked_trip"] = this_trip - - identities: defaultdict[tuple[date, str], list[StrDict]] = defaultdict(list) - for conf in items: - identities[(conf["sort_date"], conf["slug"])].append(conf) - for group in identities.values(): - if len(group) > 1: - for conf in group: - identity = str((conf["name"], conf.get("url"))) - conf["slug"] += "-" + hashlib.sha256(identity.encode()).hexdigest()[:8] - - items.sort(key=lambda item: item["sort_date"]) - return items - - -def conference_country_code(conf: StrDict) -> str | None: - """Return normalized alpha-2 country code for a conference.""" - country = conf.get("country") - if not isinstance(country, str): - return None - - code = country.strip().lower() - return code if len(code) == 2 else None - - -def normalize_country_filter(value: str | None) -> str | None: - """Normalize and validate a country query parameter.""" - if not value: - return None - - code = value.strip().lower() - if len(code) != 2: - return None - - return code if agenda.get_country(code) else None - - -def filter_conferences_by_country( - items: list[StrDict], country_code: str | None -) -> list[StrDict]: - """Filter conferences by country when a country code is provided.""" - if not country_code: - return items - - return [item for item in items if conference_country_code(item) == country_code] - - -def conference_country_options(items: list[StrDict]) -> list[StrDict]: - """Return country options for the conference country filter.""" - counts: defaultdict[str, int] = defaultdict(int) - for item in items: - code = conference_country_code(item) - if code and agenda.get_country(code): - counts[code] += 1 - - options: list[StrDict] = [] - for code, count in counts.items(): - country = agenda.get_country(code) - if not country: - continue - options.append( - { - "code": code, - "name": country.name, - "flag": country.flag, - "count": count, - } - ) - - options.sort(key=lambda item: str(item["name"])) - return options - - -def build_conference_series_list( - series_lookup: dict[str, agenda.conference.ConferenceSeries], - conferences: list[StrDict], - today: date, -) -> list[StrDict]: - """Build conference series list with conference counts.""" - - series_items: list[StrDict] = [] - for series_id, series in series_lookup.items(): - linked = [conf for conf in conferences if conf.get("series") == series_id] - latest = max((conf["sort_date"] for conf in linked), default=None) - next_conf = next( - (conf for conf in linked if conf["latest_date"] >= today), None - ) - attended = any(conf.get("going") or conf.get("linked_trip") for conf in linked) - item: StrDict = { - "id": series_id, - **series, - "count": len(linked), - "latest": latest, - "next_conf": next_conf, - "attended": attended, - } - series_items.append(item) - - series_items.sort(key=lambda item: str(item["name"]).lower()) - return series_items - - -def build_conference_timeline( - current: list[StrDict], future: list[StrDict], today: date, days: int = 90 -) -> dict[str, typing.Any] | None: - """Build data for a Gantt-style timeline of upcoming conferences.""" - timeline_start = today - timeline_end = today + timedelta(days=days) - - visible = [ - c - for c in (current + future) - if c["has_exact_dates"] - and c["start_date"] <= timeline_end - and c["end_date"] >= today - ] - if not visible: - return None - - visible.sort(key=lambda c: c["start_date"]) - - # Greedy interval-coloring: assign each conference a lane (row) - lane_ends: list[date] = [] - conf_data = [] - for conf in visible: - lane = next( - (i for i, end in enumerate(lane_ends) if end < conf["start_date"]), - len(lane_ends), - ) - if lane == len(lane_ends): - lane_ends.append(conf["end_date"]) - else: - lane_ends[lane] = conf["end_date"] - - start_off = max((conf["start_date"] - timeline_start).days, 0) - end_off = min((conf["end_date"] - timeline_start).days + 1, days) - left_pct = round(start_off / days * 100, 2) - width_pct = max(round((end_off - start_off) / days * 100, 2), 0.5) - - conf_data.append( - { - "name": conf["name"], - "url": conf.get("url"), - "slug": conf.get("slug"), - "event_date": conf["sort_date"].isoformat(), - "lane": lane, - "left_pct": left_pct, - "width_pct": width_pct, - "key": f"{conf['start_date'].isoformat()}|{conf['name']}", - "label": ( - f"{conf['name']} ({conf['start_date'].strftime('%-d %b')})" - if conf["start_date"] == conf["end_date"] - else ( - f"{conf['name']}" - f" ({conf['start_date'].strftime('%-d %b')}–" - f"{conf['end_date'].strftime('%-d %b')})" - ) - ), - } - ) - - # Month markers for x-axis labels - months = [] - d = today.replace(day=1) - while d <= timeline_end: - off = max((d - timeline_start).days, 0) - months.append( - {"label": d.strftime("%b %Y"), "left_pct": round(off / days * 100, 2)} - ) - # advance to next month - d = (d.replace(day=28) + timedelta(days=4)).replace(day=1) - - return { - "confs": conf_data, - "lane_count": len(lane_ends), - "months": months, - "days": days, - } diff --git a/agenda/conference_page.py b/agenda/conference_page.py deleted file mode 100644 index 64b07bc..0000000 --- a/agenda/conference_page.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Conference detail page and explicit flight lookup action.""" - -import hmac -import secrets -import typing -from datetime import date, datetime, timedelta - -import flask -import werkzeug - -import agenda -import agenda.conference -import agenda.conference_detail as detail -import agenda.conference_flights as flights -import agenda.conference_list -import agenda.flight_search_cache -import agenda.google_flights -import agenda.trip -from agenda.types import StrDict - -blueprint = flask.Blueprint("conference", __name__) - - -@blueprint.app_template_filter("conference_date") -def format_date(value: typing.Any, part: str = "full") -> str: - """Render dates without converting flight times out of the airport's local time.""" - parsed = value - if isinstance(value, str): - try: - parsed = ( - datetime.fromisoformat(value) - if "T" in value or " " in value - else date.fromisoformat(value) - ) - except ValueError: - return value - if isinstance(parsed, datetime): - if part == "time": - return parsed.strftime("%H:%M") - if part == "date": - return parsed.strftime("%a %-d %b %Y") - return parsed.strftime("%a %-d %b %Y at %H:%M") - if isinstance(parsed, date): - return parsed.strftime("%a %-d %b %Y") - return str(value) - - -def searched_dates( - search: StrDict, direction: str, start: date, end: date -) -> list[str]: - """Read checked dates; reconstruct the old policy for legacy cached results.""" - field = direction + "_dates" - if field in search: - return typing.cast(list[str], search[field]) - rows = search.get(direction, []) - if rows: - return sorted({str(row["departure"])[:10] for row in rows}) - offsets = ( - range(1, 5) - if search["origin"] == "BRS" - else (2 if direction == "outbound" else 1,) - ) - return [ - ( - start - timedelta(days=offset) - if direction == "outbound" - else end + timedelta(days=offset) - ).isoformat() - for offset in offsets - if direction != "outbound" or start - timedelta(days=offset) >= date.today() - ] - - -def google_flights_link( - origin: str, destination: str, start: date, end: date, search: StrDict | None = None -) -> StrDict: - """Open a round trip with the displayed route, dates and stop preference.""" - departure = start - timedelta(days=1 if origin == "BRS" else 2) - returning = end + timedelta(days=1) - if search: - for direction in ("outbound", "inbound"): - rows = search.get(direction, []) - checked = search.get(direction + "_dates", []) - selected = ( - str(rows[0]["departure"])[:10] - if rows - else checked[-1] if checked else None - ) - if selected: - if direction == "outbound": - departure = date.fromisoformat(selected) - else: - returning = date.fromisoformat(selected) - max_stops = 0 if origin == "BRS" else 1 - if ( - search - and origin != "BRS" - and any( - row.get("stops", 0) > 1 - for direction in ("outbound", "inbound") - for row in search.get(direction, []) - ) - ): - max_stops = 2 - return { - "url": agenda.google_flights.search_url( - origin, destination, departure, max_stops, returning - ), - "departure": departure, - "return": returning, - } - - -def find_conference(event_date: str, slug: str) -> StrDict: - """Resolve a dated conference identity, including approximate dates.""" - for conf in agenda.conference_list.build_conference_list( - flask.current_app.config["PERSONAL_DATA"], agenda.trip.build_trip_list() - ): - if conf["sort_date"].isoformat() == event_date and conf["slug"] == slug: - return conf - flask.abort(404) - - -@blueprint.route("/conference//", methods=["GET", "POST"]) -def page(event_date: str, slug: str) -> str | werkzeug.Response: - """GET reads only cached data; POST explicitly requests a flight search.""" - conf = find_conference(event_date, slug) - config = flask.current_app.config - suggested, airport_reason = detail.suggest_airport(conf, config["PERSONAL_DATA"]) - parameters = ( - flask.request.form if flask.request.method == "POST" else flask.request.args - ) - default_location = str(conf.get("location") or "") - airport_input = parameters.get("airport", default_location).strip()[:200] - destination = airport_input.upper() - resolved_name = "" - resolution_error = "" - if airport_input: - try: - lookup_query = ( - suggested - if suggested and airport_input == default_location - else airport_input - ) - destination, resolved_name = flights.resolve_airport(lookup_query) - except ValueError as exc: - resolution_error = str(exc) - eligible = ( - conf["date_status"] in agenda.conference.DATED_STATUSES - and conf["start_date"] > date.today() - and not conf.get("online") - and ( - str(conf.get("country", "")).lower() != "gb" - or str(conf.get("location", "")).partition(",")[0].strip().casefold() - == "belfast" - ) - ) - cache = flights.cache_path( - config["DATA_DIR"], - conf["start_date"], - conf["end_date"], - destination, - detail.is_short_haul(conf), - ) - token = flask.session.setdefault( - "conference_flight_token", secrets.token_urlsafe(32) - ) - if flask.request.method == "POST": - if not hmac.compare_digest( - str(token), flask.request.form.get("csrf_token", "") - ): - flask.abort(400) - if not eligible: - flask.abort(400) - if resolution_error or destination in {"BRS", "LON"}: - flask.flash( - resolution_error or "Choose a destination outside Bristol or London.", - "warning", - ) - else: - try: - flights.lookup( - cache, - conf["start_date"], - conf["end_date"], - destination, - detail.is_short_haul(conf), - ) - except ValueError as exc: - flask.flash(str(exc), "warning") - return flask.redirect( - flask.url_for( - "conference.page", - event_date=event_date, - slug=slug, - airport=airport_input if resolution_error else destination, - ) - ) - excluded = { - "name", - "location", - "country", - "venue", - "address", - "latitude", - "longitude", - "url", - "dates", - "date_status", - "start", - "end", - "start_date", - "end_date", - "sort_date", - "latest_date", - "display_date", - "has_exact_dates", - "series", - "series_detail", - "linked_trip", - "slug", - } - fields = [ - (key.replace("_", " ").capitalize(), value) - for key, value in conf.items() - if key not in excluded and value is not None - ] - cached = flights.read_cache(cache) if destination and not resolution_error else None - if cached: - for search in cached.get("searches", []): - for direction in ("outbound", "inbound"): - search[direction + "_dates"] = searched_dates( - search, direction, conf["start_date"], conf["end_date"] - ) - search["google_flights"] = google_flights_link( - search["origin"], - destination, - conf["start_date"], - conf["end_date"], - search, - ) - default_link = ( - google_flights_link( - "BRS" if detail.is_short_haul(conf) else "LON", - destination, - conf["start_date"], - conf["end_date"], - ) - if eligible and not resolution_error and destination - else None - ) - return flask.render_template( - "conference_detail.html", - conf=conf, - fields=fields, - get_country=agenda.get_country, - airport=airport_input, - resolved_airport=destination if not resolution_error else "", - resolved_name=resolved_name, - resolution_error=resolution_error, - airport_reason=airport_reason, - suggested=suggested, - short_haul=detail.is_short_haul(conf), - eligible=eligible, - cached=cached, - default_google_flights=default_link, - csrf_token=token, - cooldown=agenda.flight_search_cache.cooldown_message(cache.parent), - ) - - -@blueprint.get("/conference/airports") -def airport_search() -> flask.Response: - """Autocomplete airport names locally; this endpoint never searches Google.""" - query = flask.request.args.get("q", "").strip()[:200] - if len(query) < 2: - return flask.jsonify([]) - try: - return flask.jsonify(flights.airport_matches(query)) - except ValueError as exc: - return flask.jsonify(error=str(exc)) diff --git a/agenda/data.py b/agenda/data.py index 5517fc3..a508c39 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -1,50 +1,51 @@ """Agenda data.""" import asyncio +import collections +import itertools import os import typing -from datetime import date, datetime, timedelta, timezone -from time import time +from datetime import date, datetime, timedelta import dateutil.rrule import dateutil.tz import flask -import lxml # type: ignore[import-untyped] +import holidays +import isodate # type: ignore +import lxml import pytz +import yaml from . import ( accommodation, birthday, - bristol_waste, - busy, - carnival, + calendar, conference, domains, economist, - events_yaml, - gandi, + fx, gwr, hn, - holidays, meetup, - n_somerset_waste, stock_market, subscription, sun, thespacedevs, travel, uk_holiday, + uk_tz, + waste_schedule, ) -from .event import Event -from .types import StrDict -from .utils import time_function +from .types import Event, Holiday + +StrDict = dict[str, typing.Any] here = dateutil.tz.tzlocal() # deadline to file tax return # credit card expiry dates # morzine ski lifts -# chalet availability calendar +# chalet availablity calendar # starlink visible @@ -61,124 +62,298 @@ def timezone_transition( ] -async def n_somerset_waste_collection_events( - data_dir: str, postcode: str, uprn: str, force_cache: bool = False +def us_holidays(start_date: date, end_date: date) -> list[Holiday]: + """Get US holidays.""" + found: list[Holiday] = [] + for year in range(start_date.year, end_date.year + 1): + hols = holidays.country_holidays("US", years=year, language="en") + found += [ + Holiday(date=hol_date, name=title, country="us") + for hol_date, title in hols.items() + if start_date < hol_date < end_date + ] + + extra = [] + for h in found: + if h.name != "Thanksgiving": + continue + extra += [ + Holiday(date=h.date + timedelta(days=1), name="Black Friday", country="us"), + Holiday(date=h.date + timedelta(days=4), name="Cyber Monday", country="us"), + ] + + return found + extra + + +def get_nyse_holidays( + start_date: date, end_date: date, us_hols: list[Holiday] ) -> list[Event]: + """NYSE holidays.""" + known_us_hols = {(h.date, h.name) for h in us_hols} + found: list[Event] = [] + rename = {"Thanksgiving Day": "Thanksgiving"} + for year in range(start_date.year, end_date.year + 1): + hols = holidays.financial_holidays("NYSE", years=year) + found += [ + Event( + name="holiday", + date=hol_date, + title=rename.get(title, title), + ) + for hol_date, title in hols.items() + if start_date < hol_date < end_date + ] + found = [hol for hol in found if (hol.date, hol.title) not in known_us_hols] + for hol in found: + assert hol.title + hol.title += " (NYSE)" + return found + + +def get_holidays(country: str, start_date: date, end_date: date) -> list[Holiday]: + """Get holidays.""" + found: list[Holiday] = [] + for year in range(start_date.year, end_date.year + 1): + hols = holidays.country_holidays(country.upper(), years=year, language="en_US") + found += [ + Holiday( + date=hol_date, + name=title, + country=country.lower(), + ) + for hol_date, title in hols.items() + if start_date < hol_date < end_date + ] + + return found + + +def midnight(d: date) -> datetime: + """Convert from date to midnight on that day.""" + return datetime.combine(d, datetime.min.time()) + + +def dates_from_rrule( + rrule: str, start: date, end: date +) -> typing.Sequence[datetime | date]: + """Generate events from an RRULE between start_date and end_date.""" + all_day = not any(param in rrule for param in ["BYHOUR", "BYMINUTE", "BYSECOND"]) + + return [ + i.date() if all_day else uk_tz.localize(i) + for i in dateutil.rrule.rrulestr(rrule, dtstart=midnight(start)).between( + midnight(start), midnight(end) + ) + ] + + +async def waste_collection_events(data_dir: str) -> list[Event]: """Waste colllection events.""" - html = await n_somerset_waste.get_html(data_dir, postcode, uprn, force_cache) + postcode = "BS48 3HG" + uprn = "24071046" + + html = await waste_schedule.get_html(data_dir, postcode, uprn) root = lxml.html.fromstring(html) - events = n_somerset_waste.parse(root) - return typing.cast(list[Event], events) + events = waste_schedule.parse(root) + return events async def bristol_waste_collection_events( - data_dir: str, start_date: date, uprn: str, force_cache: bool = False + data_dir: str, start_date: date ) -> list[Event]: """Waste colllection events.""" - cache = "force" if force_cache else "recent" - return typing.cast( - list[Event], await bristol_waste.get(start_date, data_dir, uprn, cache) - ) + uprn = "358335" + + return await waste_schedule.get_bristol_gov_uk(start_date, data_dir, uprn) -def find_events_during_stay( - accommodation_events: list[Event], markets: list[Event] -) -> list[Event]: - """Market events that happen during accommodation stays.""" - overlapping_markets = [] - for market in markets: - market_date = market.as_date - assert isinstance(market_date, date) - for e in accommodation_events: - start, end = e.as_date, e.end_as_date - assert start and end and all(isinstance(i, date) for i in (start, end)) - # Check if the market date is within the accommodation dates. - if start <= market_date <= end: - overlapping_markets.append(market) - break # Breaks the inner loop if overlap is found. - return overlapping_markets +def combine_holidays(holidays: list[Holiday]) -> list[Event]: + """Combine UK and US holidays with the same date and title.""" + all_countries = {h.country for h in holidays} -def hide_markets_while_away( - events: list[Event], accommodation_events: list[Event] -) -> None: - """Hide markets that happen while away.""" - optional = [ - e - for e in events - if e.name == "market" or (e.title and "LHG Run Club" in e.title) - ] - going = [e for e in events if e.going] + standard_name = { + (1, 1): "New Year's Day", + (1, 6): "Epiphany", + (5, 1): "Labour Day", + (8, 15): "Assumption Day", + (12, 8): "Immaculate conception", + (12, 25): "Christmas Day", + (12, 26): "Boxing Day", + } - overlapping_markets = find_events_during_stay( - accommodation_events + going, optional - ) - for market in overlapping_markets: - events.remove(market) + combined: collections.defaultdict[ + tuple[date, str], set[str] + ] = collections.defaultdict(set) + for h in holidays: + assert isinstance(h.name, str) and isinstance(h.date, date) -class AgendaData(typing.TypedDict, total=False): - """Agenda Data.""" + event_key = (h.date, standard_name.get((h.date.month, h.date.day), h.name)) + combined[event_key].add(h.country) - now: datetime - stock_markets: list[str] - rockets: list[thespacedevs.Summary] - gwr_advance_tickets: date | None - data_gather_seconds: float - stock_market_times_seconds: float - timings: list[tuple[str, float]] - events: list[Event] - accommodation_events: list[Event] - gaps: list[StrDict] - sunrise: datetime - sunset: datetime - last_week: date - two_weeks_ago: date - errors: list[tuple[str, Exception]] - - -def rocket_launch_events(rockets: list[thespacedevs.Summary]) -> list[Event]: - """Rocket launch events.""" events: list[Event] = [] - for launch in rockets: - dt = None - - net_precision = launch["net_precision"] - skip = {"Year", "Month", "Quarter", "Fiscal Year"} - if net_precision == "Day": - dt = datetime.strptime(launch["net"], "%Y-%m-%dT%H:%M:%SZ").date() - elif ( - net_precision - and net_precision not in skip - and "Year" not in net_precision - and launch["t0_time"] - ): - dt = pytz.utc.localize( - datetime.strptime(launch["net"], "%Y-%m-%dT%H:%M:%SZ") + for (d, name), countries in combined.items(): + if len(countries) == len(all_countries): + country_list = "" + elif len(countries) < len(all_countries) / 2: + country_list = ", ".join(sorted(country.upper() for country in countries)) + else: + country_list = "not " + ", ".join( + sorted(country.upper() for country in all_countries - set(countries)) ) - if not dt: - continue - - rocket_name = ( - f'{launch["rocket"]["full_name"]}: ' - + f'{launch["mission_name"] or "[no mission]"}' + e = Event( + name="holiday", + date=d, + title=f"{name} ({country_list})" if country_list else name, ) - e = Event(name="rocket", date=dt, title=rocket_name) events.append(e) return events -def event_sort_datetime(event: Event) -> datetime: - """Return a timezone-normalized datetime suitable for sorting events.""" - dt = typing.cast(datetime, event.as_datetime) - if dt.tzinfo is None or dt.utcoffset() is None: - return dt - return dt.astimezone(timezone.utc).replace(tzinfo=None) +def get_yaml_event_date_field(item: dict[str, str]) -> str: + """Event date field name.""" + return ( + "end_date" + if item["name"] == "travel_insurance" + else ("start_date" if "start_date" in item else "date") + ) -async def get_data(now: datetime, config: flask.config.Config) -> AgendaData: +def get_yaml_event_end_date_field(item: dict[str, str]) -> str: + """Event date field name.""" + return ( + "end_date" + if item["name"] == "travel_insurance" + else ("start_date" if "start_date" in item else "date") + ) + + +def read_events_yaml(data_dir: str, start: date, end: date) -> list[Event]: + """Read eventes from YAML file.""" + events: list[Event] = [] + for item in yaml.safe_load(open(os.path.join(data_dir, "events.yaml"))): + duration = ( + isodate.parse_duration(item["duration"]) if "duration" in item else None + ) + dates = ( + dates_from_rrule(item["rrule"], start, end) + if "rrule" in item + else [item[get_yaml_event_date_field(item)]] + ) + for dt in dates: + e = Event( + name=item["name"], + date=dt, + end_date=( + dt + duration + if duration + else ( + item.get("end_date") + if item["name"] != "travel_insurance" + else None + ) + ), + title=item.get("title"), + url=item.get("url"), + ) + events.append(e) + return events + + +def find_markets_during_stay( + accommodation_events: list[Event], markets: list[Event] +) -> list[Event]: + """Market events that happen during accommodation stays.""" + overlapping_markets = [] + for market in markets: + for e in accommodation_events: + # Check if the market date is within the accommodation dates. + if e.as_date <= market.as_date <= e.end_as_date: + overlapping_markets.append(market) + break # Breaks the inner loop if overlap is found. + return overlapping_markets + + +def find_gaps(events: list[Event], min_gap_days: int = 3) -> list[StrDict]: + """Gaps of at least `min_gap_days` between events in a list of events.""" + # Sort events by start date + + gaps: list[tuple[date, date]] = [] + previous_event_end = None + + by_start_date = { + d: list(on_day) + for d, on_day in itertools.groupby(events, key=lambda e: e.as_date) + } + + by_end_date = { + d: list(on_day) + for d, on_day in itertools.groupby(events, key=lambda e: e.end_as_date) + } + + for event in events: + # Use start date for current event + start_date = event.as_date + + # If previous event exists, calculate the gap + if previous_event_end: + gap_days = (start_date - previous_event_end).days + if gap_days >= (min_gap_days + 2): + start_end = ( + previous_event_end + timedelta(days=1), + start_date - timedelta(days=1), + ) + gaps.append(start_end) + + # Update previous event end date + end = event.end_as_date + if not previous_event_end or end > previous_event_end: + previous_event_end = end + + return [ + { + "start": gap_start, + "end": gap_end, + "after": by_start_date[gap_end + timedelta(days=1)], + "before": by_end_date[gap_start - timedelta(days=1)], + } + for gap_start, gap_end in gaps + ] + + +def busy_event(e: Event) -> bool: + """Busy.""" + if e.name not in { + "event", + "accommodation", + "conference", + "dodainville", + "transport", + "meetup", + "party", + }: + return False + + if e.title in ("IA UK board meeting", "Mill Road Winter Fair"): + return False + + if e.name == "conference" and not e.going: + return False + if not e.title: + return True + if e.title == "LHG Run Club" or "Third Thursday Social" in e.title: + return False + + lc_title = e.title.lower() + return "rebels" not in lc_title and "south west data social" not in lc_title + + +async def get_data( + now: datetime, config: flask.config.Config +) -> typing.Mapping[str, str | object]: """Get data to display on agenda dashboard.""" data_dir = config["DATA_DIR"] @@ -192,51 +367,28 @@ async def get_data(now: datetime, config: flask.config.Config) -> AgendaData: minus_365 = now - timedelta(days=365) plus_365 = now + timedelta(days=365) - t0 = time() - offline_mode = bool(config.get("OFFLINE_MODE")) - result_list = await asyncio.gather( - time_function( - "gwr_advance_tickets", gwr.advance_ticket_date, data_dir, offline_mode - ), - time_function( - "backwell_bins", - n_somerset_waste_collection_events, - data_dir, - config["BACKWELL_POSTCODE"], - config["BACKWELL_UPRN"], - offline_mode, - ), - time_function( - "bristol_bins", - bristol_waste_collection_events, - data_dir, - today, - config["BRISTOL_UPRN"], - offline_mode, - ), + ( + gbpusd, + gwr_advance_tickets, + bank_holiday, + rockets, + backwell_bins, + bristol_bins, + ) = await asyncio.gather( + fx.get_gbpusd(config), + gwr.advance_ticket_date(data_dir), + uk_holiday.bank_holiday_list(last_year, next_year, data_dir), + thespacedevs.get_launches(rocket_dir, limit=40), + waste_collection_events(data_dir), + bristol_waste_collection_events(data_dir, today), ) - rockets = thespacedevs.read_cached_launches(rocket_dir) - results = {call[0]: call[1] for call in result_list} - - errors = [(call[0], call[3]) for call in result_list if call[3]] - - gwr_advance_tickets = results["gwr_advance_tickets"] - - data_gather_seconds = time() - t0 - t0 = time() - - stock_market_times = stock_market.open_and_close() - stock_market_times_seconds = time() - t0 - - reply: AgendaData = { + reply: dict[str, typing.Any] = { "now": now, - "stock_markets": stock_market_times, + "gbpusd": gbpusd, + "stock_markets": stock_market.open_and_close(), "rockets": rockets, "gwr_advance_tickets": gwr_advance_tickets, - "data_gather_seconds": data_gather_seconds, - "stock_market_times_seconds": stock_market_times_seconds, - "timings": [(call[0], call[2]) for call in result_list], } my_data = config["PERSONAL_DATA"] @@ -253,44 +405,85 @@ async def get_data(now: datetime, config: flask.config.Config) -> AgendaData: if gwr_advance_tickets: events.append(Event(name="gwr_advance_tickets", date=gwr_advance_tickets)) - us_hols = holidays.us_holidays(last_year, next_year) - events += holidays.get_nyse_holidays(last_year, next_year, us_hols) + us_hols = us_holidays(last_year, next_year) + + holidays: list[Holiday] = bank_holiday + us_hols + for country in ( + "at", + "be", + "br", + "ch", + "cz", + "de", + "dk", + "ee", + "es", + "fi", + "fr", + "gr", + "it", + "ke", + "nl", + "pl", + ): + holidays += get_holidays(country, last_year, next_year) + + events += get_nyse_holidays(last_year, next_year, us_hols) accommodation_events = accommodation.get_events( os.path.join(my_data, "accommodation.yaml") ) - holiday_list = holidays.get_all(last_year, next_year, data_dir) - events += holidays.combine_holidays(holiday_list) - events += holidays.get_school_holidays(last_year, next_year, data_dir) - if flask.g.user.is_authenticated: - events += birthday.get_birthdays( - last_year, os.path.join(my_data, "entities.yaml") - ) - events += domains.renewal_dates(my_data) + events += combine_holidays(holidays) + events += birthday.get_birthdays(last_year, os.path.join(my_data, "entities.yaml")) events += accommodation_events events += travel.all_events(my_data) events += conference.get_list(os.path.join(my_data, "conferences.yaml")) - for key in "backwell_bins", "bristol_bins": - if results[key]: - events += results[key] - events += events_yaml.read(my_data, last_year, next_year) + events += backwell_bins + bristol_bins + events += read_events_yaml(my_data, last_year, next_year) events += subscription.get_events(os.path.join(my_data, "subscriptions.yaml")) - events += gandi.get_events(data_dir) events += economist.publication_dates(last_week, next_year) events += meetup.get_events(my_data) events += hn.whoishiring(last_year, next_year) - events += carnival.rio_carnival_events(last_year, next_year) - events += rocket_launch_events(rockets) + + events += domains.renewal_dates(my_data) + + # hide markets that happen while away + markets = [e for e in events if e.name == "market"] + going = [e for e in events if e.going] + + overlapping_markets = find_markets_during_stay( + accommodation_events + going, markets + ) + for market in overlapping_markets: + events.remove(market) + + for launch in rockets: + dt = None + + if launch["net_precision"] == "Day": + dt = datetime.strptime(launch["net"], "%Y-%m-%dT00:00:00Z").date() + elif launch["t0_time"]: + dt = pytz.utc.localize( + datetime.strptime(launch["net"], "%Y-%m-%dT%H:%M:%SZ") + ) + + if not dt: + continue + + rocket_name = f'🚀{launch["rocket"]}: {launch["mission_name"] or "[no mission]"}' + e = Event(name="rocket", date=dt, title=rocket_name) + events.append(e) + events += [Event(name="today", date=today)] busy_events = [ e for e in sorted(events, key=lambda e: e.as_date) - if e.as_date > today and e.as_date < next_year and busy.busy_event(e) + if e.as_date > today and e.as_date < next_year and busy_event(e) ] - gaps = busy.find_gaps(busy_events) + gaps = find_gaps(busy_events) events += [ Event(name="gap", date=gap["start"], end_date=gap["end"]) for gap in gaps @@ -300,7 +493,7 @@ async def get_data(now: datetime, config: flask.config.Config) -> AgendaData: # at the top of the list for today. This is achieved by sorting first by # the datetime attribute, and then ensuring that events with the name # "today" are ordered before others on the same date. - events.sort(key=lambda e: (event_sort_datetime(e), e.name != "today")) + events.sort(key=lambda e: (e.as_datetime, e.name != "today")) reply["gaps"] = gaps @@ -308,10 +501,9 @@ async def get_data(now: datetime, config: flask.config.Config) -> AgendaData: reply["sunrise"] = sun.sunrise(observer) reply["sunset"] = sun.sunset(observer) reply["events"] = events - reply["accommodation_events"] = accommodation_events reply["last_week"] = last_week reply["two_weeks_ago"] = two_weeks_ago - reply["errors"] = errors + reply["fullcalendar_events"] = calendar.build_events(events) return reply diff --git a/agenda/domains.py b/agenda/domains.py index 5b03bff..99c614e 100644 --- a/agenda/domains.py +++ b/agenda/domains.py @@ -1,10 +1,10 @@ -"""Domain renewal dates.""" +"""Accomodation.""" import csv import os from datetime import datetime -from .event import Event +from .types import Event url = "https://admin.gandi.net/domain/01578ef0-a84b-11e7-bdf3-00163e6dc886/" diff --git a/agenda/economist.py b/agenda/economist.py index 21a8199..4438b98 100644 --- a/agenda/economist.py +++ b/agenda/economist.py @@ -5,7 +5,7 @@ from datetime import date, time, timedelta from dateutil.relativedelta import TH, relativedelta from . import uk_time -from .event import Event +from .types import Event def publication_dates(start_date: date, end_date: date) -> list[Event]: diff --git a/agenda/event.py b/agenda/event.py deleted file mode 100644 index b975625..0000000 --- a/agenda/event.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Types.""" - -import datetime -from dataclasses import dataclass - -from . import utils -from .types import DateOrDateTime, StrDict - -emojis = { - "market": "🧺", - "us_presidential_election": "🗳️🇺🇸", - "bus_route_closure": "🚌❌", - "meetup": "👥", - "dinner": "🍷", - "party": "🍷", - "ba_voucher": "✈️", - "accommodation": "🏨", # alternative: 🧳 - "flight": "✈️", - "conference": "🎤", - "rocket": "🚀", - "birthday": "🎈", - "waste_schedule": "🗑️", - "economist": "📰", - "running": "🏃", - "critical_mass": "🚴", - "trip": "🧳", - "hackathon": "💻", -} - - -@dataclass -class Event: - """Event.""" - - name: str - date: DateOrDateTime - end_date: DateOrDateTime | None = None - title: str | None = None - url: str | None = None - going: bool | None = None - - @property - def as_datetime(self) -> datetime.datetime: - """Date/time of event.""" - return utils.as_datetime(self.date) - - @property - def has_time(self) -> bool: - """Event has a time associated with it.""" - return isinstance(self.date, datetime.datetime) - - @property - def as_date(self) -> datetime.date: - """Date of event.""" - return ( - self.date.date() if isinstance(self.date, datetime.datetime) else self.date - ) - - @property - def end_as_date(self) -> datetime.date: - """Date of event.""" - return ( - ( - self.end_date.date() - if isinstance(self.end_date, datetime.datetime) - else self.end_date - ) - if self.end_date - else self.as_date - ) - - @property - def display_time(self) -> str | None: - """Time for display on web page.""" - return ( - self.date.strftime("%H:%M") - if isinstance(self.date, datetime.datetime) - else None - ) - - @property - def display_timezone(self) -> str | None: - """Timezone for display on web page.""" - return ( - self.date.strftime("%z") - if isinstance(self.date, datetime.datetime) - else None - ) - - def display_duration(self) -> str | None: - """Duration for display.""" - if self.end_as_date != self.as_date or not self.has_time: - return None - - assert isinstance(self.date, datetime.datetime) - assert isinstance(self.end_date, datetime.datetime) - - secs: int = int((self.end_date - self.date).total_seconds()) - - hours: int = secs // 3600 - mins: int = (secs % 3600) // 60 - - if mins == 0: - return f"{hours:d}h" - if hours == 0: - return f"{mins:d} mins" - - return f"{hours:d}h {mins:02d} mins" - - def delta_days(self, today: datetime.date) -> str: - """Return number of days from today as a string.""" - delta = (self.as_date - today).days - - match delta: - case 0: - return "today" - case 1: - return "1 day" - case _: - return f"{delta:,d} days" - - @property - def display_date(self) -> str: - """Date for display on web page.""" - if isinstance(self.date, datetime.datetime): - return self.date.strftime("%a, %d, %b %Y %H:%M %z") - else: - return self.date.strftime("%a, %d, %b %Y") - - @property - def display_title(self) -> str: - """Name for display.""" - return self.title or self.name - - @property - def emoji(self) -> str | None: - """Emoji.""" - if self.title == "LHG Run Club": - return "🏃🍻" - return emojis.get(self.name) - - @property - def title_with_emoji(self) -> str | None: - """Title with optional emoji at the start.""" - title = self.title or self.name - if title is None: - return None - emoji = self.emoji - return f"{emoji} {title}" if emoji else title diff --git a/agenda/events_yaml.py b/agenda/events_yaml.py deleted file mode 100644 index 8678d64..0000000 --- a/agenda/events_yaml.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Read events from YAML.""" - -import os -import typing -from datetime import date, datetime - -import dateutil.rrule -import isodate # type: ignore -import yaml - -from . import uk_tz -from .event import Event - - -def midnight(d: date) -> datetime: - """Convert from date to midnight on that day.""" - return datetime.combine(d, datetime.min.time()) - - -def dates_from_rrule( - rrule: str, start: date, end: date -) -> typing.Sequence[datetime | date]: - """Generate events from an RRULE between start_date and end_date.""" - all_day = not any(param in rrule for param in ["BYHOUR", "BYMINUTE", "BYSECOND"]) - - return [ - i.date() if all_day else uk_tz.localize(i) - for i in dateutil.rrule.rrulestr(rrule, dtstart=midnight(start)).between( - midnight(start), midnight(end) - ) - ] - - -def get_yaml_event_date_field(item: dict[str, str]) -> str: - """Event date field name.""" - return ( - "end_date" - if item["name"] == "travel_insurance" - else ("start_date" if "start_date" in item else "date") - ) - - -def get_yaml_event_end_date_field(item: dict[str, str]) -> str: - """Event date field name.""" - return ( - "end_date" - if item["name"] == "travel_insurance" - else ("start_date" if "start_date" in item else "date") - ) - - -def read( - data_dir: str, start: date, end: date, skip_trips: bool = False -) -> list[Event]: - """Read eventes from YAML file.""" - events: list[Event] = [] - for item in yaml.safe_load(open(os.path.join(data_dir, "events.yaml"))): - if "trip" in item and skip_trips: - continue - duration = ( - isodate.parse_duration(item["duration"]) if "duration" in item else None - ) - dates = ( - dates_from_rrule(item["rrule"], start, end) - if "rrule" in item - else [item[get_yaml_event_date_field(item)]] - ) - for dt in dates: - e = Event( - name=item["name"], - date=dt, - end_date=( - dt + duration - if duration - else ( - item.get("end_date") - if item["name"] != "travel_insurance" - else None - ) - ), - title=item.get("title"), - url=item.get("url"), - ) - events.append(e) - return events diff --git a/agenda/flight_diagnostics.py b/agenda/flight_diagnostics.py deleted file mode 100644 index d9fc059..0000000 --- a/agenda/flight_diagnostics.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Bounded, best-effort diagnostic snapshots for failed flight searches.""" - -import gzip -import json -import logging -import os -import tempfile -import time -from datetime import datetime, timezone -from pathlib import Path - -from agenda.types import StrDict - -MAX_RECORDS = 100 -RETENTION_SECONDS = 30 * 24 * 60 * 60 -logger = logging.getLogger(__name__) - - -def write_failure(directory: Path, details: StrDict) -> Path | None: - """Save one compressed JSON snapshot; logging failures never break lookups.""" - temporary: str | None = None - try: - folder = directory / "errors" - folder.mkdir(parents=True, exist_ok=True) - truncated: list[str] = [] - - def limit(value: str, size: int, field: str) -> str: - data = value.encode("utf-8") - if len(data) > size: - truncated.append(field) - return data[:size].decode("utf-8", errors="ignore") - return value - - snapshot = dict(details) - snapshot["format_version"] = 1 - for field, size in ( - ("page_html", 2 * 1024 * 1024), - ("flight_script", 1024 * 1024), - ("page_text", 64 * 1024), - ): - if isinstance(snapshot.get(field), str): - snapshot[field] = limit(snapshot[field], size, field) - responses = [] - for index, response in enumerate(snapshot.get("http_errors", [])[:5]): - item = dict(response) - if isinstance(item.get("body"), str): - item["body"] = limit( - item["body"], 256 * 1024, f"http_errors[{index}].body" - ) - responses.append(item) - snapshot["http_errors"] = responses - snapshot["truncated_fields"] = truncated - snapshot["recorded_at"] = datetime.now(timezone.utc).isoformat() - stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") - with tempfile.NamedTemporaryFile( - dir=folder, prefix=".snapshot-", delete=False - ) as temporary_output: - temporary = temporary_output.name - path = ( - folder - / f"{stamp}-{Path(temporary).name.removeprefix('.snapshot-')}.json.gz" - ) - with gzip.open(temporary, "wt", encoding="utf-8") as compressed_output: - json.dump(snapshot, compressed_output, ensure_ascii=False) - os.chmod(temporary, 0o640) - os.replace(temporary, path) - temporary = None - records = sorted( - folder.glob("*.json.gz"), - key=lambda record: record.stat().st_mtime, - reverse=True, - ) - cutoff = time.time() - RETENTION_SECONDS - for index, record in enumerate(records): - if index >= MAX_RECORDS or record.stat().st_mtime < cutoff: - record.unlink() - logger.info("Saved flight failure diagnostics to %s", path) - return path - except Exception: - logger.warning("Unable to save flight failure diagnostics", exc_info=True) - return None - finally: - if temporary is not None: - try: - Path(temporary).unlink(missing_ok=True) - except OSError: - pass diff --git a/agenda/flight_search_cache.py b/agenda/flight_search_cache.py deleted file mode 100644 index 1b64d12..0000000 --- a/agenda/flight_search_cache.py +++ /dev/null @@ -1,141 +0,0 @@ -"""Shared pacing, HTTP 429 cooldowns and caches for individual flight searches.""" - -import contextvars -import fcntl -import hashlib -import json -import time -import typing -from contextlib import contextmanager -from datetime import datetime, timezone -from pathlib import Path - -from agenda.types import StrDict - -REQUEST_INTERVAL = 3.0 -RATE_LIMIT_COOLDOWN = 30 * 60 -DAY_CACHE_SECONDS = 6 * 60 * 60 -DAY_CACHE_VERSION = 1 -TRANSPORT = "playwright-chromium" -search_directory: contextvars.ContextVar[Path | None] = contextvars.ContextVar( - "conference_flight_search_directory", default=None -) - - -class RateLimitCooldownError(RuntimeError): - """Google flight requests are temporarily paused across all workers.""" - - -@contextmanager -def use_directory(directory: Path) -> typing.Iterator[None]: - """Share a lookup's configured cache directory with its individual searches.""" - token = search_directory.set(directory) - try: - yield - finally: - search_directory.reset(token) - - -def read_state(path: Path) -> StrDict: - """Read shared JSON state, treating damaged files as a cache miss.""" - try: - state = json.loads(path.read_text()) - return typing.cast(StrDict, state) if isinstance(state, dict) else {} - except (OSError, ValueError): - return {} - - -def cooldown_message(directory: Path) -> str | None: - """Describe an active shared cooldown without making any requests.""" - state = read_state(directory / "rate-limit.json") - until = state.get("cooldown_until") - if not isinstance(until, (float, int)) or until <= time.time(): - return None - when = datetime.fromtimestamp(until, timezone.utc).strftime( - "%a %-d %b at %H:%M UTC" - ) - return f"Google returned HTTP 429. All flight searches are paused until {when}. Cached results remain available." - - -def write_state(path: Path, state: StrDict) -> None: - """Use the agenda's atomic JSON writer for state and day caches.""" - # Imported lazily to avoid a module cycle. - from agenda.conference_flights import write_cache - - write_cache(path, state) - - -def wait_for_request(directory: Path) -> None: - """Space browser searches across processes.""" - directory.mkdir(parents=True, exist_ok=True) - with (directory / "request.lock").open("a+") as lock: - fcntl.flock(lock, fcntl.LOCK_EX) - message = cooldown_message(directory) - if message: - raise RateLimitCooldownError(message) - state_path = directory / "rate-limit.json" - state = read_state(state_path) - last = state.get("last_request_at", 0) - last = float(last) if isinstance(last, (float, int)) else 0.0 - delay = min(REQUEST_INTERVAL, max(0.0, last + REQUEST_INTERVAL - time.time())) - if delay: - time.sleep(delay) - state["last_request_at"] = time.time() - write_state(state_path, state) - - -def block_requests(directory: Path) -> None: - """Set a shared cooldown immediately after the first HTTP 429.""" - directory.mkdir(parents=True, exist_ok=True) - with (directory / "request.lock").open("a+") as lock: - fcntl.flock(lock, fcntl.LOCK_EX) - path = directory / "rate-limit.json" - state = read_state(path) - state["cooldown_until"] = time.time() + RATE_LIMIT_COOLDOWN - state["transport"] = TRANSPORT - write_state(path, state) - - -def cached_day( - directory: Path, - origin: str, - destination: str, - day: str, - direct: bool, - fetch: typing.Callable[[], list[StrDict]], -) -> list[StrDict]: - """Reuse a route/date's results across conferences, including empty results.""" - key = json.dumps( - [ - DAY_CACHE_VERSION, - origin, - destination, - day, - direct, - "BEST", - "BA", - "GBP", - "en-GB", - "GB", - ] - ) - folder = directory / "days" - folder.mkdir(parents=True, exist_ok=True) - path = folder / (hashlib.sha256(key.encode()).hexdigest() + ".json") - # Recheck under the lock so concurrent lookups cannot duplicate a fetch. - with path.with_suffix(".lock").open("a+") as lock: - fcntl.flock(lock, fcntl.LOCK_EX) - state = read_state(path) - timestamp = state.get("fetched_at") - if ( - isinstance(timestamp, (float, int)) - and time.time() - timestamp < DAY_CACHE_SECONDS - and isinstance(state.get("flights"), list) - and (state["flights"] or state.get("transport") == TRANSPORT) - ): - return typing.cast(list[StrDict], state["flights"]) - rows = fetch() - write_state( - path, {"fetched_at": time.time(), "flights": rows, "transport": TRANSPORT} - ) - return rows diff --git a/agenda/fx.py b/agenda/fx.py index 28f7e26..1d48e49 100644 --- a/agenda/fx.py +++ b/agenda/fx.py @@ -9,10 +9,6 @@ from decimal import Decimal import flask import httpx -DEFAULT_FX_CACHE_TTL_HOURS = 24 -DEFAULT_FX_FAILURE_RETRY_HOURS = 24 -FRANKFURTER_CACHE_PREFIX = "frankfurter" - async def get_gbpusd(config: flask.config.Config) -> Decimal: """Get the current value for GBPUSD, with caching.""" @@ -46,243 +42,3 @@ async def get_gbpusd(config: flask.config.Config) -> Decimal: data = json.loads(r.text, parse_float=Decimal) return typing.cast(Decimal, 1 / data["quotes"]["USDGBP"]) - - -def read_cached_rates( - filename: str | None, currencies: list[str] -) -> dict[str, Decimal]: - """Read FX rates from cache.""" - if filename is None: - return {} - - with open(filename) as file: - data = json.load(file, parse_float=Decimal) - - frankfurter_rates = _frankfurter_rates(data, currencies) - if frankfurter_rates: - return frankfurter_rates - - if isinstance(data, list): - return {} - - if not isinstance(data, dict): - return {} - - rates = data.get("rates") - if isinstance(rates, dict): - return {cur: Decimal(rates[cur]) for cur in currencies if cur in rates} - - quotes = data.get("quotes") - if isinstance(quotes, dict): - return { - cur: Decimal(quotes[f"GBP{cur}"]) - for cur in currencies - if f"GBP{cur}" in quotes - } - - return {} - - -def _frankfurter_rates(data: typing.Any, currencies: list[str]) -> dict[str, Decimal]: - """Extract rates from Frankfurter's response format.""" - if not isinstance(data, list): - return {} - - rates: dict[str, Decimal] = {} - for item in data: - if not isinstance(item, dict): - continue - - quote = item.get("quote") - rate = item.get("rate") - if isinstance(quote, str) and quote in currencies and rate is not None: - rates[quote] = Decimal(rate) - - return rates - - -def _fx_cache_datetime(filename: str) -> datetime: - """Extract the cache timestamp from an FX cache filename.""" - return datetime.strptime(filename[:16], "%Y-%m-%d_%H:%M") - - -def _has_required_quotes(filename: str, currencies: list[str]) -> bool: - """Return true if the cache file contains the requested GBP quotes.""" - try: - return len(read_cached_rates(filename, currencies)) == len(currencies) - except (OSError, json.JSONDecodeError): - return False - - -def _latest_file( - fx_dir: str, filenames: list[str], currencies: list[str], *, valid_only: bool -) -> str | None: - """Return the newest matching FX cache filename.""" - matching_files = [ - filename - for filename in filenames - if not valid_only - or _has_required_quotes(os.path.join(fx_dir, filename), currencies) - ] - - return max(matching_files) if matching_files else None - - -def get_rates_exchangerate_host(config: flask.config.Config) -> dict[str, Decimal]: - """Get exchange rates from exchangerate.host. - - Kept as a fallback implementation in case we decide to switch back from - Frankfurter. - """ - currencies = config["CURRENCIES"] - access_key = config["EXCHANGERATE_ACCESS_KEY"] - data_dir = config["DATA_DIR"] - cache_ttl_hours = int(config.get("FX_CACHE_TTL_HOURS", DEFAULT_FX_CACHE_TTL_HOURS)) - failure_retry_hours = int( - config.get("FX_FAILURE_RETRY_HOURS", DEFAULT_FX_FAILURE_RETRY_HOURS) - ) - - now = datetime.now() - now_str = now.strftime("%Y-%m-%d_%H:%M") - fx_dir = os.path.join(data_dir, "fx") - os.makedirs(fx_dir, exist_ok=True) # Ensure the directory exists - - currency_string = ",".join(sorted(currencies)) - file_suffix = f"{currency_string}_to_GBP.json" - existing_data = os.listdir(fx_dir) - existing_files = [f for f in existing_data if f[17:] == file_suffix] - - latest_attempt = _latest_file(fx_dir, existing_files, currencies, valid_only=False) - latest_valid = _latest_file(fx_dir, existing_files, currencies, valid_only=True) - latest_valid_path = ( - os.path.join(fx_dir, latest_valid) if latest_valid is not None else None - ) - - if latest_valid is not None: - recent = _fx_cache_datetime(latest_valid) - delta = now - recent - - if delta < timedelta(hours=cache_ttl_hours) or config["OFFLINE_MODE"]: - return read_cached_rates(latest_valid_path, currencies) - - if latest_attempt is not None: - recent_attempt = _fx_cache_datetime(latest_attempt) - attempt_delta = now - recent_attempt - if attempt_delta < timedelta(hours=failure_retry_hours): - return read_cached_rates(latest_valid_path, currencies) - - url = "http://api.exchangerate.host/live" - params = {"currencies": currency_string, "source": "GBP", "access_key": access_key} - - filename = f"{now_str}_{file_suffix}" - try: - with httpx.Client() as client: - response = client.get(url, params=params, timeout=10) - except (httpx.ConnectError, httpx.ReadTimeout): - return read_cached_rates(latest_valid_path, currencies) - - try: - data = json.loads(response.text, parse_float=Decimal) - except json.decoder.JSONDecodeError: - return read_cached_rates(latest_valid_path, currencies) - - if not data.get("success", True) or not isinstance(data.get("quotes"), dict): - with open(os.path.join(fx_dir, filename), "w") as file: - file.write(response.text) - return read_cached_rates(latest_valid_path, currencies) - - with open(os.path.join(fx_dir, filename), "w") as file: - file.write(response.text) - - return { - cur: Decimal(data["quotes"][f"GBP{cur}"]) - for cur in currencies - if f"GBP{cur}" in data["quotes"] - } - - -def get_rates(config: flask.config.Config) -> dict[str, Decimal]: - """Get current values of exchange rates for a list of currencies against GBP.""" - currencies = config["CURRENCIES"] - data_dir = config["DATA_DIR"] - cache_ttl_hours = int(config.get("FX_CACHE_TTL_HOURS", DEFAULT_FX_CACHE_TTL_HOURS)) - failure_retry_hours = int( - config.get("FX_FAILURE_RETRY_HOURS", DEFAULT_FX_FAILURE_RETRY_HOURS) - ) - - now = datetime.now() - now_str = now.strftime("%Y-%m-%d_%H:%M") - fx_dir = os.path.join(data_dir, "fx") - os.makedirs(fx_dir, exist_ok=True) # Ensure the directory exists - - currency_string = ",".join(sorted(currencies)) - legacy_file_suffix = f"{currency_string}_to_GBP.json" - frankfurter_file_suffix = f"{FRANKFURTER_CACHE_PREFIX}_{legacy_file_suffix}" - existing_data = os.listdir(fx_dir) - valid_cache_files = [ - f - for f in existing_data - if f[17:] in (legacy_file_suffix, frankfurter_file_suffix) - ] - attempt_files = [f for f in existing_data if f[17:] == frankfurter_file_suffix] - # Cache filenames already record the requested currencies. Compare against - # the newest request, including failed attempts, so failures retain backoff. - all_rate_files = [f for f in existing_data if f.endswith("_to_GBP.json")] - latest_request = max(all_rate_files) if all_rate_files else None - currencies_changed = latest_request is not None and latest_request[17:] not in ( - legacy_file_suffix, - frankfurter_file_suffix, - ) - - latest_attempt = _latest_file(fx_dir, attempt_files, currencies, valid_only=False) - latest_source_valid = _latest_file( - fx_dir, attempt_files, currencies, valid_only=True - ) - latest_valid = _latest_file(fx_dir, valid_cache_files, currencies, valid_only=True) - latest_valid_path = ( - os.path.join(fx_dir, latest_valid) if latest_valid is not None else None - ) - - if config["OFFLINE_MODE"]: - return read_cached_rates(latest_valid_path, currencies) - - if latest_source_valid is not None and not currencies_changed: - recent = _fx_cache_datetime(latest_source_valid) - delta = now - recent - - if delta < timedelta(hours=cache_ttl_hours): - return read_cached_rates( - os.path.join(fx_dir, latest_source_valid), currencies - ) - - if latest_attempt is not None and not currencies_changed: - recent_attempt = _fx_cache_datetime(latest_attempt) - attempt_delta = now - recent_attempt - if attempt_delta < timedelta(hours=failure_retry_hours): - return read_cached_rates(latest_valid_path, currencies) - - url = "https://api.frankfurter.dev/v2/rates" - params = {"base": "GBP", "quotes": currency_string} - - filename = f"{now_str}_{frankfurter_file_suffix}" - try: - with httpx.Client() as client: - response = client.get(url, params=params, timeout=10) - except (httpx.ConnectError, httpx.ReadTimeout): - return read_cached_rates(latest_valid_path, currencies) - - try: - data = json.loads(response.text, parse_float=Decimal) - except json.decoder.JSONDecodeError: - return read_cached_rates(latest_valid_path, currencies) - - frankfurter_rates = _frankfurter_rates(data, currencies) - if not frankfurter_rates: - with open(os.path.join(fx_dir, filename), "w") as file: - file.write(response.text) - return read_cached_rates(latest_valid_path, currencies) - - with open(os.path.join(fx_dir, filename), "w") as file: - file.write(response.text) - - return frankfurter_rates diff --git a/agenda/gandi.py b/agenda/gandi.py deleted file mode 100644 index b5f3278..0000000 --- a/agenda/gandi.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Gandi domain renewal dates.""" - -import os -from .event import Event -from datetime import datetime - -import json - - -def get_events(data_dir: str) -> list[Event]: - """Get subscription renewal dates.""" - filename = os.path.join(data_dir, "gandi_domains.json") - - with open(filename) as f: - items = json.load(f) - - assert isinstance(items, list) - assert all(item["fqdn"] and item["dates"]["registry_ends_at"] for item in items) - - return [ - Event( - date=datetime.fromisoformat(item["dates"]["registry_ends_at"]).date(), - name="domain", - title=item["fqdn"] + " renewal", - ) - for item in items - ] diff --git a/agenda/generate_booking_yaml.py b/agenda/generate_booking_yaml.py deleted file mode 100644 index 3ce8ddd..0000000 --- a/agenda/generate_booking_yaml.py +++ /dev/null @@ -1,483 +0,0 @@ -"""Generate travel booking YAML from booking text or a booking URL.""" - -import argparse -import configparser -import importlib -import json -import os -import sys -import typing -from dataclasses import dataclass -from datetime import date, datetime -from pathlib import Path - -import html2text -import lxml.html -import openai -import requests -import yaml - -USER_AGENT = "generate-booking-yaml/0.1" -REPO_ROOT = Path(__file__).resolve().parent.parent -SPEC_PATH = REPO_ROOT / "docs" / "personal-data-yaml.md" -PERSONAL_DATA_DIR = Path("~/src/personal-data").expanduser() - - -class TripLike(typing.Protocol): - """Trip attributes needed for booking import matching.""" - - start: date - end: date | None - - -@dataclass(frozen=True) -class BookingConfig: - """Configuration for one travel booking YAML generator.""" - - booking_type: str - yaml_filename: str - spec_heading: str - start_field: str - excluded_top_level_keys: tuple[str, ...] - json_key: str = "booking" - - -BOOKING_CONFIGS: dict[str, BookingConfig] = { - "flight": BookingConfig( - booking_type="flight", - yaml_filename="flights.yaml", - spec_heading="flights.yaml", - start_field="depart", - excluded_top_level_keys=("trip",), - ), - "train": BookingConfig( - booking_type="train", - yaml_filename="trains.yaml", - spec_heading="trains.yaml", - start_field="depart", - excluded_top_level_keys=("trip",), - ), - "accommodation": BookingConfig( - booking_type="accommodation", - yaml_filename="accommodation.yaml", - spec_heading="accommodation.yaml", - start_field="from", - excluded_top_level_keys=("trip", "latitude", "longitude"), - ), -} - - -def read_api_key() -> str: - """Read API key from ~/.config/openai/config.""" - config_path = os.path.expanduser("~/.config/openai/config") - parser = configparser.ConfigParser() - parser.read(config_path) - return parser["openai"]["api_key"] - - -def read_markdown_section(markdown_text: str, heading: str) -> str: - """Return one second-level markdown section by heading text.""" - section_headings = (f"## `{heading}`", f"## {heading}") - start = -1 - matched_heading = "" - for section_heading in section_headings: - start = markdown_text.find(section_heading) - if start != -1: - matched_heading = section_heading - break - if start == -1: - raise ValueError(f"Could not find section for heading {heading!r}") - - next_heading = markdown_text.find("\n## ", start + len(matched_heading)) - if next_heading == -1: - return markdown_text[start:].strip() - return markdown_text[start:next_heading].strip() - - -def yaml_format_description(config: BookingConfig) -> str: - """Return relevant personal-data YAML documentation for the prompt.""" - spec_text = SPEC_PATH.read_text() - sections = [ - read_markdown_section(spec_text, "General Rules"), - read_markdown_section(spec_text, "Cross-File References"), - read_markdown_section(spec_text, config.spec_heading), - ] - return "\n\n".join(sections) - - -def read_existing_bookings( - config: BookingConfig, data_dir: Path = PERSONAL_DATA_DIR -) -> str: - """Read the existing YAML file for examples and local style.""" - path = data_dir / config.yaml_filename - return path.read_text() - - -def build_prompt( - booking_text: str, - config: BookingConfig, - current_bookings: str | None = None, -) -> str: - """Build prompt to pass to the LLM.""" - bookings = current_bookings - if bookings is None: - bookings = read_existing_bookings(config) - excluded_keys = ", ".join(f'"{key}"' for key in config.excluded_top_level_keys) - - return f""" -I keep a record of all my {config.booking_type} bookings in a YAML file. - -Use this YAML format specification: - -{yaml_format_description(config)} - -Here's my current list of bookings for examples of local style and known -references. -=== -{bookings} -=== -Here's a new booking I just made. - -Return the YAML representation for this booking using the documented format and -the same local style as my existing bookings. - -Rules: -- Wrap the response in a JSON object with a single key "{config.json_key}" that - contains the booking in YAML. -- The value of "{config.json_key}" must be YAML text, not JSON. -- Exclude these top-level keys from the YAML: {excluded_keys}. -- Do not invent details that are not present in the booking text. -- Quote prices and identifiers that might otherwise be parsed as numbers. - -=== -{booking_text} -""" - - -def get_from_open_ai(prompt: str, model: str = "gpt-5.4") -> dict[str, str]: - """Pass prompt to OpenAI and get reply.""" - client = openai.OpenAI(api_key=read_api_key()) - - response = client.chat.completions.create( - messages=[{"role": "user", "content": prompt}], - model=model, - response_format={"type": "json_object"}, - ) - - reply = response.choices[0].message.content - assert isinstance(reply, str) - return typing.cast(dict[str, str], json.loads(reply)) - - -def fetch_webpage(url: str) -> lxml.html.HtmlElement: - """Fetch webpage HTML and parse it.""" - response = requests.get(url, headers={"User-Agent": USER_AGENT}) - response.raise_for_status() - return lxml.html.fromstring(response.content) - - -def webpage_to_text(root: lxml.html.HtmlElement) -> str: - """Convert parsed HTML into readable text content.""" - root_copy = lxml.html.fromstring(lxml.html.tostring(root)) - - for script_or_style in root_copy.xpath("//script|//style"): - script_or_style.drop_tree() - - text_maker = html2text.HTML2Text() - text_maker.ignore_links = False - text_maker.ignore_images = True - return text_maker.handle(lxml.html.tostring(root_copy, encoding="unicode")) - - -def url_to_booking_text(url: str) -> str: - """Fetch a URL and convert it to source text for the model.""" - return webpage_to_text(fetch_webpage(url)) - - -def booking_text_from_args(args: list[str]) -> str: - """Return booking text from a URL argument or stdin.""" - if args: - if len(args) != 1: - raise SystemExit("Usage: generate-BOOKING-booking-yaml [URL]") - return url_to_booking_text(args[0]) - return sys.stdin.read() - - -def generate_booking_yaml( - booking_text: str, config: BookingConfig, model: str = "gpt-5.4" -) -> str: - """Generate booking YAML from source text.""" - prompt = build_prompt(booking_text, config) - return get_from_open_ai(prompt, model=model)[config.json_key] - - -def datetime_from_yaml_value(value: typing.Any) -> datetime: - """Convert a YAML date/datetime/string value into a datetime.""" - if isinstance(value, datetime): - return value - if isinstance(value, date): - return datetime.combine(value, datetime.min.time()) - if isinstance(value, str): - parsed = datetime.fromisoformat(value) - return parsed - raise TypeError(f"Unsupported departure value: {value!r}") - - -def first_departure(booking: dict[str, typing.Any], config: BookingConfig) -> datetime: - """Return the first departure datetime for a generated booking.""" - if config.booking_type == "flight": - flights = booking["flights"] - assert isinstance(flights, list) - first_flight = flights[0] - assert isinstance(first_flight, dict) - return datetime_from_yaml_value(first_flight["depart"]) - - return datetime_from_yaml_value(booking[config.start_field]) - - -def comparable_departure( - booking: dict[str, typing.Any], config: BookingConfig -) -> datetime: - """Return a timezone-naive departure datetime for sorting.""" - return first_departure(booking, config).replace(tzinfo=None) - - -def generated_bookings_from_yaml(yaml_text: str) -> list[dict[str, typing.Any]]: - """Parse generated booking YAML into a list of booking mappings.""" - loaded = yaml.safe_load(yaml_text) - if isinstance(loaded, dict): - return [typing.cast(dict[str, typing.Any], loaded)] - if isinstance(loaded, list) and all(isinstance(item, dict) for item in loaded): - return typing.cast(list[dict[str, typing.Any]], loaded) - raise ValueError("Generated booking YAML must be a mapping or list of mappings.") - - -def trip_key_position(booking: dict[str, typing.Any], config: BookingConfig) -> int: - """Return the preferred insertion position for the top-level trip key.""" - keys = list(booking) - if config.booking_type == "flight": - if "booking_reference" in booking: - return keys.index("booking_reference") + 1 - return 0 - - if config.booking_type == "accommodation": - if "country" in booking: - return keys.index("country") + 1 - if "location" in booking: - return keys.index("location") + 1 - return 0 - - if "to" in booking: - return keys.index("to") + 1 - if "from" in booking: - return keys.index("from") + 1 - return 0 - - -def set_trip_key( - booking: dict[str, typing.Any], config: BookingConfig, trip_date: date -) -> dict[str, typing.Any]: - """Set trip in the usual top-level position while preserving other key order.""" - without_trip = {key: value for key, value in booking.items() if key != "trip"} - keys = list(without_trip) - position = trip_key_position(without_trip, config) - reordered: dict[str, typing.Any] = {} - - for index, key in enumerate(keys): - if index == position: - reordered["trip"] = trip_date - reordered[key] = without_trip[key] - - if "trip" not in reordered: - reordered["trip"] = trip_date - - booking.clear() - booking.update(reordered) - return booking - - -def build_trips(data_dir: Path) -> list[TripLike]: - """Build trips from personal data without importing agenda.trip at module load.""" - trip_module = importlib.import_module("agenda.trip") - build_trip_list = typing.cast( - typing.Callable[..., list[TripLike]], trip_module.build_trip_list - ) - return build_trip_list(data_dir=str(data_dir)) - - -def matching_trip_date(depart: datetime, data_dir: Path = PERSONAL_DATA_DIR) -> date: - """Find the trip grouping date for a departure, falling back to departure date.""" - depart_date = depart.date() - matching_starts: list[date] = [] - - for trip in build_trips(data_dir): - trip_end = trip.end or trip.start - if trip.start <= depart_date <= trip_end: - matching_starts.append(trip.start) - - if matching_starts: - return max(matching_starts) - return depart_date - - -def add_trip_dates( - bookings: list[dict[str, typing.Any]], - config: BookingConfig, - data_dir: Path = PERSONAL_DATA_DIR, -) -> None: - """Add the top-level trip key to generated booking mappings.""" - for booking in bookings: - trip_date = matching_trip_date(first_departure(booking, config), data_dir) - set_trip_key(booking, config, trip_date) - - -def dump_generated_bookings(bookings: list[dict[str, typing.Any]]) -> str: - """Dump only generated bookings for insertion into an existing YAML list.""" - text = yaml.dump(bookings, sort_keys=False, allow_unicode=True) - return text.lstrip() - - -def join_yaml_list_blocks(preamble: str, blocks: list[str], trailing: str = "") -> str: - """Join top-level YAML list blocks with a blank line between items.""" - body = "\n\n".join(block.rstrip("\n") for block in blocks) - return preamble + body + "\n" + trailing - - -def split_yaml_list_blocks(text: str) -> tuple[str, list[str], str]: - """Split a top-level YAML list into preamble, item blocks, and trailing text.""" - lines = text.splitlines(keepends=True) - first_item = next( - (index for index, line in enumerate(lines) if line.startswith("- ")), None - ) - if first_item is None: - return text, [], "" - - item_starts = [ - index - for index, line in enumerate(lines[first_item:], start=first_item) - if line.startswith("- ") - ] - blocks = [ - "".join(lines[start:end]) - for start, end in zip(item_starts, item_starts[1:] + [len(lines)]) - ] - return "".join(lines[:first_item]), blocks, "" - - -def existing_bookings_from_blocks(blocks: list[str]) -> list[dict[str, typing.Any]]: - """Parse split YAML item blocks into booking mappings.""" - bookings = [] - for block in blocks: - loaded = yaml.safe_load(block) - if not isinstance(loaded, list) or len(loaded) != 1: - raise ValueError("Could not parse existing booking block.") - item = loaded[0] - if not isinstance(item, dict): - raise ValueError("Existing booking block is not a mapping.") - bookings.append(typing.cast(dict[str, typing.Any], item)) - return bookings - - -def insertion_index( - existing_bookings: list[dict[str, typing.Any]], - new_bookings: list[dict[str, typing.Any]], - config: BookingConfig, -) -> int: - """Return the chronological insertion index for generated bookings.""" - new_depart = min(comparable_departure(booking, config) for booking in new_bookings) - for index, booking in enumerate(existing_bookings): - if comparable_departure(booking, config) > new_depart: - return index - return len(existing_bookings) - - -def insert_booking_text( - existing_text: str, - new_yaml_text: str, - config: BookingConfig, -) -> str: - """Insert generated booking YAML into an existing top-level YAML list.""" - preamble, blocks, trailing = split_yaml_list_blocks(existing_text) - new_bookings = generated_bookings_from_yaml(new_yaml_text) - _, new_blocks, _ = split_yaml_list_blocks(dump_generated_bookings(new_bookings)) - if len(new_blocks) != len(new_bookings): - raise ValueError("Could not split generated booking YAML into item blocks.") - - existing_bookings = existing_bookings_from_blocks(blocks) - - new_items = sorted( - zip(new_bookings, new_blocks), - key=lambda item: comparable_departure(item[0], config), - ) - for booking, block in new_items: - insert_at = insertion_index(existing_bookings, [booking], config) - existing_bookings.insert(insert_at, booking) - blocks.insert(insert_at, block) - - return join_yaml_list_blocks(preamble, blocks, trailing) - - -def import_booking_yaml( - generated_yaml: str, - config: BookingConfig, - data_dir: Path = PERSONAL_DATA_DIR, -) -> int: - """Add generated booking YAML to the configured personal-data file.""" - bookings = generated_bookings_from_yaml(generated_yaml) - add_trip_dates(bookings, config, data_dir) - new_yaml = dump_generated_bookings(bookings) - - yaml_path = data_dir / config.yaml_filename - existing_text = yaml_path.read_text() - updated_text = insert_booking_text(existing_text, new_yaml, config) - yaml_path.write_text(updated_text) - return len(bookings) - - -def main_for_type(booking_type: str, argv: list[str] | None = None) -> int: - """CLI entrypoint for a specific booking type.""" - parser = argparse.ArgumentParser( - description=( - f"Generate {booking_type} booking YAML from stdin or a URL and import it." - ) - ) - parser.add_argument("url", nargs="?", help="Booking URL to fetch") - parser.add_argument("--model", default=os.environ.get("OPENAI_MODEL", "gpt-5.4")) - parser.add_argument( - "--data-dir", - default=str(PERSONAL_DATA_DIR), - help="Directory containing personal-data YAML files.", - ) - parser.add_argument( - "--print-only", - action="store_true", - help="Print generated YAML instead of editing the personal-data file.", - ) - parsed = parser.parse_args(argv) - - config = BOOKING_CONFIGS[booking_type] - args = [parsed.url] if parsed.url else [] - booking_text = booking_text_from_args(args) - new_yaml = generate_booking_yaml(booking_text, config, model=parsed.model) - if parsed.print_only: - print(new_yaml) - return 0 - - count = import_booking_yaml(new_yaml, config, data_dir=Path(parsed.data_dir)) - print(f"Imported {count} {booking_type} booking(s).") - return 0 - - -def train_main(argv: list[str] | None = None) -> int: - """CLI entrypoint for train booking YAML generation.""" - return main_for_type("train", argv) - - -def flight_main(argv: list[str] | None = None) -> int: - """CLI entrypoint for flight booking YAML generation.""" - return main_for_type("flight", argv) - - -def accommodation_main(argv: list[str] | None = None) -> int: - """CLI entrypoint for accommodation booking YAML generation.""" - return main_for_type("accommodation", argv) diff --git a/agenda/geomob.py b/agenda/geomob.py deleted file mode 100644 index e992747..0000000 --- a/agenda/geomob.py +++ /dev/null @@ -1,225 +0,0 @@ -"""Geomob events.""" - -import logging -import os -import subprocess -from pathlib import Path -from dataclasses import dataclass -from datetime import date, datetime -from typing import List - -import dateutil.parser -import flask -import lxml.html # type: ignore[import-untyped] -import requests - -import agenda.add_new_conference -import agenda.conference_list -import agenda.mail -import agenda.utils - -logger = logging.getLogger(__name__) -AGENDA_BASE_URL = "https://edwardbetts.com/agenda" - - -@dataclass(frozen=True) -class GeomobEvent: - """Geomob event.""" - - date: date - href: str - hashtag: str - - -def extract_events( - tree: lxml.html.HtmlElement, -) -> List[GeomobEvent]: - """Extract upcoming events from the HTML content.""" - events = [] - - for event in tree.xpath('//ol[@class="event-list"]/li/a'): - date_str, _, hashtag = event.text_content().strip().rpartition(" ") - events.append( - GeomobEvent( - date=dateutil.parser.parse(date_str).date(), - href=event.get("href"), - hashtag=hashtag, - ) - ) - - return events - - -def find_new_events( - prev: list[GeomobEvent], cur: list[GeomobEvent] -) -> list[GeomobEvent]: - """Find new events that appear in cur but not in prev.""" - return list(set(cur) - set(prev)) - - -def geomob_email( - new_events: list[GeomobEvent], - base_url: str, - recording_results: dict[GeomobEvent, str] | None = None, - conference_links: dict[GeomobEvent, str] | None = None, -) -> tuple[str, str]: - """Generate email subject and body for new events. - - Args: - new_events (List[Event]): List of new events. - base_url (str): The base URL of the website. - - Returns: - tuple[str, str]: Email subject and body. - """ - assert new_events - - subject = f"{len(new_events)} New Geomob Event(s) Announced" - - body_lines = ["Hello,\n", "Here are the new Geomob events:\n"] - for event in new_events: - url = base_url + event.href - # Check for double slashes in the path part only (after protocol) - if "://" in url: - protocol, rest = url.split("://", 1) - assert "//" not in rest, f"Double slash found in URL path: {url}" - else: - assert "//" not in url, f"Double slash found in URL: {url}" - event_details = f"Date: {event.date}\nURL: {url}\nHashtag: {event.hashtag}\n" - if conference_links and event in conference_links: - event_details += f"Conference page: {conference_links[event]}\n" - if recording_results is not None: - event_details += f"Recording: {recording_results[event]}\n" - body_lines.append(event_details) - body_lines.append("-" * 40) - - body = "\n".join(body_lines) - return (subject, body) - - -def conference_page_links( - events: list[GeomobEvent], base_url: str, personal_data: str -) -> dict[GeomobEvent, str]: - """Find saved events using the same dated identities as the conference pages.""" - try: - conferences = agenda.conference_list.build_conference_list(personal_data, []) - except Exception: - logger.warning( - "Could not find conference links for Geomob email", exc_info=True - ) - return {} - by_identity = { - (conference.get("url"), conference["start_date"]): conference - for conference in conferences - } - links = {} - for event in events: - conference = by_identity.get((base_url + event.href, event.date)) - if conference is not None: - links[event] = ( - f"{AGENDA_BASE_URL}/conference/{conference['sort_date'].isoformat()}/{conference['slug']}" - ) - return links - - -def record_event(event: GeomobEvent, base_url: str) -> str: - """Record an event, reporting failures without suppressing its notification. - - Leave failed changes in place for inspection. Only conferences.yaml is staged - and committed, and an initially dirty repository is never modified. - """ - repository = Path("~/src/personal-data").expanduser() - yaml_path = repository / "conferences.yaml" - validator = Path(__file__).resolve().parent.parent / "validate_yaml.py" - stage = "check repository" - recorded = False - - def run(command: list[str]) -> str: - result = subprocess.run( - command, - cwd=repository, - capture_output=True, - text=True, - timeout=300, - check=True, - ) - return result.stdout.strip() - - try: - if run(["git", "status", "--porcelain"]): - return "FAILED at check repository: uncommitted changes; no steps run." - stage = "git pull --rebase" - run(["git", "pull", "--rebase"]) - stage = "add conference" - added = agenda.add_new_conference.add_new_conference( - base_url + event.href, str(yaml_path) - ) - recorded = True - stage = "validate_yaml.py" - run(["python3", str(validator)]) - if added: - stage = "git add conferences.yaml" - run(["git", "add", "--", "conferences.yaml"]) - stage = "git commit" - run( - [ - "git", - "commit", - "--only", - "-m", - f"Add {event.hashtag} event on {event.date}", - "--", - "conferences.yaml", - ] - ) - stage = "git push" - run(["git", "push"]) - return ( - "Recorded in conferences.yaml, validated, committed and pushed." - if added - else "Already recorded in conferences.yaml; validated and pushed." - ) - except Exception as exc: - details = str(exc) - if isinstance(exc, subprocess.CalledProcessError): - details += f"\n{exc.stdout or ''}\n{exc.stderr or ''}" - elif isinstance(exc, subprocess.TimeoutExpired): - details += f"\n{exc.stdout!r}\n{exc.stderr!r}" - state = "Recorded locally; " if recorded else "" - return f"{state}FAILED at {stage}: {details.strip()}" - - -def get_cached_upcoming_events_list(geomob_dir: str) -> list[GeomobEvent]: - """Get known geomob events.""" - filename = agenda.utils.get_most_recent_file(geomob_dir, "html") - return extract_events(lxml.html.parse(filename).getroot()) if filename else [] - - -def update(config: flask.config.Config) -> None: - """Get upcoming Geomob events and report new ones.""" - geomob_dir = os.path.join(config["DATA_DIR"], "geomob") - - prev_events = get_cached_upcoming_events_list(geomob_dir) - r = requests.get("https://thegeomob.com/") - cur_events = extract_events(lxml.html.fromstring(r.content)) - - if cur_events == prev_events: - return # no change - - now = datetime.now() - new_filename = os.path.join(geomob_dir, now.strftime("%Y-%m-%d_%H:%M:%S.html")) - open(new_filename, "w").write(r.text) - - new_events = list(set(cur_events) - set(prev_events)) - if not new_events: - return - - base_url = "https://thegeomob.com" - recording_results = {event: record_event(event, base_url) for event in new_events} - links = ( - conference_page_links(new_events, base_url, config["PERSONAL_DATA"]) - if config.get("PERSONAL_DATA") - else {} - ) - subject, body = geomob_email(new_events, base_url, recording_results, links) - agenda.mail.send_mail(config, subject, body) diff --git a/agenda/google_flights.py b/agenda/google_flights.py deleted file mode 100644 index 871a663..0000000 --- a/agenda/google_flights.py +++ /dev/null @@ -1,417 +0,0 @@ -"""Read Google Flights in Chromium via Playwright, without a flight API client. - -Query schema and embedded result format documented by AWeirdDev/fast-flights. -See licenses/fast-flights-MIT.txt. Google's undocumented format may change. -""" - -import base64 -import contextvars -import json -import logging -import os -import re -import shutil -import typing -from contextlib import contextmanager -from datetime import date, datetime -from pathlib import Path -from urllib.parse import urlencode, urlsplit - -from playwright.sync_api import ( - BrowserContext, - Page, - Playwright, - Response, - sync_playwright, -) - -from agenda import flight_diagnostics, flight_search_cache -from agenda.types import StrDict - -logger = logging.getLogger(__name__) - -PARSER_VERSION = 2 -LONDON_AIRPORTS = ("LHR", "LGW", "STN", "LTN", "LCY", "SEN") - - -class FlightDateUnavailableError(ValueError): - """Google explicitly rejected a search beyond its available date range.""" - - -def date_limit_message(text: str) -> str | None: - """Recognize a provider date-range error without assuming a fixed horizon.""" - normalized_text = " ".join(text.split()) - if re.search( - r"(?:date|flight|departure|return)[^.]{0,160}too far[^.]{0,80}future", - normalized_text, - re.IGNORECASE, - ): - return "Requested flight date is too far in the future. Please try again closer to departure." - return None - - -def chromium_executable() -> str: - """Avoid Debian's shell wrapper: Apache hides its /proc/cpuinfo probe.""" - binary = Path("/usr/lib/chromium/chromium") - if binary.is_file() and os.access(binary, os.X_OK): - return str(binary) - executable = shutil.which("chromium") - if executable is None: - raise RuntimeError("Chromium is not installed") - return executable - - -def varint(value: int) -> bytes: - """Encode an unsigned protobuf integer.""" - result = bytearray() - while value > 127: - result.append((value & 127) | 128) - value >>= 7 - result.append(value) - return bytes(result) - - -def field(number: int, value: str | bytes | int) -> bytes: - """Encode the small subset of protobuf needed by a search URL.""" - if isinstance(value, int): - return varint(number << 3) + varint(value) - data = value.encode() if isinstance(value, str) else value - return varint((number << 3) | 2) + varint(len(data)) + data - - -def search_segment(origin: str, destination: str, day: date, max_stops: int) -> bytes: - """Encode airports, departure date and stop limit for one direction.""" - segment = field(2, day.isoformat()) + field(5, max_stops) - for number, code in ((13, origin), (14, destination)): - for member in code.split(","): - for airport in LONDON_AIRPORTS if member == "LON" else (member,): - segment += field(number, field(1, 1) + field(2, airport)) - return segment - - -def search_url( - origin: str, - destination: str, - day: date, - max_stops: int, - return_day: date | None = None, -) -> str: - """One adult, economy, British locale; one-way or a prefilled return trip.""" - info = field(3, search_segment(origin, destination, day, max_stops)) - if return_day is not None: - info += field(3, search_segment(destination, origin, return_day, max_stops)) - info += field(8, 1) + field(9, 1) + field(19, 1 if return_day is not None else 2) - return "https://www.google.com/travel/flights/search?" + urlencode( - { - "tfs": base64.urlsafe_b64encode(info).decode().rstrip("="), - "tfu": "EgQIABABIgA", - "hl": "en-GB", - "gl": "GB", - "curr": "GBP", - } - ) - - -def timestamp(values: list[int], clock: list[int | None] | None) -> str: - """Google omits zero hours/minutes; preserve midnight and overnight dates.""" - padded = [*(clock or []), None, None] - return datetime( - values[0], values[1], values[2], padded[0] or 0, padded[1] or 0 - ).isoformat() - - -def parse_results(script: str, url: str) -> list[StrDict]: - """Read Google's best flights followed by its remaining itineraries.""" - marker = re.search(r"\bdata\s*:\s*", script) - if marker is None: - raise ValueError("Google Flights page did not contain flight data") - payload, _ = json.JSONDecoder().raw_decode(script[marker.end() :]) - if not isinstance(payload, list) or len(payload) < 4: - message = date_limit_message(json.dumps(payload)) - if message: - raise FlightDateUnavailableError(message) - raise ValueError("Unrecognized Google Flights result format") - airlines: dict[str, str] = {} - try: - airlines = dict(payload[7][1][1]) - except (IndexError, TypeError, ValueError): - pass - rows: list[StrDict] = [] - malformed = 0 - for section in payload[2:4]: - if not section: - continue - for item in section[0] or []: - try: - itinerary = item[0] - raw_legs = itinerary[2] - if not raw_legs: - continue - legs = [] - for leg in raw_legs: - airline = leg[22] - code = str(airline[0]) - operating = airline[2] if len(airline) > 2 else None - legs.append( - { - "airline": airlines.get(code, code), - "airline_code": code, - "operating_airline_code": operating or code, - "number": f"{code} {airline[1]}", - "origin": leg[3], - "destination": leg[6], - } - ) - fare = item[1][0] if item[1] else [] - price = fare[1] if len(fare) > 1 else None - rows.append( - { - "price": price, - "currency": "GBP", - "stops": len(legs) - 1, - "duration": itinerary[9], - "departure": timestamp(raw_legs[0][20], raw_legs[0][8]), - "arrival": timestamp(raw_legs[-1][21], raw_legs[-1][10]), - "legs": legs, - "url": url, - } - ) - except (IndexError, TypeError, ValueError) as exc: - malformed += 1 - if not rows: - last_error = exc - if malformed and not rows: - raise ValueError("Could not decode Google Flights itineraries") from last_error - return rows - - -class BrowserSearch: - """One lazy browser session shared by the dates in a conference lookup.""" - - def __init__(self, directory: Path) -> None: - self.directory = directory - self.playwright: Playwright | None = None - self.context: BrowserContext | None = None - self.page: Page | None = None - self.rate_limited = False - self.response: Response | None = None - self.error_responses: list[Response] = [] - self.flight_script: str | None = None - - def start(self) -> Page: - """Use installed Chromium, including when running as Apache's www-data.""" - if self.page is None: - executable = chromium_executable() - self.playwright = sync_playwright().start() - self.context = self.playwright.chromium.launch_persistent_context( - str(self.directory / "browser-profile"), - executable_path=executable, - headless=True, - locale="en-GB", - timezone_id="Europe/London", - args=["--disable-dev-shm-usage"], - env={ - **os.environ, - "XDG_CONFIG_HOME": str(self.directory / "browser-config"), - "XDG_CACHE_HOME": str(self.directory / "browser-cache"), - }, - ) - self.page = self.context.new_page() - self.page.on("response", self.check_response) - self.page.set_default_timeout(20000) - return self.page - - def check_response(self, response: Response) -> None: - """Observe rate limits on flight data requests as well as navigation.""" - host = urlsplit(response.url).hostname or "" - is_google = host == "google.com" or host.endswith(".google.com") - if response.status >= 400 and is_google and len(self.error_responses) < 5: - self.error_responses.append(response) - if response.status == 429 and is_google: - if not self.rate_limited: - flight_search_cache.block_requests(self.directory) - self.rate_limited = True - - def check_cooldown(self) -> None: - """Surface a subrequest's 429 instead of reporting missing flight data.""" - if self.rate_limited: - raise flight_search_cache.RateLimitCooldownError( - flight_search_cache.cooldown_message(self.directory) - ) - - def search( - self, origin: str, destination: str, day: date, max_stops: int - ) -> list[StrDict]: - """Pace searches and capture failed responses without extra Google requests.""" - flight_search_cache.wait_for_request(self.directory) - self.response = None - self.error_responses = [] - self.flight_script = None - try: - return self.perform_search(origin, destination, day, max_stops) - except Exception as exc: - try: - self.record_failure(origin, destination, day, max_stops, exc) - except Exception: - logger.warning( - "Unable to capture flight failure diagnostics", exc_info=True - ) - raise - - def perform_search( - self, origin: str, destination: str, day: date, max_stops: int - ) -> list[StrDict]: - """Navigate once, honour consent, and stop immediately on rate limiting.""" - page = self.start() - url = search_url(origin, destination, day, max_stops) - response = page.goto(url, wait_until="domcontentloaded", timeout=45000) - self.response = response - if response is not None and response.status == 429: - flight_search_cache.block_requests(self.directory) - raise flight_search_cache.RateLimitCooldownError( - flight_search_cache.cooldown_message(self.directory) - ) - self.check_cooldown() - if response is not None and response.status >= 400: - raise RuntimeError(f"Google Flights returned HTTP {response.status}") - if "consent.google." in page.url: - page.get_by_role("button", name="Reject all", exact=True).click() - page.wait_for_url("**/travel/flights/**", timeout=20000) - script = page.locator("script.ds\\:1") - try: - script.wait_for(state="attached", timeout=20000) - except Exception as exc: - self.check_cooldown() - text = " ".join(page.locator("body").inner_text().split())[:400] - raise RuntimeError( - f"Google Flights did not load results ({page.url}): {text}" - ) from exc - self.check_cooldown() - self.flight_script = script.inner_text() - return self.read_results(page, self.flight_script, url, day) - - def read_results( - self, page: Page, script: str, url: str, day: date - ) -> list[StrDict]: - """Interpret provider messages when the embedded data isn't a result list.""" - try: - return parse_results(script, url) - except FlightDateUnavailableError: - raise - except ValueError as exc: - text = " ".join(page.locator("body").inner_text().split()) - message = date_limit_message(text) - if message: - raise FlightDateUnavailableError(message) from None - if re.search( - r"\b(?:no (?:(?:non[-\u2010-\u2015 ]?stop|direct) )?flights found|no results found|no matching flights)\b", - text, - re.IGNORECASE, - ): - return [] - ahead = (day - date.today()).days - context = f"Google Flights returned unusable search data for {day.strftime('%a %-d %b %Y')} ({ahead} days ahead)." - if ahead > 300: - context += " This date may be beyond the available airline schedules; try again closer to departure." - raise ValueError(context) from exc - - def record_failure( - self, origin: str, destination: str, day: date, max_stops: int, exc: Exception - ) -> None: - """Capture already-loaded data; unavailable page fields are recorded explicitly.""" - from agenda.conference_flights import error_detail - - details: StrDict = { - "origin": origin, - "destination": destination, - "departure_date": day.isoformat(), - "max_stops": max_stops, - "currency": "GBP", - "language": "en-GB", - "country": "GB", - "requested_url": search_url(origin, destination, day, max_stops), - "error": error_detail(exc), - "flight_script": self.flight_script, - "transport": flight_search_cache.TRANSPORT, - "parser_version": PARSER_VERSION, - } - capture_errors: list[str] = [] - if self.page is not None: - captures: tuple[tuple[str, typing.Callable[[], str]], ...] = ( - ("page_url", lambda: self.page.url if self.page else ""), - ("page_html", lambda: self.page.content() if self.page else ""), - ( - "page_text", - lambda: ( - self.page.locator("body").inner_text(timeout=2000) - if self.page - else "" - ), - ), - ) - for field, fetch in captures: - try: - details[field] = fetch() - except Exception as capture_error: - capture_errors.append( - f"{field}: {type(capture_error).__name__}: {capture_error}" - ) - responses = list(self.error_responses[:5]) - if self.response is not None: - details["http_status"] = self.response.status - if self.response.status >= 400 and self.response not in responses: - responses = [self.response, *responses][:5] - errors = [] - for response in responses: - try: - errors.append( - { - "url": response.url, - "status": response.status, - "body": response.text(), - } - ) - except Exception as capture_error: - capture_errors.append( - f"response body: {type(capture_error).__name__}: {capture_error}" - ) - details["http_errors"] = errors - details["capture_errors"] = capture_errors - flight_diagnostics.write_failure(self.directory, details) - - def close(self) -> None: - """Always release Chromium, including on failed searches.""" - try: - if self.context is not None: - self.context.close() - finally: - if self.playwright is not None: - self.playwright.stop() - - -session: contextvars.ContextVar[BrowserSearch | None] = contextvars.ContextVar( - "conference_browser_session", default=None -) - - -@contextmanager -def use_browser(directory: Path) -> typing.Iterator[None]: - """Lazy initialization keeps cache hits free of browser startup costs.""" - browser = BrowserSearch(directory) - token = session.set(browser) - try: - yield - finally: - session.reset(token) - browser.close() - - -def search(origin: str, destination: str, day: date, max_stops: int) -> list[StrDict]: - """Attach the route/date to browser failures for display on the page.""" - browser = session.get() - if browser is None: - raise RuntimeError("Flight searches require a conference browser session") - try: - return browser.search(origin, destination, day, max_stops) - except Exception as exc: - raise RuntimeError(f"{origin} → {destination} on {day.isoformat()}") from exc diff --git a/agenda/gwr.py b/agenda/gwr.py index 06fc273..5806939 100644 --- a/agenda/gwr.py +++ b/agenda/gwr.py @@ -3,7 +3,6 @@ import os import re from datetime import date, datetime -from html import unescape from time import time import httpx @@ -11,31 +10,6 @@ import httpx url = "https://www.gwr.com/your-tickets/choosing-your-ticket/advance-tickets" -def parse_date_string(date_str: str) -> date: - """Parse date string from HTML.""" - date_str = " ".join(unescape(date_str).split()) - if not date_str[-1].isdigit(): # If the year is missing, use the current year - date_str += f" {date.today().year}" - - return datetime.strptime(date_str, "%A %d %B %Y").date() - - -def extract_dates(html: str) -> None | dict[str, date]: - """Extract dates from HTML.""" - pattern = re.compile( - r"\s*(Weekdays|Saturdays|Sundays)*" - + r"\s*(.*?)(?:\*\*)?\s*", - ) - - if not pattern.search(html): - return None - - return { - match.group(1): parse_date_string(match.group(2)) - for match in pattern.finditer(html) - } - - def extract_weekday_date(html: str) -> date | None: """Furthest date of GWR advance ticket booking.""" # Compile a regular expression pattern to match the relevant table row @@ -44,19 +18,22 @@ def extract_weekday_date(html: str) -> date | None: ) # Search the HTML for the pattern - if match := pattern.search(html): - return parse_date_string(match.group(1)) - else: + if not (match := pattern.search(html)): return None + date_str = match.group(1) + + # If the year is missing, use the current year + if not date_str[-1].isdigit(): + date_str += f" {date.today().year}" + + return datetime.strptime(date_str, "%A %d %B %Y").date() -async def advance_tickets_page_html( - data_dir: str, ttl: int = 60 * 60 * 6, force_cache: bool = False -) -> str: +async def advance_tickets_page_html(data_dir: str, ttl: int = 60 * 60 * 6) -> str: """Get advance-tickets web page HTML with cache.""" filename = os.path.join(data_dir, "advance-tickets.html") mtime = os.path.getmtime(filename) if os.path.exists(filename) else 0 - if force_cache or (time() - mtime) < ttl: # use cache + if (time() - mtime) < ttl: # use cache return open(filename).read() async with httpx.AsyncClient() as client: r = await client.get(url) @@ -65,7 +42,7 @@ async def advance_tickets_page_html( return html -async def advance_ticket_date(data_dir: str, force_cache: bool = False) -> date | None: +async def advance_ticket_date(data_dir: str) -> date | None: """Get GWR advance tickets date with cache.""" - html = await advance_tickets_page_html(data_dir, force_cache=force_cache) + html = await advance_tickets_page_html(data_dir) return extract_weekday_date(html) diff --git a/agenda/hn.py b/agenda/hn.py index 52bdb0a..9f11e91 100644 --- a/agenda/hn.py +++ b/agenda/hn.py @@ -5,7 +5,7 @@ from datetime import date, datetime, time, timedelta import pytz from dateutil.relativedelta import relativedelta -from .event import Event +from .types import Event eastern_time = pytz.timezone("America/New_York") diff --git a/agenda/holidays.py b/agenda/holidays.py deleted file mode 100644 index 5c91e95..0000000 --- a/agenda/holidays.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Holidays.""" - -import collections -from datetime import date, timedelta - -import flask - -import agenda.uk_holiday -import holidays -from agenda.uk_school_holiday import school_holiday_list - -from .event import Event -from .types import Holiday, Trip - - -def get_trip_holidays(trip: Trip) -> list[Holiday]: - """Get holidays happening during trip.""" - if not trip.end: - return [] - countries = {"GB", *(c.alpha_2 for c in trip.countries)} - return sorted( - ( - hol - for hol in get_all( - trip.start, trip.end, flask.current_app.config["DATA_DIR"] - ) - if hol.country.upper() in countries - ), - key=lambda item: (item.date, item.country), - ) - - -def get_school_holidays(start_date: date, end_date: date, data_dir: str) -> list[Event]: - """Get UK school holidays from cache.""" - return school_holiday_list(start_date, end_date, data_dir) - - -def get_trip_school_holidays(trip: Trip) -> list[Event]: - """Get UK school holidays happening during trip.""" - if not trip.end: - return [] - - return get_school_holidays( - trip.start, - trip.end, - flask.current_app.config["DATA_DIR"], - ) - - -def us_holidays(start_date: date, end_date: date) -> list[Holiday]: - """Get US holidays.""" - found: list[Holiday] = [] - for year in range(start_date.year, end_date.year + 1): - hols = holidays.country_holidays("US", years=year, language="en") - found += [ - Holiday(date=hol_date, name=title, country="us") - for hol_date, title in hols.items() - if start_date < hol_date < end_date - ] - - extra = [] - for h in found: - if h.name != "Thanksgiving": - continue - extra += [ - Holiday(date=h.date + timedelta(days=1), name="Black Friday", country="us"), - Holiday(date=h.date + timedelta(days=4), name="Cyber Monday", country="us"), - ] - - return found + extra - - -def get_nyse_holidays( - start_date: date, end_date: date, us_hols: list[Holiday] -) -> list[Event]: - """NYSE holidays.""" - known_us_hols = {(h.date, h.name) for h in us_hols} - found: list[Event] = [] - rename = {"Thanksgiving Day": "Thanksgiving"} - for year in range(start_date.year, end_date.year + 1): - hols = holidays.financial_holidays("NYSE", years=year) - found += [ - Event( - name="holiday", - date=hol_date, - title=rename.get(title, title), - ) - for hol_date, title in hols.items() - if start_date <= hol_date <= end_date - ] - found = [hol for hol in found if (hol.date, hol.title) not in known_us_hols] - for hol in found: - assert hol.title - hol.title += " (NYSE)" - return found - - -def get_holidays(country: str, start_date: date, end_date: date) -> list[Holiday]: - """Get holidays.""" - found: list[Holiday] = [] - uc_country = country.upper() - - holiday_country = getattr(holidays, uc_country) - default_language = holiday_country.default_language - - def skip_holiday(holiday_name: str) -> bool: - """Skip holiday.""" - return country == "se" and holiday_name == "Sunday" - - for year in range(start_date.year, end_date.year + 1): - en_hols = holidays.country_holidays(uc_country, years=year, language="en_US") - local_lang = holidays.country_holidays( - uc_country, years=year, language=default_language - ) - found += [ - Holiday( - date=hol_date, - name=title, - local_name=local_lang[hol_date], - country=country.lower(), - ) - for hol_date, title in en_hols.items() - if start_date <= hol_date <= end_date and not skip_holiday(title) - ] - - return found - - -def combine_holidays(holidays: list[Holiday]) -> list[Event]: - """Combine UK and US holidays with the same date and title.""" - all_countries = {h.country for h in holidays} - - standard_name = { - (1, 1): "New Year's Day", - (1, 6): "Epiphany", - (5, 1): "Labour Day", - (8, 15): "Assumption Day", - (12, 8): "Immaculate conception", - (12, 25): "Christmas Day", - (12, 26): "Boxing Day", - } - - combined: collections.defaultdict[tuple[date, str], set[str]] = ( - collections.defaultdict(set) - ) - - for h in holidays: - assert isinstance(h.name, str) and isinstance(h.date, date) - - event_key = (h.date, standard_name.get((h.date.month, h.date.day), h.name)) - combined[event_key].add(h.country) - - events: list[Event] = [] - for (d, name), countries in combined.items(): - if len(countries) == len(all_countries): - country_list = "" - elif len(countries) < len(all_countries) / 2: - country_list = ", ".join(sorted(country.upper() for country in countries)) - else: - country_list = "not " + ", ".join( - sorted(country.upper() for country in all_countries - set(countries)) - ) - - e = Event( - name="holiday", - date=d, - title=f"{name} ({country_list})" if country_list else name, - ) - events.append(e) - - return events - - -def get_all(last_year: date, next_year: date, data_dir: str) -> list[Holiday]: - """Get holidays for various countries and return as a list.""" - us_hols = us_holidays(last_year, next_year) - - bank_holidays = agenda.uk_holiday.bank_holiday_list(last_year, next_year, data_dir) - - holiday_list: list[Holiday] = bank_holidays + us_hols - for country in flask.current_app.config["HOLIDAY_COUNTRIES"]: - holiday_list += get_holidays(country, last_year, next_year) - - return holiday_list - - -def trip_school_holiday_map(trips: list[Trip], data_dir: str) -> dict[str, list[Event]]: - """Map trip-start ISO date to overlapping UK school holidays.""" - if not trips: - return {} - - starts = [trip.start for trip in trips] - ends = [trip.end or trip.start for trip in trips] - school_holidays = get_school_holidays( - min(starts), - max(ends), - data_dir, - ) - - result: dict[str, list[Event]] = {} - for trip in trips: - trip_end = trip.end or trip.start - overlaps = [ - school_holiday - for school_holiday in school_holidays - if school_holiday.as_date <= trip_end - and school_holiday.end_as_date >= trip.start - ] - result[trip.start.isoformat()] = overlaps - - return result diff --git a/agenda/ical.py b/agenda/ical.py deleted file mode 100644 index f180cc5..0000000 --- a/agenda/ical.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Shared helpers for generating iCalendar feeds.""" - -from __future__ import annotations - -from datetime import date, datetime, timezone -from typing import Iterable - - -def escape_text(value: str) -> str: - """Escape text for safer ICS output.""" - return ( - value.replace("\\", "\\\\") - .replace(";", "\\;") - .replace(",", "\\,") - .replace("\n", "\\n") - ) - - -def _fold_line(value: str) -> Iterable[str]: - """Yield RFC5545 folded lines.""" - if len(value) <= 75: - yield value - return - - remaining = value - first = True - while remaining: - segment = remaining[:75] - remaining = remaining[75:] - if not first: - segment = " " + segment - yield segment - first = False - - -def append_property(lines: list[str], name: str, value: str) -> None: - """Append a folded property line to the ICS output.""" - for line in _fold_line(f"{name}:{value}"): - lines.append(line) - - -def format_datetime_utc(dt_value: datetime) -> str: - """Return datetime formatted in UTC for ICS.""" - if dt_value.tzinfo is None: - dt_value = dt_value.replace(tzinfo=timezone.utc) - else: - dt_value = dt_value.astimezone(timezone.utc) - return dt_value.strftime("%Y%m%dT%H%M%SZ") - - -def format_date(date_value: date) -> str: - """Return date formatted for all-day ICS events.""" - return date_value.strftime("%Y%m%d") diff --git a/agenda/mail.py b/agenda/mail.py deleted file mode 100644 index ac629eb..0000000 --- a/agenda/mail.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Send e-mail.""" - -import smtplib -from email.message import EmailMessage -from email.utils import formatdate, make_msgid - -import flask - - -def send_mail(config: flask.config.Config, subject: str, body: str) -> None: - """Send an e-mail.""" - msg = EmailMessage() - - msg["Subject"] = subject - msg["To"] = f"{config['NAME']} <{config['MAIL_TO']}>" - msg["From"] = f"{config['NAME']} <{config['MAIL_FROM']}>" - msg["Date"] = formatdate() - msg["Message-ID"] = make_msgid() - - # Add extra mail headers - for header, value in config["MAIL_HEADERS"]: - msg[header] = value - - msg.set_content(body) - - s = smtplib.SMTP(config["SMTP_HOST"]) - s.sendmail(config["MAIL_TO"], [config["MAIL_TO"]], msg.as_string()) - s.quit() diff --git a/agenda/meetup.py b/agenda/meetup.py index df52a23..ab80d16 100644 --- a/agenda/meetup.py +++ b/agenda/meetup.py @@ -4,7 +4,7 @@ import json import os.path from datetime import datetime -from .event import Event +from .types import Event def get_events(data_dir: str) -> list[Event]: @@ -21,7 +21,7 @@ def get_events(data_dir: str) -> list[Event]: date=start, end_date=end, name="meetup", - title=item_event["title"], + title="👥" + item_event["title"], url=item_event["eventUrl"], ) events.append(e) diff --git a/agenda/meteors.py b/agenda/meteors.py deleted file mode 100644 index 5b6e5cb..0000000 --- a/agenda/meteors.py +++ /dev/null @@ -1,270 +0,0 @@ -"""Meteor shower data calculations.""" - -import typing -from datetime import datetime, timedelta - -import ephem # type: ignore - -MeteorShower = dict[str, typing.Any] - - -# Meteor shower definitions with parent comet orbital elements -METEOR_SHOWERS = { - "Quadrantids": { - "name": "Quadrantids", - "radiant_ra": "15h20m", # Right ascension at peak - "radiant_dec": "+49.5°", # Declination at peak - "peak_solar_longitude": 283.16, # Solar longitude at peak - "activity_start": 283.16 - 10, # Activity period - "activity_end": 283.16 + 10, - "rate_max": 120, - "rate_min": 50, - "parent_body": "2003 EH1", - "visibility": "Northern Hemisphere", - "description": "The year kicks off with the Quadrantids, known for their brief but intense peak lasting only about 4 hours.", - "velocity_kms": 41, - }, - "Lyrids": { - "name": "Lyrids", - "radiant_ra": "18h04m", - "radiant_dec": "+32.32°", - "peak_solar_longitude": 32.32, - "activity_start": 32.32 - 10, - "activity_end": 32.32 + 10, - "rate_max": 18, - "rate_min": 10, - "parent_body": "C/1861 G1 (Thatcher)", - "visibility": "Both hemispheres", - "description": "The Lyrids are one of the oldest recorded meteor showers, with observations dating back 2,700 years.", - "velocity_kms": 49, - }, - "Eta Aquariids": { - "name": "Eta Aquariids", - "radiant_ra": "22h32m", - "radiant_dec": "-1.0°", - "peak_solar_longitude": 45.5, - "activity_start": 45.5 - 15, - "activity_end": 45.5 + 15, - "rate_max": 60, - "rate_min": 30, - "parent_body": "1P/Halley", - "visibility": "Southern Hemisphere (best)", - "description": "Created by debris from Halley's Comet, these meteors are fast and often leave glowing trails.", - "velocity_kms": 66, - }, - "Perseids": { - "name": "Perseids", - "radiant_ra": "03h04m", - "radiant_dec": "+58.0°", - "peak_solar_longitude": 140.0, - "activity_start": 140.0 - 15, - "activity_end": 140.0 + 15, - "rate_max": 100, - "rate_min": 50, - "parent_body": "109P/Swift-Tuttle", - "visibility": "Northern Hemisphere", - "description": "One of the most popular meteor showers, viewing conditions vary by year based on moon phase.", - "velocity_kms": 59, - }, - "Orionids": { - "name": "Orionids", - "radiant_ra": "06h20m", - "radiant_dec": "+16.0°", - "peak_solar_longitude": 208.0, - "activity_start": 208.0 - 15, - "activity_end": 208.0 + 15, - "rate_max": 25, - "rate_min": 15, - "parent_body": "1P/Halley", - "visibility": "Both hemispheres", - "description": "Another shower created by Halley's Comet debris, known for their speed and brightness.", - "velocity_kms": 66, - }, - "Geminids": { - "name": "Geminids", - "radiant_ra": "07h28m", - "radiant_dec": "+32.0°", - "peak_solar_longitude": 262.2, - "activity_start": 262.2 - 10, - "activity_end": 262.2 + 10, - "rate_max": 120, - "rate_min": 60, - "parent_body": "3200 Phaethon", - "visibility": "Both hemispheres", - "description": "The best shower of most years with the highest rates. Unusual for being caused by an asteroid rather than a comet.", - "velocity_kms": 35, - }, - "Ursids": { - "name": "Ursids", - "radiant_ra": "14h28m", - "radiant_dec": "+75.0°", - "peak_solar_longitude": 270.7, - "activity_start": 270.7 - 5, - "activity_end": 270.7 + 5, - "rate_max": 10, - "rate_min": 5, - "parent_body": "8P/Tuttle", - "visibility": "Northern Hemisphere", - "description": "A minor shower that closes out the year, best viewed from dark locations away from city lights.", - "velocity_kms": 33, - }, -} - - -def calculate_solar_longitude_date(year: int, target_longitude: float) -> datetime: - """Calculate the date when the Sun reaches a specific longitude for a given year.""" - # Start from beginning of year - start_date = datetime(year, 1, 1) - - # Use PyEphem to calculate solar longitude - observer = ephem.Observer() - observer.lat = "0" # Equator - observer.lon = "0" # Greenwich - observer.date = start_date - - sun = ephem.Sun(observer) - - # Search for the date when solar longitude matches target - # Solar longitude 0° = Spring Equinox (around March 20) - # We need to find when sun reaches the target longitude - - # Approximate: start search from reasonable date based on longitude - if target_longitude < 90: # Spring (Mar-Jun) - search_start = datetime(year, 3, 1) - elif target_longitude < 180: # Summer (Jun-Sep) - search_start = datetime(year, 6, 1) - elif target_longitude < 270: # Fall (Sep-Dec) - search_start = datetime(year, 9, 1) - else: # Winter (Dec-Mar) - search_start = datetime(year, 12, 1) - - observer.date = search_start - - # Search within a reasonable range (±60 days) - for day_offset in range(-60, 61): - test_date = search_start + timedelta(days=day_offset) - observer.date = test_date - sun.compute(observer) - - # Convert ecliptic longitude to degrees - sun_longitude = float(sun.hlon) * 180 / ephem.pi - - # Check if we're close to target longitude (within 0.5 degrees) - if abs(sun_longitude - target_longitude) < 0.5: - return test_date - - # Fallback: return approximation based on solar longitude - # Rough approximation: solar longitude increases ~1° per day - days_from_equinox = target_longitude - equinox_date = datetime(year, 3, 20) # Approximate spring equinox - return equinox_date + timedelta(days=days_from_equinox) - - -def calculate_moon_phase(date_obj: datetime) -> tuple[float, str]: - """Calculate moon phase for a given date.""" - observer = ephem.Observer() - observer.date = date_obj - - moon = ephem.Moon(observer) - moon.compute(observer) - - # Moon phase (0 = new moon, 0.5 = full moon, 1 = new moon again) - phase = moon.phase / 100.0 - - # Determine moon phase name and viewing quality - if phase < 0.1: - phase_name = "New Moon" - viewing_quality = "excellent" - elif phase < 0.3: - phase_name = "Waxing Crescent" - viewing_quality = "good" - elif phase < 0.7: - phase_name = "First Quarter" if phase < 0.5 else "Waxing Gibbous" - viewing_quality = "moderate" - elif phase < 0.9: - phase_name = "Full Moon" - viewing_quality = "poor" - else: - phase_name = "Waning Crescent" - viewing_quality = "good" - - return phase, f"{phase_name} ({viewing_quality} viewing)" - - -def calculate_meteor_shower_data(year: int) -> list[MeteorShower]: - """Calculate meteor shower data for a given year using astronomical calculations.""" - meteor_data = [] - - for shower_id, shower_info in METEOR_SHOWERS.items(): - # Calculate peak date based on solar longitude - peak_longitude = shower_info["peak_solar_longitude"] - assert isinstance(peak_longitude, (int, float)) - peak_date = calculate_solar_longitude_date(year, peak_longitude) - - # Calculate activity period - start_longitude = shower_info["activity_start"] - assert isinstance(start_longitude, (int, float)) - activity_start = calculate_solar_longitude_date(year, start_longitude) - - end_longitude = shower_info["activity_end"] - assert isinstance(end_longitude, (int, float)) - activity_end = calculate_solar_longitude_date(year, end_longitude) - - # Calculate moon phase at peak - moon_illumination, moon_phase_desc = calculate_moon_phase(peak_date) - - # Format dates - peak_formatted = peak_date.strftime("%B %d") - if peak_date.day != (peak_date + timedelta(days=1)).day: - peak_formatted += f"-{(peak_date + timedelta(days=1)).strftime('%d')}" - - active_formatted = ( - f"{activity_start.strftime('%B %d')} - {activity_end.strftime('%B %d')}" - ) - - # Determine viewing quality based on moon phase - viewing_quality = ( - "excellent" - if moon_illumination < 0.3 - else ( - "good" - if moon_illumination < 0.7 - else "moderate" if moon_illumination < 0.9 else "poor" - ) - ) - - meteor_shower = { - "name": shower_info["name"], - "peak": peak_formatted, - "active": active_formatted, - "rate": f"{shower_info['rate_min']}-{shower_info['rate_max']} meteors per hour", - "radiant": str(shower_info["radiant_ra"]).split("h")[0] - + "h " - + str(shower_info["radiant_dec"]), - "moon_phase": moon_phase_desc, - "visibility": shower_info["visibility"], - "description": shower_info["description"], - "peak_date": peak_date.strftime("%Y-%m-%d"), - "start_date": activity_start.strftime("%Y-%m-%d"), - "end_date": activity_end.strftime("%Y-%m-%d"), - "rate_min": shower_info["rate_min"], - "rate_max": shower_info["rate_max"], - "moon_illumination": moon_illumination, - "viewing_quality": viewing_quality, - "parent_body": shower_info["parent_body"], - "velocity_kms": shower_info["velocity_kms"], - } - - meteor_data.append(meteor_shower) - - # Sort by peak date - meteor_data.sort(key=lambda x: datetime.strptime(str(x["peak_date"]), "%Y-%m-%d")) - - return meteor_data - - -def get_meteor_data(year: int | None = None) -> list[MeteorShower]: - """Get meteor shower data for a specific year using astronomical calculations.""" - if year is None: - year = datetime.now().year - return calculate_meteor_shower_data(year) diff --git a/agenda/n_somerset_waste.py b/agenda/n_somerset_waste.py deleted file mode 100644 index 2d9cddd..0000000 --- a/agenda/n_somerset_waste.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Waste collection schedules.""" - -import os -import re -from collections import defaultdict -from datetime import date, datetime, time, timedelta - -import httpx -import lxml.html - -from . import uk_time -from .event import Event -from .utils import make_waste_dir - -ttl_hours = 12 - - -async def get_html( - data_dir: str, postcode: str, uprn: str, force_cache: bool = False -) -> str: - """Get waste schedule.""" - now = datetime.now() - waste_dir = os.path.join(data_dir, "waste") - - make_waste_dir(data_dir) - - existing_data = os.listdir(waste_dir) - existing = [f for f in existing_data if f.endswith(".html")] - if existing: - recent_filename = max(existing) - recent = datetime.strptime(recent_filename, "%Y-%m-%d_%H:%M.html") - delta = now - recent - - if existing and (force_cache or delta < timedelta(hours=ttl_hours)): - return open(os.path.join(waste_dir, recent_filename)).read() - - now_str = now.strftime("%Y-%m-%d_%H:%M") - filename = f"{waste_dir}/{now_str}.html" - - forms_base_url = "https://forms.n-somerset.gov.uk" - url = "https://forms.n-somerset.gov.uk/Waste/CollectionSchedule" - async with httpx.AsyncClient() as client: - r = await client.post( - url, - data={ - "PreviousHouse": "", - "PreviousPostcode": "-", - "Postcode": postcode, - "SelectedUprn": uprn, - }, - ) - form_post_html = r.text - pattern = r'

Object moved to here<\/a>\.<\/h2>' - m = re.search(pattern, form_post_html) - if m: - r = await client.get(forms_base_url + m.group(1)) - html = r.text - open(filename, "w").write(html) - return html - - -def parse_waste_schedule_date(day_and_month: str) -> date: - """Parse waste schedule date.""" - today = date.today() - fmt = "%A %d %B %Y" - d = datetime.strptime(f"{day_and_month} {today.year}", fmt).date() - if d < today: - d = datetime.strptime(f"{day_and_month} {today.year + 1}", fmt).date() - return d - - -def parse(root: lxml.html.HtmlElement) -> list[Event]: - """Parse waste schedule.""" - tbody = root.find(".//table/tbody") - assert tbody is not None - by_date = defaultdict(list) - for e_service, e_next_date, e_following in tbody: - assert e_service.text and e_next_date.text and e_following.text - service = e_service.text - next_date = parse_waste_schedule_date(e_next_date.text) - following_date = parse_waste_schedule_date(e_following.text) - - by_date[next_date].append(service) - by_date[following_date].append(service) - - return [ - Event( - name="waste_schedule", - date=uk_time(d, time(6, 30)), - title="Backwell: " + ", ".join(services), - ) - for d, services in by_date.items() - ] diff --git a/agenda/schengen.py b/agenda/schengen.py deleted file mode 100644 index 7f1d355..0000000 --- a/agenda/schengen.py +++ /dev/null @@ -1,299 +0,0 @@ -"""Schengen area rolling time calculator for travel tracking.""" - -from datetime import date, datetime, timedelta - -from .types import SchengenCalculation, SchengenStay, StrDict -from .utils import depart_datetime - -# Schengen Area countries as of 2025 -SCHENGEN_COUNTRIES = { - # EU countries in Schengen - "at", # Austria - "be", # Belgium - "bg", # Bulgaria (joined January 2025) - "hr", # Croatia - "cz", # Czech Republic - "dk", # Denmark - "ee", # Estonia - "fi", # Finland - "fr", # France - "de", # Germany - "gr", # Greece - "hu", # Hungary - "it", # Italy - "lv", # Latvia - "lt", # Lithuania - "lu", # Luxembourg - "mt", # Malta - "nl", # Netherlands - "pl", # Poland - "pt", # Portugal - "ro", # Romania (joined January 2025) - "sk", # Slovakia - "si", # Slovenia - "es", # Spain - "se", # Sweden - # Non-EU countries in Schengen - "is", # Iceland - "li", # Liechtenstein - "no", # Norway - "ch", # Switzerland -} - - -def is_schengen_country(country_code: str) -> bool: - """Check if a country is in the Schengen area.""" - if not country_code or not isinstance(country_code, str): - return False - return country_code.lower() in SCHENGEN_COUNTRIES - - -def extract_schengen_stays_from_travel( - travel_items: list[StrDict], -) -> list[SchengenStay]: - """Extract Schengen stays from travel items.""" - stays: list[SchengenStay] = [] - current_location = None - entry_date = None - - # Handle empty travel items - if not travel_items: - return stays - - # Sort travel items by departure date, filtering out items without depart date - sorted_items = sorted( - [item for item in travel_items if item.get("depart")], - key=lambda x: depart_datetime(x), - ) - - for item in sorted_items: - from_country = None - to_country = None - travel_date = item.get("depart") - - if not travel_date: - continue - - # Extract travel date - if isinstance(travel_date, datetime): - travel_date = travel_date.date() - elif not isinstance(travel_date, date): - # Skip items with invalid travel dates - continue - - # Determine origin and destination countries - if item.get("type") == "flight": - from_airport = item.get("from_airport", {}) - to_airport = item.get("to_airport", {}) - from_country = from_airport.get("country") - to_country = to_airport.get("country") - elif item.get("type") == "train": - from_station = item.get("from_station", {}) - to_station = item.get("to_station", {}) - from_country = from_station.get("country") - to_country = to_station.get("country") - elif item.get("type") == "ferry": - from_terminal = item.get("from_terminal", {}) - to_terminal = item.get("to_terminal", {}) - from_country = from_terminal.get("country") - to_country = to_terminal.get("country") - - # Handle entering/exiting Schengen - if current_location and is_schengen_country(current_location): - # Currently in Schengen - if to_country and not is_schengen_country(to_country): - # Exiting Schengen - use departure date - if entry_date: - stays.append( - SchengenStay( - entry_date=entry_date, - exit_date=travel_date, - country=current_location, - days=0, # Will be calculated in __post_init__ - ) - ) - entry_date = None - else: - # Currently outside Schengen - if to_country and is_schengen_country(to_country): - # Entering Schengen - use arrival date for long-haul flights - arrive_date = item.get("arrive") - assert arrive_date - if isinstance(arrive_date, datetime): - arrive_date = arrive_date.date() - assert isinstance(arrive_date, date) - entry_date = arrive_date - - current_location = to_country - - # Handle case where still in Schengen - if current_location and is_schengen_country(current_location) and entry_date: - stays.append( - SchengenStay( - entry_date=entry_date, - exit_date=None, # Still in Schengen - country=current_location, - days=0, # Will be calculated in __post_init__ - ) - ) - - return stays - - -def calculate_schengen_time( - travel_items: list[StrDict], calculation_date: date | None = None -) -> SchengenCalculation: - """ - Calculate Schengen rolling time compliance. - - Args: - travel_items: List of travel items from the trip system - calculation_date: Date to calculate from (defaults to today) - - Returns: - SchengenCalculation with compliance status and details - """ - if calculation_date is None: - calculation_date = date.today() - - # Extract Schengen stays from travel data - stays = extract_schengen_stays_from_travel(travel_items) - - # Calculate 180-day window (ending on calculation_date) - window_start = calculation_date - timedelta(days=179) - window_end = calculation_date - - # Find stays that overlap with the 180-day window - relevant_stays = [] - total_days = 0 - - for stay in stays: - # Check if stay overlaps with our 180-day window - stay_start = stay.entry_date - stay_end = stay.exit_date or calculation_date - - if stay_end >= window_start and stay_start <= window_end: - # Calculate overlap with window - overlap_start = max(stay_start, window_start) - overlap_end = min(stay_end, window_end) - overlap_days = (overlap_end - overlap_start).days + 1 - - if overlap_days > 0: - # Create a new stay object for the overlapping period - overlapping_stay = SchengenStay( - entry_date=overlap_start, - exit_date=overlap_end if overlap_end != calculation_date else None, - country=stay.country, - days=overlap_days, - ) - relevant_stays.append(overlapping_stay) - total_days += overlap_days - - # Calculate compliance - is_compliant = total_days <= 90 - days_remaining = max(0, 90 - total_days) - - # Calculate next reset date (when earliest stay in window expires) - next_reset_date = None - if relevant_stays: - earliest_stay = min(relevant_stays, key=lambda s: s.entry_date) - next_reset_date = earliest_stay.entry_date + timedelta(days=180) - - return SchengenCalculation( - total_days_used=total_days, - days_remaining=days_remaining, - is_compliant=is_compliant, - current_180_day_period=(window_start, window_end), - stays_in_period=relevant_stays, - next_reset_date=next_reset_date, - ) - - -def format_schengen_report(calculation: SchengenCalculation) -> str: - """Format a human-readable Schengen compliance report.""" - report = [] - - # Header - report.append("=== SCHENGEN AREA COMPLIANCE REPORT ===") - report.append( - f"Calculation period: {calculation.current_180_day_period[0]} to {calculation.current_180_day_period[1]}" - ) - report.append("") - - # Summary - status = "✅ COMPLIANT" if calculation.is_compliant else "❌ NON-COMPLIANT" - report.append(f"Status: {status}") - report.append(f"Days used: {calculation.total_days_used}/90") - - if calculation.is_compliant: - report.append(f"Days remaining: {calculation.days_remaining}") - else: - report.append(f"Days over limit: {calculation.days_over_limit}") - - if calculation.next_reset_date: - report.append(f"Next reset date: {calculation.next_reset_date}") - - report.append("") - - # Detailed stays - if calculation.stays_in_period: - report.append("Stays in current 180-day period:") - for stay in calculation.stays_in_period: - exit_str = ( - stay.exit_date.strftime("%Y-%m-%d") if stay.exit_date else "ongoing" - ) - report.append( - f" • {stay.entry_date.strftime('%Y-%m-%d')} to {exit_str} " - f"({stay.country.upper()}): {stay.days} days" - ) - else: - report.append("No Schengen stays in current 180-day period.") - - return "\n".join(report) - - -def get_schengen_countries_list() -> list[str]: - """Get a list of all Schengen area country codes.""" - return sorted(list(SCHENGEN_COUNTRIES)) - - -def predict_future_compliance( - travel_items: list[StrDict], - future_travel: list[tuple[date, date, str]], # (entry_date, exit_date, country) -) -> list[SchengenCalculation]: - """ - Predict future Schengen compliance with planned travel. - - Args: - travel_items: Existing travel history - future_travel: List of planned trips as (entry_date, exit_date, country) - - Returns: - List of SchengenCalculation objects for each planned trip - """ - predictions = [] - - # Create mock travel items for future trips - extended_travel = travel_items.copy() - - for entry_date, exit_date, country in future_travel: - # Add entry - extended_travel.append( - {"type": "flight", "depart": entry_date, "to_airport": {"country": country}} - ) - - # Add exit - extended_travel.append( - { - "type": "flight", - "depart": exit_date, - "from_airport": {"country": country}, - "to_airport": {"country": "gb"}, # Assuming return to UK - } - ) - - # Calculate compliance at the exit date - calculation = calculate_schengen_time(extended_travel, exit_date) - predictions.append(calculation) - - return predictions diff --git a/agenda/stats.py b/agenda/stats.py deleted file mode 100644 index 10f8981..0000000 --- a/agenda/stats.py +++ /dev/null @@ -1,188 +0,0 @@ -"""Trip statistic functions.""" - -from collections import defaultdict -import typing -from typing import TYPE_CHECKING, Counter, Mapping - -import agenda - -if TYPE_CHECKING: - from agenda.types import Trip - -StrDict = dict[str, typing.Any] - - -def travel_legs(trip: Trip, stats: StrDict) -> None: - """Calculate stats for travel legs.""" - for leg in trip.travel: - stats.setdefault("co2_kg", 0) - stats.setdefault("co2_by_transport_type", {}) - if "co2_kg" in leg: - stats["co2_kg"] += leg["co2_kg"] - transport_type = leg["type"] - stats["co2_by_transport_type"].setdefault(transport_type, 0) - stats["co2_by_transport_type"][transport_type] += leg["co2_kg"] - if leg["type"] == "flight": - from agenda.types import airport_label - - stats.setdefault("flight_count", 0) - stats.setdefault("airlines", Counter()) - stats.setdefault("airports", Counter()) - stats["flight_count"] += 1 - stats["airlines"][leg["airline_detail"]["name"]] += 1 - for field in ("from_airport", "to_airport"): - airport = leg.get(field) - if airport: - country = agenda.get_country(airport.get("country")) - label = airport_label(airport) - display = f"{country.flag} {label}" if country else label - stats["airports"][display] += 1 - if leg["type"] == "train": - stats.setdefault("train_count", 0) - stats["train_count"] += 1 - stats.setdefault("stations", Counter()) - train_legs = leg.get("legs", []) - if train_legs: - for train_leg in train_legs: - for field in ("from_station", "to_station"): - station = train_leg.get(field) - if station: - country = agenda.get_country(station.get("country")) - label = station["name"] - display = f"{country.flag} {label}" if country else label - stats["stations"][display] += 1 - else: - for field in ("from_station", "to_station"): - station = leg.get(field) - if station: - country = agenda.get_country(station.get("country")) - label = station["name"] - display = f"{country.flag} {label}" if country else label - stats["stations"][display] += 1 - - -def conferences(trip: Trip, yearly_stats: Mapping[int, StrDict]) -> None: - """Calculate conference stats.""" - for c in trip.conferences: - yearly_stats[c["start"].year].setdefault("conferences", 0) - yearly_stats[c["start"].year]["conferences"] += 1 - - -def calculate_overall_stats(yearly_stats: dict[int, StrDict]) -> StrDict: - """Aggregate yearly stats into overall stats for airlines, airports, stations.""" - overall: StrDict = { - "airlines": Counter(), - "airports": Counter(), - "stations": Counter(), - "flight_count": 0, - "train_count": 0, - "co2_kg": 0.0, - "co2_by_transport_type": {}, - } - - for year_stats in yearly_stats.values(): - if "airlines" in year_stats: - overall["airlines"] += year_stats["airlines"] - if "airports" in year_stats: - overall["airports"] += year_stats["airports"] - if "stations" in year_stats: - overall["stations"] += year_stats["stations"] - overall["flight_count"] += year_stats.get("flight_count", 0) - overall["train_count"] += year_stats.get("train_count", 0) - overall["co2_kg"] += year_stats.get("co2_kg", 0) - for transport_type, co2_kg in year_stats.get( - "co2_by_transport_type", {} - ).items(): - overall["co2_by_transport_type"].setdefault(transport_type, 0.0) - overall["co2_by_transport_type"][transport_type] += co2_kg - - return overall - - -def calculate_yearly_stats( - trips: list[Trip], previously_visited: set[str] | None = None -) -> dict[int, StrDict]: - """Calculate total distance and distance by transport type grouped by year.""" - yearly_stats: defaultdict[int, StrDict] = defaultdict(dict) - first_visit_year: dict[str, int] = {} - excluded_new: set[str] = previously_visited or set() - - for trip in trips: - year = trip.start.year - for country in trip.countries: - if country.alpha_2 == "GB": - continue - alpha_2 = country.alpha_2 - if alpha_2 not in first_visit_year or year < first_visit_year[alpha_2]: - first_visit_year[alpha_2] = year - - for trip in trips: - year = trip.start.year - dist = trip.total_distance() - yearly_stats[year].setdefault("count", 0) - yearly_stats[year]["count"] += 1 - - conferences(trip, yearly_stats) - - if dist: - yearly_stats[year]["total_distance"] = ( - yearly_stats[year].get("total_distance", 0) + trip.total_distance() - ) - - for transport_type, distance in trip.distances_by_transport_type(): - yearly_stats[year].setdefault("distances_by_transport_type", {}) - yearly_stats[year]["distances_by_transport_type"][transport_type] = ( - yearly_stats[year]["distances_by_transport_type"].get(transport_type, 0) - + distance - ) - - for country in trip.countries: - if country.alpha_2 == "GB": - continue - yearly_stats[year].setdefault("countries", set()) - yearly_stats[year]["countries"].add(country) - if ( - first_visit_year.get(country.alpha_2) == year - and country.alpha_2 not in excluded_new - ): - yearly_stats[year].setdefault("new_countries", set()) - yearly_stats[year]["new_countries"].add(country) - - travel_legs(trip, yearly_stats[year]) - - return dict(yearly_stats) - - -def calc_total_distance(trips: list[Trip]) -> float: - """Total distance for trips.""" - total = 0.0 - for item in trips: - if dist := item.total_distance(): - total += dist - - return total - - -def calc_total_co2_kg(trips: list[Trip]) -> float: - """Total CO₂ for trips.""" - return sum(item.total_co2_kg() or 0.0 for item in trips) - - -def sum_distances_by_transport_type(trips: list[Trip]) -> list[tuple[str, float]]: - """Sum distances by transport type.""" - distances_by_transport_type: defaultdict[str, float] = defaultdict(float) - for trip in trips: - for transport_type, dist in trip.distances_by_transport_type(): - distances_by_transport_type[transport_type] += dist - - return list(distances_by_transport_type.items()) - - -def sum_co2_by_transport_type(trips: list[Trip]) -> list[tuple[str, float]]: - """Sum CO₂ by transport type.""" - co2_by_transport_type: defaultdict[str, float] = defaultdict(float) - for trip in trips: - for transport_type, co2_kg in trip.co2_by_transport_type(): - co2_by_transport_type[transport_type] += co2_kg - - return list(co2_by_transport_type.items()) diff --git a/agenda/stock_market.py b/agenda/stock_market.py index 60b8c7a..60fb5a4 100644 --- a/agenda/stock_market.py +++ b/agenda/stock_market.py @@ -3,14 +3,26 @@ from datetime import timedelta, timezone import dateutil.tz -import exchange_calendars # type: ignore +import exchange_calendars import pandas -from . import utils - here = dateutil.tz.tzlocal() +def timedelta_display(delta: timedelta) -> str: + """Format timedelta as a human readable string.""" + total_seconds = int(delta.total_seconds()) + days, remainder = divmod(total_seconds, 24 * 60 * 60) + hours, remainder = divmod(remainder, 60 * 60) + mins, secs = divmod(remainder, 60) + + return " ".join( + f"{v:>3} {label}" + for v, label in ((days, "days"), (hours, "hrs"), (mins, "mins")) + if v + ) + + def open_and_close() -> list[str]: """Stock markets open and close times.""" # The trading calendars code is slow, maybe there is a faster way to do this @@ -28,11 +40,11 @@ def open_and_close() -> list[str]: if cal.is_open_on_minute(now_local): next_close = cal.next_close(now).tz_convert(here) next_close = next_close.replace(minute=round(next_close.minute, -1)) - delta_close = utils.timedelta_display(next_close - now_local) + delta_close = timedelta_display(next_close - now_local) prev_open = cal.previous_open(now).tz_convert(here) prev_open = prev_open.replace(minute=round(prev_open.minute, -1)) - delta_open = utils.timedelta_display(now_local - prev_open) + delta_open = timedelta_display(now_local - prev_open) msg = ( f"{label:>6} market opened {delta_open} ago, " @@ -42,7 +54,7 @@ def open_and_close() -> list[str]: ts = cal.next_open(now) ts = ts.replace(minute=round(ts.minute, -1)) ts = ts.tz_convert(here) - delta = utils.timedelta_display(ts - now_local) + delta = timedelta_display(ts - now_local) msg = f"{label:>6} market opens in {delta}" + ( f" ({ts:%H:%M})" if (ts - now_local) < timedelta(days=1) else "" ) diff --git a/agenda/subscription.py b/agenda/subscription.py index a9f8ccc..9c2036c 100644 --- a/agenda/subscription.py +++ b/agenda/subscription.py @@ -2,7 +2,7 @@ import yaml -from .event import Event +from .types import Event def get_events(filepath: str) -> list[Event]: diff --git a/agenda/sun.py b/agenda/sun.py index 17995bb..72adc69 100644 --- a/agenda/sun.py +++ b/agenda/sun.py @@ -3,7 +3,7 @@ import typing from datetime import datetime -import ephem # type: ignore +import ephem def bristol() -> ephem.Observer: diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index 7bcb57d..6ece23c 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -1,302 +1,37 @@ """Get details of upcoming space launches.""" -import fcntl import json -import re -import time import os import typing -import tempfile from datetime import datetime -import requests - -from .types import StrDict -from .utils import filename_timestamp +import httpx Launch = dict[str, typing.Any] Summary = dict[str, typing.Any] -ttl = 60 * 60 * 2 # two hours -LIMIT = 500 -ACTIVE_CREWED_FLIGHTS_CACHE_FILE = "active_crewed_flights.json" - - -def write_json_cache(filename: str, payload: StrDict) -> None: - """Publish a complete cache atomically, preserving old data on write failure.""" - contents = json.dumps(payload) - temporary: str | None = None - try: - with tempfile.NamedTemporaryFile( - mode="w", dir=os.path.dirname(filename), prefix=".cache-", delete=False - ) as cache: - temporary = cache.name - cache.write(contents) - cache.flush() - # The updater and web server run as different users. - os.fchmod(cache.fileno(), 0o644) - os.fsync(cache.fileno()) - os.replace(temporary, filename) - finally: - if temporary is not None and os.path.exists(temporary): - os.unlink(temporary) - - -def read_json_cache(filename: str) -> StrDict | None: - """Skip empty, unreadable, or damaged cache files.""" - try: - with open(filename) as cache: - if os.fstat(cache.fileno()).st_size == 0: - return None - data = json.load(cache) - except (OSError, ValueError): - return None - return data if isinstance(data, dict) else None - - -def latest_launches_cache(rocket_dir: str) -> tuple[datetime, StrDict] | None: - """Find the newest usable timestamped launch cache.""" - existing = sorted( - ( - entry - for f in os.listdir(rocket_dir) - if (entry := filename_timestamp(f, "json")) is not None - ), - reverse=True, - ) - for updated, filename in existing: - data = read_json_cache(os.path.join(rocket_dir, filename)) - if data is not None and isinstance(data.get("results"), list): - return updated, data - return None - - -class RateLimitDeferred(Exception): - """The shared API budget or server cooldown requires waiting.""" - - -def api_get( - rocket_dir: str, url: str, params: dict[str, str | int] | None = None -) -> requests.Response: - """Share a conservative rolling request budget across updater processes.""" - # Keep three calls spare for other clients sharing the public IP. - filename = os.path.join(rocket_dir, "api_budget.json") - with open(filename + ".lock", "a") as lock: - fcntl.flock(lock, fcntl.LOCK_EX) - now = time.time() - state = read_json_cache(filename) or {} - calls = [float(t) for t in state.get("calls", []) if float(t) > now - 3600] - retry_at = float(state.get("retry_at", 0)) - if now < retry_at or len(calls) >= 12: - raise RateLimitDeferred() - calls.append(now) - # Count attempts even if the connection fails. - write_json_cache(filename, {"calls": calls, "retry_at": retry_at}) - response = requests.get(url, params=params, timeout=30) - if response.status_code == 429: - delay = 3600.0 - try: - delay = float(response.headers["Retry-After"]) - except (KeyError, ValueError): - match = re.search(r"available in (\d+) seconds", response.text) - if match: - delay = float(match[1]) - write_json_cache(filename, {"calls": calls, "retry_at": now + delay + 1}) - raise RateLimitDeferred() - return response - - -def next_launch_api_data(rocket_dir: str, limit: int = LIMIT) -> StrDict | None: +async def next_launch_api(rocket_dir: str, limit: int = 200) -> list[Launch]: """Get the next upcoming launches from the API.""" now = datetime.now() filename = os.path.join(rocket_dir, now.strftime("%Y-%m-%d_%H:%M:%S.json")) url = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/" params: dict[str, str | int] = {"limit": limit} - r = api_get(rocket_dir, url, params) - try: - data: StrDict = r.json() - except requests.exceptions.JSONDecodeError: - return None - # Only persist valid launch payloads; rate-limit / error responses must not - # overwrite the cache or they become the "most recent" file. - if isinstance(data.get("results"), list): - write_json_cache(filename, data) - return data - - -def next_launch_api(rocket_dir: str, limit: int = LIMIT) -> list[Summary] | None: - """Get the next upcoming launches from the API.""" - data = next_launch_api_data(rocket_dir, limit) - if not data: - return None + async with httpx.AsyncClient() as client: + r = await client.get(url, params=params) + open(filename, "w").write(r.text) + data = r.json() return [summarize_launch(launch) for launch in data["results"]] -def parse_api_datetime(value: typing.Any) -> datetime | None: - """Parse API datetime strings into datetime objects.""" - if not isinstance(value, str): - return None +def filename_timestamp(filename: str) -> tuple[datetime, str] | None: + """Get datetime from filename.""" try: - return datetime.fromisoformat(value.replace("Z", "+00:00")) + ts = datetime.strptime(filename, "%Y-%m-%d_%H:%M:%S.json") except ValueError: return None - - -def is_crewed_spaceflight(flight: Launch) -> bool: - """Return True when a spaceflight is crewed/human-rated.""" - spacecraft_config = get_nested(flight, ["spacecraft", "spacecraft_config"]) - if isinstance(spacecraft_config, dict) and spacecraft_config.get("human_rated"): - return True - - mission_type = get_nested(flight, ["launch", "mission", "type"]) - if isinstance(mission_type, str) and "human" in mission_type.lower(): - return True - - return False - - -def is_active_crewed_spaceflight(flight: Launch, now: datetime) -> bool: - """Return True when a crewed spaceflight is active (mission not yet ended).""" - launch = flight.get("launch") - if not isinstance(launch, dict): - return False - - if not is_crewed_spaceflight(flight): - return False - - launch_net = parse_api_datetime(launch.get("net")) - if launch_net and launch_net > now: - return False - - mission_end = parse_api_datetime(flight.get("mission_end")) - if mission_end and mission_end <= now: - return False - - spacecraft_in_space = get_nested(flight, ["spacecraft", "in_space"]) - if spacecraft_in_space is True: - return True - - if mission_end is None: - landing_success = get_nested(flight, ["landing", "success"]) - if landing_success is True: - return False - return True - - return True - - -def active_crewed_flights_api( - rocket_dir: str, limit: int = LIMIT -) -> list[Summary] | None: - """ - Get active crewed spaceflights from the SpaceDevs API. - - The API does not reliably expose a direct filter for active flights, so this - paginates through results and applies local filtering. - """ - now = datetime.now().astimezone() - url = "https://ll.thespacedevs.com/2.2.0/spacecraft/flight/" - params: dict[str, str | int] = {"limit": limit} - launches: list[Summary] = [] - seen_slugs: set[str] = set() - page = 0 - max_pages = 20 - - while url and page < max_pages: - r = api_get(rocket_dir, url, params if page == 0 else None) - if not r.ok: - return None - try: - data: StrDict = r.json() - except requests.exceptions.JSONDecodeError: - return None - - results = data.get("results") - if not isinstance(results, list): - return None - - for flight in results: - if not isinstance(flight, dict): - continue - if not is_active_crewed_spaceflight(flight, now): - continue - - launch = flight.get("launch") - if not isinstance(launch, dict): - continue - - launch_summary = summarize_launch(typing.cast(Launch, launch)) - slug = launch_summary.get("slug") - if not isinstance(slug, str): - continue - if slug in seen_slugs: - continue - seen_slugs.add(slug) - launches.append(launch_summary) - - next_url = data.get("next") - url = next_url if isinstance(next_url, str) else "" - params = {} - page += 1 - - # Never replace the cache with an incomplete paginated result. - return None if url else launches - - -def get_active_crewed_flights_cache_filename(rocket_dir: str) -> str: - """Path of the active crewed flights cache file.""" - return os.path.join(rocket_dir, ACTIVE_CREWED_FLIGHTS_CACHE_FILE) - - -def load_active_crewed_flights_cache( - rocket_dir: str, -) -> tuple[datetime, list[Summary]] | None: - """Load active crewed flights cache file.""" - filename = get_active_crewed_flights_cache_filename(rocket_dir) - cache_data = read_json_cache(filename) - if cache_data is None: - return None - - updated_str = cache_data.get("updated") - updated = parse_api_datetime(updated_str) - results = cache_data.get("results") - if not updated or not isinstance(results, list): - return None - - summary_results: list[Summary] = [r for r in results if isinstance(r, dict)] - return (updated.replace(tzinfo=None), summary_results) - - -def write_active_crewed_flights_cache(rocket_dir: str, launches: list[Summary]) -> None: - """Write active crewed flights cache file.""" - filename = get_active_crewed_flights_cache_filename(rocket_dir) - payload: StrDict = { - "updated": datetime.now().isoformat(), - "results": launches, - } - write_json_cache(filename, payload) - - -def get_active_crewed_flights( - rocket_dir: str, refresh: bool = False -) -> list[Summary] | None: - """Get active crewed flights with cache and API fallback.""" - now = datetime.now() - cached = load_active_crewed_flights_cache(rocket_dir) - if cached and not refresh and (now - cached[0]).total_seconds() <= ttl: - return cached[1] - - try: - active_flights = active_crewed_flights_api(rocket_dir) - if active_flights is not None: - write_active_crewed_flights_cache(rocket_dir, active_flights) - return active_flights - except Exception: - pass - - return cached[1] if cached else [] + return (ts, filename) def format_time(time_str: str, net_precision: str) -> tuple[str, str | None]: @@ -324,8 +59,6 @@ def format_time(time_str: str, net_precision: str) -> tuple[str, str | None]: include_time = True case _ if net_precision and net_precision.startswith("Quarter "): time_format = f"Q{net_precision[-1]} %Y" - case _ if net_precision and net_precision.startswith("Year Half "): - time_format = f"H{net_precision[-1]} %Y" case _: time_format = None @@ -383,7 +116,6 @@ def summarize_launch(launch: Launch) -> Summary: return { "name": launch.get("name"), - "slug": launch["slug"], "status": launch.get("status"), "net": launch.get("net"), "net_precision": net_precision, @@ -394,7 +126,7 @@ def summarize_launch(launch: Launch) -> Summary: "launch_provider": launch_provider, "launch_provider_abbrev": launch_provider_abbrev, "launch_provider_type": get_nested(launch, ["launch_service_provider", "type"]), - "rocket": launch["rocket"]["configuration"], + "rocket": launch["rocket"]["configuration"]["full_name"], "mission": launch.get("mission"), "mission_name": get_nested(launch, ["mission", "name"]), "pad_name": launch["pad"]["name"], @@ -402,215 +134,24 @@ def summarize_launch(launch: Launch) -> Summary: "location": launch["pad"]["location"]["name"], "country_code": launch["pad"]["country_code"], "orbit": get_nested(launch, ["mission", "orbit"]), - "probability": launch["probability"], - "weather_concerns": launch["weather_concerns"], - "image": launch.get("image"), } -def is_launches_cache_fresh(rocket_dir: str) -> bool: - """Return True if the launches cache is younger than the TTL.""" - cached = latest_launches_cache(rocket_dir) - return cached is not None and (datetime.now() - cached[0]).total_seconds() <= ttl - - -def load_cached_launches(rocket_dir: str) -> StrDict | None: - """Read the most recent usable cache of launches.""" - cached = latest_launches_cache(rocket_dir) - return cached[1] if cached is not None else None - - -def read_cached_launches(rocket_dir: str) -> list[Summary]: - """Read cached launches.""" - data = load_cached_launches(rocket_dir) - if not data or not isinstance(data.get("results"), list): - return [] - return [summarize_launch(launch) for launch in data["results"]] - - -def get_launches( - rocket_dir: str, limit: int = LIMIT, refresh: bool = False -) -> list[Summary] | None: +async def get_launches(rocket_dir: str, limit: int = 200) -> list[Summary]: """Get rocket launches with caching.""" - if refresh or not is_launches_cache_fresh(rocket_dir): + now = datetime.now() + existing = [x for x in (filename_timestamp(f) for f in os.listdir(rocket_dir)) if x] + + existing.sort(reverse=True) + + if not existing or (now - existing[0][0]).seconds > 3600: # one hour try: - upcoming = next_launch_api(rocket_dir, limit=limit) - if upcoming is None: - raise RuntimeError("unable to fetch upcoming launches") - active_crewed = get_active_crewed_flights(rocket_dir, refresh=refresh) or [] - by_slug = { - typing.cast(str, launch["slug"]): launch - for launch in upcoming - if isinstance(launch.get("slug"), str) - } - for launch in active_crewed: - slug = launch.get("slug") - if isinstance(slug, str) and slug not in by_slug: - by_slug[slug] = launch - return sorted(by_slug.values(), key=lambda launch: str(launch.get("net"))) - except Exception: - pass # fallback to cached version + return await next_launch_api(rocket_dir, limit=limit) + except httpx.ReadTimeout: + pass - data = load_cached_launches(rocket_dir) - if not data: - return [] - upcoming = [summarize_launch(launch) for launch in data["results"]] - active_crewed = get_active_crewed_flights(rocket_dir, refresh=refresh) or [] - by_slug = { - typing.cast(str, launch["slug"]): launch - for launch in upcoming - if isinstance(launch.get("slug"), str) - } - for launch in active_crewed: - slug = launch.get("slug") - if isinstance(slug, str) and slug not in by_slug: - by_slug[slug] = launch - return sorted(by_slug.values(), key=lambda launch: str(launch.get("net"))) + f = existing[0][1] - -def format_date(dt: datetime) -> str: - """Human readable date.""" - return dt.strftime("%d %b %Y at %H:%M UTC") - - -def format_datetime_change(field_name: str, old_val: str, new_val: str) -> str: - """Format a datetime field change with proper error handling.""" - try: - old_dt = datetime.fromisoformat(old_val.replace("Z", "+00:00")) - new_dt = datetime.fromisoformat(new_val.replace("Z", "+00:00")) - return ( - f"{field_name} changed from {format_date(old_dt)} to {format_date(new_dt)}" - ) - except (ValueError, AttributeError): - return f"{field_name} changed from {old_val} to {new_val}" - - -def format_datetime_update(field_name: str, new_val: str) -> str: - """Format a datetime field update (showing only the new value).""" - try: - new_dt = datetime.fromisoformat(new_val.replace("Z", "+00:00")) - return f"{field_name}: {format_date(new_dt)}" - except (ValueError, AttributeError): - return f"{field_name}: {new_val}" - - -def format_probability_change(old_val: int, new_val: int) -> str: - """Format probability field changes.""" - if old_val is None: - return f"Launch probability set to {new_val}%" - elif new_val is None: - return "Launch probability removed" - else: - return f"Launch probability changed from {old_val}% to {new_val}%" - - -def format_launch_changes(differences: StrDict) -> str: - """Convert deepdiff output to human-readable format.""" - changes: list[str] = [] - processed_paths: set[str] = set() - - SKIP_FIELDS = { - "agency_launch_attempt_count", - "agency_launch_attempt_count_year", - "location_launch_attempt_count", - "location_launch_attempt_count_year", - "pad_launch_attempt_count", - "pad_launch_attempt_count_year", - "orbital_launch_attempt_count", - "orbital_launch_attempt_count_year", - } - - # --- 1. Handle Special Group Value Changes --- - # Process high-level, user-friendly summaries first. - values = differences.get("values_changed", {}) - if "root['status']['name']" in values: - old_val = values["root['status']['name']"]["old_value"] - new_val = values["root['status']['name']"]["new_value"] - changes.append(f"Status changed from '{old_val}' to '{new_val}'") - processed_paths.add("root['status']") - - if "root['net_precision']['name']" in values: - old_val = values["root['net_precision']['name']"]["old_value"] - new_val = values["root['net_precision']['name']"]["new_value"] - changes.append(f"Launch precision changed from '{old_val}' to '{new_val}'") - processed_paths.add("root['net_precision']") - - # --- 2. Handle Type Changes --- - # This is often more significant than a value change (e.g., probability becoming None). - if "type_changes" in differences: - for path, change in differences["type_changes"].items(): - if any(path.startswith(p) for p in processed_paths): - continue - - field = path.replace("root['", "").replace("']", "").replace("root.", "") - - if field == "probability": - # Use custom formatter only for meaningful None transitions. - if change["old_type"] is type(None) or change["new_type"] is type(None): - changes.append( - format_probability_change( - change["old_value"], change["new_value"] - ) - ) - else: # For other type changes (e.g., int to str), use the generic message. - changes.append( - f"{field.replace('_', ' ').title()} type changed " - + f"from {change['old_type'].__name__} to {change['new_type'].__name__}" - ) - else: - changes.append( - f"{field.replace('_', ' ').title()} type changed " - + f"from {change['old_type'].__name__} to {change['new_type'].__name__}" - ) - processed_paths.add(path) - - # --- 3. Handle Remaining Value Changes --- - for path, change in values.items(): - if any(path.startswith(p) for p in processed_paths): - continue - - field = path.replace("root['", "").replace("']", "").replace("root.", "") - if field in SKIP_FIELDS: - continue - - old_val = change["old_value"] - new_val = change["new_value"] - - match field: - case "net": - changes.append(format_datetime_change("Launch time", old_val, new_val)) - case "window_start": - changes.append( - format_datetime_change("Launch window start", old_val, new_val) - ) - case "window_end": - changes.append( - format_datetime_change("Launch window end", old_val, new_val) - ) - case "last_updated": - changes.append(format_datetime_update("Last updated", new_val)) - case "name": - changes.append(f"Mission name changed from '{old_val}' to '{new_val}'") - case "probability": - changes.append(format_probability_change(old_val, new_val)) - case _: - changes.append(f"{field} changed from '{old_val}' to '{new_val}'") - processed_paths.add(path) - - # --- 4. Handle Added/Removed Fields --- - if "dictionary_item_added" in differences: - for path in differences["dictionary_item_added"]: - field = path.replace("root['", "").replace("']", "").replace("root.", "") - changes.append(f"New field added: {field.replace('_', ' ').title()}") - - if "dictionary_item_removed" in differences: - for path in differences["dictionary_item_removed"]: - field = path.replace("root['", "").replace("']", "").replace("root.", "") - changes.append(f"Field removed: {field.replace('_', ' ').title()}") - - # Sort changes for deterministic output in tests - return ( - "\n".join(f"• {change}" for change in sorted(changes)) - if changes - else "No specific changes detected" - ) + filename = os.path.join(rocket_dir, f) + data = json.load(open(filename)) + return [summarize_launch(launch) for launch in data["results"]] diff --git a/agenda/travel.py b/agenda/travel.py index 440e6af..4825c9c 100644 --- a/agenda/travel.py +++ b/agenda/travel.py @@ -1,86 +1,36 @@ """Travel.""" -import decimal -import json import os import typing -import flask import yaml -from geopy.distance import geodesic # type: ignore -from .event import Event -from .types import StrDict +from .types import Event Leg = dict[str, str] TravelList = list[dict[str, typing.Any]] -RouteDistances = dict[tuple[str, str], float] - - -def coords(airport: StrDict) -> tuple[float, float]: - """Longitude / Latitude as coordinate tuples.""" - # return (airport["longitude"], airport["latitude"]) - return (airport["latitude"], airport["longitude"]) - - -def flight_distance(f: StrDict) -> float: - """Distance of flight.""" - return float(geodesic(coords(f["from_airport"]), coords(f["to_airport"])).km) - - -def route_distances_as_json(route_distances: RouteDistances) -> str: - """Format route distances as JSON string.""" - return ( - "[\n" - + ",\n".join( - " " + json.dumps([s1, s2, dist]) - for (s1, s2), dist in route_distances.items() - ) - + "\n]" - ) - def parse_yaml(travel_type: str, data_dir: str) -> TravelList: """Parse flights YAML and return list of travel.""" filepath = os.path.join(data_dir, travel_type + ".yaml") - items: TravelList = yaml.safe_load(open(filepath)) - if not all(isinstance(item, dict) for item in items): - return items - - for item in items: - price = item.get("price") - if price: - item["price"] = decimal.Decimal(price) - - return items + return typing.cast(TravelList, yaml.safe_load(open(filepath))) def get_flights(data_dir: str) -> list[Event]: """Get travel events.""" - bookings = parse_yaml("flights", data_dir) - airlines = parse_yaml("airlines", data_dir) - by_iata = {a["iata"]: a for a in airlines} - events = [] - for booking in bookings: - for item in booking["flights"]: - if not item["depart"].date(): - continue - airline = by_iata[item["airline"]] - item["airline_code"] = airline[ - "iata" if not airline.get("flight_number_prefer_icao") else "icao" - ] - - e = Event( - date=item["depart"], - end_date=item.get("arrive"), - name="transport", - title=f'✈️ {item["from"]} to {item["to"]} ({flight_number(item)})', - url=(item.get("url") if flask.g.user.is_authenticated else None), - ) - events.append(e) - return events + return [ + Event( + date=item["depart"], + end_date=item.get("arrive"), + name="transport", + title=f'✈️ {item["from"]} to {item["to"]} ({flight_number(item)})', + url=item.get("url"), + ) + for item in parse_yaml("flights", data_dir) + if item["depart"].date() + ] def get_trains(data_dir: str) -> list[Event]: @@ -93,7 +43,7 @@ def get_trains(data_dir: str) -> list[Event]: end_date=leg["arrive"], name="transport", title=f'🚆 {leg["from"]} to {leg["to"]}', - url=(item.get("url") if flask.g.user.is_authenticated else None), + url=item.get("url"), ) for leg in item["legs"] ] @@ -102,7 +52,7 @@ def get_trains(data_dir: str) -> list[Event]: def flight_number(flight: Leg) -> str: """Flight number.""" - airline_code = flight["airline_code"] + airline_code = flight["airline"] # make sure this is the airline code, not the airline name assert " " not in airline_code and not any(c.islower() for c in airline_code) @@ -112,43 +62,3 @@ def flight_number(flight: Leg) -> str: def all_events(data_dir: str) -> list[Event]: """Get all flights and rail journeys.""" return get_trains(data_dir) + get_flights(data_dir) - - -def train_leg_distance(geojson_data: StrDict) -> float: - """Calculate the total length of a LineString in kilometers from GeoJSON data.""" - # Extract coordinates - first_object = geojson_data["features"][0]["geometry"] - assert first_object["type"] in ("LineString", "MultiLineString") - - if first_object["type"] == "LineString": - coord_list = [first_object["coordinates"]] - else: - first_object["type"] == "MultiLineString" - coord_list = first_object["coordinates"] - - total_length_km = 0.0 - - for coordinates in coord_list: - total_length_km += sum( - float(geodesic(coordinates[i], coordinates[i + 1]).km) - for i in range(len(coordinates) - 1) - ) - - return total_length_km - - -def load_route_distances(data_dir: str) -> RouteDistances: - """Load cache of route distances.""" - route_distances: RouteDistances = {} - with open(os.path.join(data_dir, "route_distances.json")) as f: - for s1, s2, dist in json.load(f): - route_distances[(s1, s2)] = dist - - return route_distances - - -def add_leg_route_distance(leg: StrDict, route_distances: RouteDistances) -> None: - s1, s2 = sorted([leg["from"], leg["to"]]) - dist = route_distances.get((s1, s2)) - if dist: - leg["distance"] = dist diff --git a/agenda/trip.py b/agenda/trip.py deleted file mode 100644 index 6de8e4d..0000000 --- a/agenda/trip.py +++ /dev/null @@ -1,1355 +0,0 @@ -"""Trips.""" - -import decimal -import hashlib -import json -import os -import typing -import unicodedata -from datetime import date, datetime, time, timedelta, timezone - -import flask -import pycountry -import yaml -from geopy.distance import geodesic # type: ignore - -from agenda import conference, ical, travel, trip_schengen -from agenda.types import StrDict, Trip, TripElement -from agenda.utils import as_date, as_datetime, depart_datetime - -TRAIN_CO2_KG_PER_KM = 0.037 -COACH_CO2_KG_PER_KM = 0.027 -FERRY_CO2_KG_PER_KM = 0.02254 -BUS_CO2_KG_PER_KM = 0.1 -CAR_CO2_KG_PER_KM = 0.218 -NOON = time(12) - - -class Airline(typing.TypedDict, total=False): - """Airline.""" - - iata: str - icao: str - name: str - - -def load_flight_destination_rules( - data_dir: str, -) -> list[tuple[str, set[str]]]: - """Load flight destination rules from personal data. - - YAML schema: - - origin: BRS - airline: U2 - destinations: [AGP, ALC] - """ - filename = os.path.join(data_dir, "flight_destinations.yaml") - if not os.path.exists(filename): - return [] - - raw = yaml.safe_load(open(filename)) - if not isinstance(raw, list): - return [] - - rules: list[tuple[str, set[str]]] = [] - for item in raw: - if not isinstance(item, dict): - continue - from_iata = item.get("origin") - airline = item.get("airline") - destinations = item.get("destinations") - if ( - not isinstance(from_iata, str) - or not isinstance(airline, str) - or not isinstance(destinations, list) - ): - continue - destination_set = {d.upper() for d in destinations if isinstance(d, str)} - if destination_set: - rules.append((from_iata.upper(), destination_set)) - return rules - - -def normalize_place_name(value: str) -> str: - """Normalize place names for case/diacritics-insensitive matching.""" - normalized = unicodedata.normalize("NFKD", value) - no_marks = "".join(ch for ch in normalized if not unicodedata.combining(ch)) - return " ".join(no_marks.casefold().split()) - - -def preferred_airport_iata(stop: StrDict, airports: dict[str, StrDict]) -> str | None: - """Return preferred airport IATA for a stop from airport data.""" - location = stop.get("location") - country = stop.get("country") - if not isinstance(location, str) or not isinstance(country, str): - return None - - location_normalized = normalize_place_name(location) - country_lower = country.casefold() - match_scores: list[tuple[int, str]] = [] - for iata, airport in airports.items(): - airport_country = airport.get("country") - if ( - not isinstance(airport_country, str) - or airport_country.casefold() != country_lower - ): - continue - - score = 0 - city = airport.get("city") - if isinstance(city, str) and normalize_place_name(city) == location_normalized: - score = max(score, 3) - alt_name = airport.get("alt_name") - if ( - isinstance(alt_name, str) - and normalize_place_name(alt_name) == location_normalized - ): - score = max(score, 2) - name = airport.get("name") - if isinstance(name, str) and normalize_place_name(name) == location_normalized: - score = max(score, 1) - if score: - match_scores.append((score, iata)) - - if not match_scores: - return None - match_scores.sort(key=lambda item: (-item[0], item[1])) - return match_scores[0][1] - - -def get_unbooked_flight_origin_iata( - destination_iata: str | None, origin_rules: list[tuple[str, set[str]]] -) -> str: - """Choose origin airport for an unbooked flight.""" - if destination_iata: - for from_iata, destinations in origin_rules: - if destination_iata in destinations: - return from_iata - return "LHR" - - -UNBOOKED_RAIL_DESTINATIONS: dict[tuple[str, str], tuple[str, str]] = { - ("fr", "paris"): ("London St Pancras", "Paris Gare du Nord"), -} -EUROSTAR_LONDON_STATION = "London St Pancras" -EUROSTAR_HOME_FEEDER_ROUTE = ("Bristol Temple Meads", "London Paddington") - - -def load_station_lookup(data_dir: str) -> dict[str, StrDict]: - """Load stations keyed by station name.""" - stations = travel.parse_yaml("stations", data_dir) - return {station["name"]: station for station in stations} - - -def get_unbooked_rail_route(item: StrDict, data_dir: str) -> StrDict | None: - """Return an assumed rail route for a conference without booked travel.""" - location = item.get("location") - country = item.get("country") - if not isinstance(location, str) or not isinstance(country, str): - return None - - station_names = UNBOOKED_RAIL_DESTINATIONS.get( - (country.casefold(), normalize_place_name(location)) - ) - if station_names is None: - return None - - stations = load_station_lookup(data_dir) - from_station = stations.get(station_names[0]) - to_station = stations.get(station_names[1]) - if from_station is None or to_station is None: - return None - - key = "_".join(["train"] + sorted([from_station["name"], to_station["name"]])) - route: StrDict = {"type": "train", "key": key} - geojson_filename = from_station.get("routes", {}).get(to_station["name"]) - if geojson_filename: - route["geojson_filename"] = os.path.join("train_routes", geojson_filename) - else: - route["from"] = latlon_tuple(from_station) - route["to"] = latlon_tuple(to_station) - return route - - -def build_train_route( - train_from: StrDict, train_to: StrDict, seen_geojson: set[str] -) -> StrDict | None: - """Build a map route between two stations.""" - geojson_filename = train_from.get("routes", {}).get(train_to["name"]) - key = "_".join(["train"] + sorted([train_from["name"], train_to["name"]])) - if not geojson_filename: - return { - "type": "train", - "key": key, - "from": latlon_tuple(train_from), - "to": latlon_tuple(train_to), - } - - if geojson_filename in seen_geojson: - return None - seen_geojson.add(geojson_filename) - - return { - "type": "train", - "key": key, - "geojson_filename": os.path.join("train_routes", geojson_filename), - } - - -def train_operator_is_eurostar(item: StrDict) -> bool: - """Return True when a train item is operated by Eurostar.""" - operator = item.get("operator") - return isinstance(operator, str) and operator.casefold() == "eurostar" - - -def is_eurostar_departure_from_london(train: StrDict) -> bool: - """Return True when a train journey departs London St Pancras on Eurostar.""" - if train.get("from") == EUROSTAR_LONDON_STATION and train_operator_is_eurostar( - train - ): - return True - - for leg in train.get("legs", []): - if leg.get("from") == EUROSTAR_LONDON_STATION and train_operator_is_eurostar( - leg - ): - return True - return False - - -def get_eurostar_home_feeder_route( - trip: Trip, data_dir: str, seen_geojson: set[str] -) -> StrDict | None: - """Return the assumed Bristol to Paddington route for London Eurostar trips.""" - if not any( - t["type"] == "train" and is_eurostar_departure_from_london(t) - for t in trip.travel - ): - return None - - stations = load_station_lookup(data_dir) - from_station = stations.get(EUROSTAR_HOME_FEEDER_ROUTE[0]) - to_station = stations.get(EUROSTAR_HOME_FEEDER_ROUTE[1]) - if from_station is None or to_station is None: - return None - - return build_train_route(from_station, to_station, seen_geojson) - - -def load_travel(travel_type: str, plural: str, data_dir: str) -> list[StrDict]: - """Read flight and train journeys.""" - items: list[StrDict] = travel.parse_yaml(plural, data_dir) - for item in items: - item["type"] = travel_type - return items - - -def add_station_objects(item: StrDict, by_name: dict[str, StrDict]) -> None: - """Lookup stations and add to train or leg.""" - item["from_station"] = by_name[item["from"]] - item["to_station"] = by_name[item["to"]] - - -def load_trains( - data_dir: str, route_distances: travel.RouteDistances | None = None -) -> list[StrDict]: - """Load trains.""" - trains = load_travel("train", "trains", data_dir) - stations = travel.parse_yaml("stations", data_dir) - by_name = {station["name"]: station for station in stations} - - for train in trains: - add_station_objects(train, by_name) - for leg in train["legs"]: - add_station_objects(leg, by_name) - if route_distances: - travel.add_leg_route_distance(leg, route_distances) - - if "distance" in leg: - leg["co2_kg"] = leg["distance"] * TRAIN_CO2_KG_PER_KM - - if all("distance" in leg for leg in train["legs"]): - train["distance"] = sum(leg["distance"] for leg in train["legs"]) - train["co2_kg"] = sum(leg["co2_kg"] for leg in train["legs"]) - - return trains - - -def load_ferries( - data_dir: str, route_distances: travel.RouteDistances | None = None -) -> list[StrDict]: - """Load ferries.""" - ferries = load_travel("ferry", "ferries", data_dir) - terminals = travel.parse_yaml("ferry_terminals", data_dir) - by_name = {terminal["name"]: terminal for terminal in terminals} - - for item in ferries: - assert item["from"] in by_name and item["to"] in by_name - from_terminal, to_terminal = by_name[item["from"]], by_name[item["to"]] - item["from_terminal"] = from_terminal - item["to_terminal"] = to_terminal - - if route_distances: - travel.add_leg_route_distance(item, route_distances) - - if "distance" in item: - item["co2_kg"] = item["distance"] * FERRY_CO2_KG_PER_KM - - geojson = from_terminal["routes"].get(item["to"]) - if geojson: - item["geojson_filename"] = geojson - - return ferries - - -def load_road_transport( - travel_type: str, - plural: str, - stops_yaml: str, - data_dir: str, - co2_factor: float, - route_distances: travel.RouteDistances | None = None, -) -> list[StrDict]: - """Load road transport (bus or coach).""" - items = load_travel(travel_type, plural, data_dir) - stops = travel.parse_yaml(stops_yaml, data_dir) - by_name = {stop["name"]: stop for stop in stops} - - for item in items: - add_station_objects(item, by_name) - if route_distances: - travel.add_leg_route_distance(item, route_distances) - if "distance" in item: - item["co2_kg"] = item["distance"] * co2_factor - from_station = item.get("from_station") - to_station = item.get("to_station") - if from_station and to_station: - # Support scalar or mapping routes: string or dict of stop name -> geojson filename - routes_val = from_station.get("routes", {}) - if isinstance(routes_val, str): - geo: str | None = routes_val - else: - geo = routes_val.get(to_station.get("name")) - if geo: - item["geojson_filename"] = geo - - return items - - -def load_coaches( - data_dir: str, route_distances: travel.RouteDistances | None = None -) -> list[StrDict]: - """Load coaches.""" - return load_road_transport( - "coach", - "coaches", - "coach_stations", - data_dir, - COACH_CO2_KG_PER_KM, - route_distances, - ) - - -def load_buses( - data_dir: str, route_distances: travel.RouteDistances | None = None -) -> list[StrDict]: - """Load buses.""" - return load_road_transport( - "bus", "buses", "bus_stops", data_dir, BUS_CO2_KG_PER_KM, route_distances - ) - - -def route_filename_without_extension(route: str) -> str: - """Return route filename without a GeoJSON extension.""" - return route.removesuffix(".geojson") - - -def line_strings_from_geojson(geojson_data: StrDict) -> list[list[list[float]]]: - """Extract LineString coordinate arrays from GeoJSON.""" - if geojson_data["type"] == "FeatureCollection": - features = typing.cast(list[StrDict], geojson_data["features"]) - return [ - line for feature in features for line in line_strings_from_geojson(feature) - ] - - if geojson_data["type"] == "Feature": - return line_strings_from_geojson(typing.cast(StrDict, geojson_data["geometry"])) - - if geojson_data["type"] == "LineString": - return [typing.cast(list[list[float]], geojson_data["coordinates"])] - - if geojson_data["type"] == "MultiLineString": - return typing.cast(list[list[list[float]]], geojson_data["coordinates"]) - - return [] - - -def geojson_lonlat_to_latlon(coord: list[float]) -> tuple[float, float]: - """Convert a GeoJSON lon/lat coordinate to a Leaflet lat/lon tuple.""" - return (coord[1], coord[0]) - - -def geojson_route_endpoints( - geojson_data: StrDict, -) -> tuple[tuple[float, float], tuple[float, float]] | None: - """Return the first and last points of a GeoJSON route.""" - lines = line_strings_from_geojson(geojson_data) - populated_lines = [line for line in lines if line] - if not populated_lines: - return None - return ( - geojson_lonlat_to_latlon(populated_lines[0][0]), - geojson_lonlat_to_latlon(populated_lines[-1][-1]), - ) - - -def geojson_route_distance_km(geojson_data: StrDict) -> float: - """Calculate the length of a GeoJSON route in kilometres.""" - total = 0.0 - for line in line_strings_from_geojson(geojson_data): - for i in range(len(line) - 1): - total += float( - geodesic( - geojson_lonlat_to_latlon(line[i]), - geojson_lonlat_to_latlon(line[i + 1]), - ).km - ) - return total - - -def car_endpoint_type(name: str) -> str: - """Choose a marker type for a car route endpoint.""" - return ( - "home" - if name.casefold() in {"home", "pch", "picture house court"} - else "car_stop" - ) - - -def car_route_labels(item: StrDict) -> tuple[str | None, str | None]: - """Return car route from/to labels from YAML or the route filename.""" - from_label = item.get("from") - to_label = item.get("to") - if isinstance(from_label, str) and isinstance(to_label, str): - return (from_label, to_label) - - route = item.get("route") - if not isinstance(route, str): - return ( - from_label if isinstance(from_label, str) else None, - to_label if isinstance(to_label, str) else None, - ) - - route_stem = os.path.basename(route_filename_without_extension(route)) - if "_to_" not in route_stem: - return ( - from_label if isinstance(from_label, str) else None, - to_label if isinstance(to_label, str) else None, - ) - - start, end = route_stem.split("_to_", 1) - return ( - from_label if isinstance(from_label, str) else start.replace("_", " "), - to_label if isinstance(to_label, str) else end.replace("_", " "), - ) - - -def car_endpoint_marker_type(item: StrDict, direction: str, label: str) -> str: - """Return the marker type for a car route endpoint.""" - marker_type = item.get(direction + "_type") - return marker_type if isinstance(marker_type, str) else car_endpoint_type(label) - - -def show_car_endpoint_marker(item: StrDict, direction: str, label: str) -> bool: - """Return whether to show a car endpoint marker on the map.""" - direction_marker = item.get(direction + "_show_marker") - if isinstance(direction_marker, bool): - return direction_marker - - show_markers = item.get("show_markers") - if isinstance(show_markers, bool): - return show_markers - - return car_endpoint_type(label) == "home" - - -def load_cars(data_dir: str) -> list[StrDict]: - """Load car journeys.""" - filename = os.path.join(data_dir, "car_journeys.yaml") - if not os.path.exists(filename): - return [] - - items = load_travel("car", "car_journeys", data_dir) - for item in items: - route = item.get("route") - if not isinstance(route, str): - continue - - route_filename = route_filename_without_extension(route) - item["geojson_filename"] = route_filename - - with open( - os.path.join(data_dir, "car_routes", route_filename + ".geojson") - ) as f: - geojson_data = typing.cast(StrDict, json.load(f)) - - if "distance" not in item: - item["distance"] = geojson_route_distance_km(geojson_data) - if "distance" in item and "co2_kg" not in item: - item["co2_kg"] = item["distance"] * CAR_CO2_KG_PER_KM - - endpoints = geojson_route_endpoints(geojson_data) - from_label, to_label = car_route_labels(item) - if endpoints is None: - continue - - for direction, field, label, endpoint in ( - ("from", "from_location", from_label, endpoints[0]), - ("to", "to_location", to_label, endpoints[1]), - ): - if not label or not show_car_endpoint_marker(item, direction, label): - continue - item[field] = { - "name": label, - "type": car_endpoint_marker_type(item, direction, label), - "latitude": endpoint[0], - "longitude": endpoint[1], - } - - if from_label: - item["from"] = from_label - if to_label: - item["to"] = to_label - - return items - - -def process_flight( - flight: StrDict, by_iata: dict[str, Airline], airports: list[StrDict] -) -> None: - """Add airport detail, airline name and distance to flight.""" - if flight["from"] in airports: - flight["from_airport"] = airports[flight["from"]] - if flight["to"] in airports: - flight["to_airport"] = airports[flight["to"]] - if "airline" in flight: - airline = by_iata[flight["airline"]] - flight["airline_detail"] = airline - flight["airline_code"] = airline[ - "iata" if not airline.get("flight_number_prefer_icao") else "icao" - ] - - flight["distance"] = travel.flight_distance(flight) - - -def load_flight_bookings(data_dir: str) -> list[StrDict]: - """Load flight bookings.""" - bookings = load_travel("flight", "flights", data_dir) - airlines = yaml.safe_load(open(os.path.join(data_dir, "airlines.yaml"))) - by_iata = {a["iata"]: a for a in airlines} - airports = travel.parse_yaml("airports", data_dir) - for booking in bookings: - for flight in booking["flights"]: - process_flight(flight, by_iata, airports) - return bookings - - -def load_flights(flight_bookings: list[StrDict]) -> list[StrDict]: - """Load flights.""" - flights = [] - for booking in flight_bookings: - for flight in booking["flights"]: - for f in "type", "trip", "booking_reference", "price", "currency": - if f in booking: - flight[f] = booking[f] - flights.append(flight) - return flights - - -def collect_travel_items( - flight_bookings: list[StrDict], - data_dir: str | None = None, - route_distances: travel.RouteDistances | None = None, -) -> list[StrDict]: - """Generate list of trips.""" - if data_dir is None: - data_dir = flask.current_app.config["PERSONAL_DATA"] - - return sorted( - load_flights(load_flight_bookings(data_dir)) - + load_trains(data_dir, route_distances=route_distances) - + load_ferries(data_dir, route_distances=route_distances) - + load_coaches(data_dir, route_distances=route_distances) - + load_buses(data_dir, route_distances=route_distances) - + load_cars(data_dir), - key=depart_datetime, - ) - - -def group_travel_items_into_trips( - data: StrDict, yaml_trip_list: list[StrDict] -) -> list[Trip]: - """Group travel items into trips.""" - trips: dict[date, Trip] = {} - yaml_trip_lookup = {item["trip"]: item for item in yaml_trip_list} - for key, item_list in data.items(): - assert isinstance(item_list, list) - for item in item_list: - if not (start := item.get("trip")): - continue - if start not in trips: - from_yaml = yaml_trip_lookup.get(start, {}) - trips[start] = Trip( - start=start, **{k: v for k, v in from_yaml.items() if k != "trip"} - ) - getattr(trips[start], key).append(item) - - return [trip for _, trip in sorted(trips.items())] - - -def build_trip_list( - data_dir: str | None = None, - route_distances: travel.RouteDistances | None = None, -) -> list[Trip]: - """Generate list of trips.""" - if data_dir is None: - data_dir = flask.current_app.config["PERSONAL_DATA"] - - yaml_trip_list = travel.parse_yaml("trips", data_dir) - - flight_bookings = load_flight_bookings(data_dir) - conferences = travel.parse_yaml("conferences", data_dir) - for conf in conferences: - date_fields = conference.conference_date_fields(conf) - if "start" in date_fields: - conf["start"] = date_fields["start"] - conf["end"] = date_fields["end"] - - data = { - "flight_bookings": flight_bookings, - "travel": collect_travel_items(flight_bookings, data_dir, route_distances), - "accommodation": travel.parse_yaml("accommodation", data_dir), - "conferences": conferences, - "events": travel.parse_yaml("events", data_dir), - } - - for item in data["accommodation"]: - price = item.get("price") - if price: - item["price"] = decimal.Decimal(price) - - return group_travel_items_into_trips(data, yaml_trip_list) - - -def add_coordinates_for_unbooked_flights( - routes: list[StrDict], coordinates: list[StrDict], data_dir: str -) -> None: - """Add coordinates for flights that haven't been booked yet.""" - if not any(route["type"] == "unbooked_flight" for route in routes): - return - - airports = typing.cast(dict[str, StrDict], travel.parse_yaml("airports", data_dir)) - existing_airport_names = { - typing.cast(str, pin["name"]) - for pin in coordinates - if pin.get("type") == "airport" and isinstance(pin.get("name"), str) - } - iata_codes: set[str] = set() - for route in routes: - if route["type"] != "unbooked_flight": - continue - iata_codes.add(typing.cast(str, route.get("from_iata", "LHR"))) - to_iata = route.get("to_iata") - if isinstance(to_iata, str): - iata_codes.add(to_iata) - - for iata in sorted(iata_codes): - airport = airports.get(iata) - if not airport: - continue - airport_name = typing.cast(str, airport["name"]) - if airport_name in existing_airport_names: - continue - coordinates.append( - { - "name": airport_name, - "type": "airport", - "latitude": airport["latitude"], - "longitude": airport["longitude"], - } - ) - existing_airport_names.add(airport_name) - - -def stations_from_travel(t: StrDict) -> list[StrDict]: - """Stations from train journey.""" - station_list = [t["from_station"], t["to_station"]] - for leg in t["legs"]: - station_list.append(leg["from_station"]) - station_list.append(leg["to_station"]) - - return station_list - - -def process_station_list(station_list: list[StrDict]) -> StrDict: - """Proess sation list.""" - stations = {} - for s in station_list: - if s["name"] in stations: - continue - stations[s["name"]] = s - return stations - - -def get_locations(trip: Trip) -> dict[str, StrDict]: - """Collect locations of all travel locations in trip.""" - locations: dict[str, StrDict] = { - "station": {}, - "airport": {}, - "ferry_terminal": {}, - "coach_station": {}, - "bus_stop": {}, - "home": {}, - "car_stop": {}, - } - - station_list = [] - for t in trip.travel: - match t["type"]: - case "train": - station_list += stations_from_travel(t) - case "coach": - for field in ("from_station", "to_station"): - s = t[field] - locations["coach_station"][s["name"]] = s - case "bus": - for field in ("from_station", "to_station"): - s = t[field] - locations["bus_stop"][s["name"]] = s - case "flight": - for field in "from_airport", "to_airport": - if field in t: - locations["airport"][t[field]["iata"]] = t[field] - case "ferry": - for field in "from_terminal", "to_terminal": - terminal = t[field] - locations["ferry_terminal"][terminal["name"]] = terminal - case "car": - for field in ("from_location", "to_location"): - location = t.get(field) - if not location: - continue - location_type = location.get("type", "car_stop") - locations[location_type][location["name"]] = location - - locations["station"] = process_station_list(station_list) - return locations - - -def coordinate_dict(item: StrDict, coord_type: str) -> StrDict: - """Build coordinate dict for item.""" - return { - "name": item["name"], - "type": coord_type, - "latitude": item["latitude"], - "longitude": item["longitude"], - } - - -def coordinate_key(item: StrDict) -> tuple[str, str]: - """Return the de-duplication key for a coordinate item.""" - return (item["type"], item["name"]) - - -def add_coordinate_if_missing(coordinates: list[StrDict], item: StrDict) -> None: - """Append a coordinate item if the same type/name is not already present.""" - key = coordinate_key(item) - if any(coordinate_key(existing) == key for existing in coordinates): - return - coordinates.append(item) - - -def add_coordinates_for_eurostar_home_feeder( - trip: Trip, coordinates: list[StrDict], data_dir: str -) -> None: - """Add the assumed Bristol station pin for London Eurostar feeder travel.""" - if not any( - t["type"] == "train" and is_eurostar_departure_from_london(t) - for t in trip.travel - ): - return - - stations = load_station_lookup(data_dir) - from_station = stations.get(EUROSTAR_HOME_FEEDER_ROUTE[0]) - if from_station is None: - return - - add_coordinate_if_missing(coordinates, coordinate_dict(from_station, "station")) - - -def timed_departures(item: StrDict) -> list[datetime]: - """Return timed train/flight departure datetimes from a travel item.""" - depart_values: list[typing.Any] = [] - - if item.get("type") in ("flight", "train"): - depart_values.append(item.get("depart")) - - if item.get("type") == "train": - legs = item.get("legs", []) - if isinstance(legs, list): - depart_values.extend( - leg.get("depart") for leg in legs if isinstance(leg, dict) - ) - - return [value for value in depart_values if isinstance(value, datetime)] - - -def timed_arrivals(item: StrDict) -> list[datetime]: - """Return timed train/flight arrival datetimes from a travel item.""" - arrive_values: list[typing.Any] = [] - - if item.get("type") in ("flight", "train"): - arrive_values.append(item.get("arrive")) - - if item.get("type") == "train": - legs = item.get("legs", []) - if isinstance(legs, list): - arrive_values.extend( - leg.get("arrive") for leg in legs if isinstance(leg, dict) - ) - - return [value for value in arrive_values if isinstance(value, datetime)] - - -def has_morning_train_or_flight_departure(trip: Trip, target_date: date) -> bool: - """Return true if a train or flight leaves before noon on target_date.""" - return any( - depart.date() == target_date and depart.time() < NOON - for item in trip.travel - for depart in timed_departures(item) - ) - - -def has_afternoon_train_or_flight_arrival(trip: Trip, target_date: date) -> bool: - """Return true if a train or flight arrives after noon on target_date.""" - return any( - arrive.date() == target_date and arrive.time() > NOON - for item in trip.travel - for arrive in timed_arrivals(item) - ) - - -def conference_free_days(trip: Trip) -> dict[str, tuple[int, int]]: - """Return (days_before, days_after) exploration days for each conference. - - Keyed by the conference start date as an ISO string. Days are relative to - the trip boundary or the adjacent conference's end/start for multi-conference - trips. - """ - if not trip.conferences or not trip.end: - return {} - - def conf_attend_start(c: StrDict) -> date: - return as_date(c.get("attend_start") or c["start"]) - - def conf_attend_end(c: StrDict) -> date: - return as_date(c.get("attend_end") or c["end"]) - - sorted_confs = sorted(trip.conferences, key=conf_attend_start) - result: dict[str, tuple[int, int]] = {} - - for i, conf in enumerate(sorted_confs): - before_boundary = conf_attend_end(sorted_confs[i - 1]) if i > 0 else trip.start - after_boundary = ( - conf_attend_start(sorted_confs[i + 1]) - if i < len(sorted_confs) - 1 - else trip.end - ) - days_before = (conf_attend_start(conf) - before_boundary).days - days_after = (after_boundary - conf_attend_end(conf)).days - if ( - i == 0 - and days_before > 0 - and has_afternoon_train_or_flight_arrival(trip, before_boundary) - ): - days_before -= 1 - if ( - i == len(sorted_confs) - 1 - and days_after > 0 - and has_morning_train_or_flight_departure(trip, after_boundary) - ): - days_after -= 1 - result[str(conf["start"])] = (days_before, days_after) - - return result - - -def collect_trip_coordinates(trip: Trip) -> list[StrDict]: - """Extract and de-duplicate travel location coordinates from trip.""" - coords = [] - - src = [ - ("accommodation", trip.accommodation), - ("conference", trip.conferences), - ("event", trip.events), - ] - for coord_type, item_list in src: - coords += [ - coordinate_dict(item, coord_type) - for item in item_list - if "latitude" in item and "longitude" in item - ] - - locations = get_locations(trip) - for coord_type, coord_dict in locations.items(): - coords += [coordinate_dict(s, coord_type) for s in coord_dict.values()] - - return coords - - -def destination_latlon( - stop: StrDict, airports: dict[str, StrDict] -) -> tuple[float, float] | None: - """Resolve destination coordinates from airport lookup or explicit lat/lon.""" - iata = preferred_airport_iata(stop, airports) - if iata: - airport = airports.get(iata) - if airport: - return latlon_tuple(airport) - if "latitude" in stop and "longitude" in stop: - return latlon_tuple(stop) - return None - - -def latlon_tuple(stop: StrDict) -> tuple[float, float]: - """Given a transport stop return the lat/lon as a tuple.""" - return (stop["latitude"], stop["longitude"]) - - -def read_geojson(data_dir: str, filename: str) -> str: - """Read GeoJSON from file.""" - return open(os.path.join(data_dir, filename + ".geojson")).read() - - -def get_trip_routes(trip: Trip, data_dir: str) -> list[StrDict]: - """Get routes for given trip to show on map.""" - routes: list[StrDict] = [] - seen_geojson: set[str] = set() - eurostar_home_feeder_route = get_eurostar_home_feeder_route( - trip, data_dir, seen_geojson - ) - if eurostar_home_feeder_route is not None: - routes.append(eurostar_home_feeder_route) - - for t in trip.travel: - if t["type"] == "ferry": - ferry_from, ferry_to = t["from_terminal"], t["to_terminal"] - - key = "_".join(["ferry"] + sorted([ferry_from["name"], ferry_to["name"]])) - filename = os.path.join("ferry_routes", t["geojson_filename"]) - - routes.append( - { - "type": "train", - "key": key, - "geojson_filename": filename, - } - ) - continue - if t["type"] == "flight": - if "from_airport" not in t or "to_airport" not in t: - continue - fly_from, fly_to = t["from_airport"], t["to_airport"] - key = "_".join(["flight"] + sorted([fly_from["iata"], fly_to["iata"]])) - routes.append( - { - "type": "flight", - "key": key, - "from": latlon_tuple(fly_from), - "to": latlon_tuple(fly_to), - } - ) - continue - if t["type"] in ("coach", "bus"): - route_type = t["type"] - stop_from, stop_to = t["from_station"], t["to_station"] - key = "_".join([route_type] + sorted([stop_from["name"], stop_to["name"]])) - if t.get("geojson_filename"): - filename = os.path.join(f"{route_type}_routes", t["geojson_filename"]) - routes.append( - {"type": route_type, "key": key, "geojson_filename": filename} - ) - else: - routes.append( - { - "type": route_type, - "key": key, - "from": latlon_tuple(stop_from), - "to": latlon_tuple(stop_to), - } - ) - continue - if t["type"] == "car": - route = t.get("geojson_filename") - if not isinstance(route, str): - continue - route_filename = route_filename_without_extension(route) - key = "_".join( - [ - "car", - typing.cast(str, t.get("from", "")), - typing.cast(str, t.get("to", "")), - route_filename, - ] - ) - routes.append( - { - "type": "car", - "key": key, - "geojson_filename": os.path.join("car_routes", route_filename), - } - ) - continue - if t["type"] == "train": - for leg in t["legs"]: - train_from, train_to = leg["from_station"], leg["to_station"] - route = build_train_route(train_from, train_to, seen_geojson) - if route is not None: - routes.append(route) - - if routes: - return routes - - airports = typing.cast(dict[str, StrDict], travel.parse_yaml("airports", data_dir)) - origin_rules = load_flight_destination_rules(data_dir) - - unbooked_routes = [] - for item in trip.conferences: - unbooked_rail_route = get_unbooked_rail_route(item, data_dir) - if unbooked_rail_route is not None: - unbooked_routes.append(unbooked_rail_route) - continue - - if item["country"] in {"gb", "be"}: # not flying to Belgium - continue - destination_iata = preferred_airport_iata(item, airports) - destination = destination_latlon(item, airports) - if not destination: - continue - from_iata = get_unbooked_flight_origin_iata(destination_iata, origin_rules) - from_airport = airports.get(from_iata) - if not from_airport: - from_airport = airports["LHR"] - from_iata = "LHR" - - unbooked_routes.append( - { - "type": "unbooked_flight", - "key": f'{from_iata}_{item["location"]}_{item["country"]}', - "from_iata": from_iata, - "to_iata": destination_iata, - "from": latlon_tuple(from_airport), - "to": destination, - } - ) - - return unbooked_routes - - -def get_coordinates_and_routes( - trip_list: list[Trip], data_dir: str | None = None -) -> tuple[list[StrDict], list[StrDict]]: - """Given a list of trips return the associated coordinates and routes.""" - if data_dir is None: - data_dir = flask.current_app.config["PERSONAL_DATA"] - coordinates = [] - seen_coordinates: set[tuple[str, str]] = set() - routes = [] - seen_routes: set[str] = set() - for trip in trip_list: - for stop in collect_trip_coordinates(trip): - key = coordinate_key(stop) - if key in seen_coordinates: - continue - coordinates.append(stop) - seen_coordinates.add(key) - - add_coordinates_for_eurostar_home_feeder(trip, coordinates, data_dir) - seen_coordinates = {coordinate_key(stop) for stop in coordinates} - - for route in get_trip_routes(trip, data_dir): - if route["key"] in seen_routes: - continue - routes.append(route) - seen_routes.add(route["key"]) - - add_coordinates_for_unbooked_flights(routes, coordinates, data_dir) - - for route in routes: - if "geojson_filename" in route: - route["geojson"] = read_geojson(data_dir, route.pop("geojson_filename")) - - return (coordinates, routes) - - -def get_trip_list( - route_distances: travel.RouteDistances | None = None, -) -> list[Trip]: - """Get list of trips respecting current authentication status.""" - trips = [ - trip - for trip in build_trip_list(route_distances=route_distances) - if flask.g.user.is_authenticated or not trip.private - ] - - # Add Schengen compliance information to each trip - for trip in trips: - trip_schengen.add_schengen_compliance_to_trip(trip) - - return trips - - -DEFAULT_EVENT_DURATION = timedelta(hours=1) - - -def _ensure_utc(dt_value: datetime) -> datetime: - """Ensure datetimes are timezone-aware in UTC.""" - if dt_value.tzinfo is None: - return dt_value.replace(tzinfo=timezone.utc) - return dt_value.astimezone(timezone.utc) - - -def _event_datetimes(element: TripElement) -> tuple[datetime, datetime]: - """Return start and end datetimes for a trip element.""" - start_dt = as_datetime(element.start_time) - end_dt = as_datetime(element.end_time) if element.end_time else None - if end_dt is None or end_dt <= start_dt: - end_dt = start_dt + DEFAULT_EVENT_DURATION - return _ensure_utc(start_dt), _ensure_utc(end_dt) - - -def _event_all_day_dates(element: TripElement) -> tuple[date, date]: - """Return start and exclusive end dates for all-day events.""" - start_date = as_date(element.start_time) - end_source = element.end_time or element.start_time - end_date = as_date(end_source) + timedelta(days=1) - return start_date, end_date - - -def _trip_element_location(element: TripElement) -> str | None: - """Derive a location string for the element.""" - start_loc: str | None = element.start_loc - end_loc: str | None = element.end_loc - if start_loc and end_loc: - return f"{start_loc} → {end_loc}" - if start_loc: - return start_loc - if end_loc: - return end_loc - return None - - -def _trip_element_description(trip: Trip, element: TripElement) -> str: - """Build a textual description for the element.""" - lines = [ - f"Trip: {trip.title}", - f"Type: {element.element_type}", - ] - if element.element_type == "conference": - location_label = element.detail.get("location") - venue_label = element.detail.get("venue") or location_label - if place := _format_place( - location_label, - element.start_country, - element.element_type, - label_prefix="Location", - skip_country_if_in_label=True, - ): - lines.append(place) - if place := _format_place( - venue_label, - element.end_country, - element.element_type, - label_prefix="Venue", - ): - lines.append(place) - else: - if place := _format_place( - element.start_loc, - element.start_country, - element.element_type, - label_prefix="From", - ): - lines.append(place) - if place := _format_place( - element.end_loc, - element.end_country, - element.element_type, - label_prefix="To", - ): - lines.append(place) - lines.append(f"Trip start date: {_format_trip_start_date(trip)}") - return "\n".join(lines) - - -def _format_place( - label: str | None, - country: pycountry.db.Country | None, - element_type: str, - label_prefix: str | None = None, - skip_country_if_in_label: bool = False, -) -> str | None: - """Combine location label and country into a single string.""" - parts: list[str] = [] - formatted_label = label - if element_type == "train" and label: - stripped = label.strip() - if not stripped.lower().endswith("station"): - formatted_label = f"{stripped} station" - else: - formatted_label = stripped - if formatted_label: - parts.append(formatted_label) - if country: - include_country = True - if (element_type == "train" or skip_country_if_in_label) and formatted_label: - include_country = country.name.lower() not in formatted_label.lower() - if include_country: - parts.append(country.name) - if not parts: - return None - if label_prefix: - return f"{label_prefix}: {', '.join(parts)}" - return ", ".join(parts) - - -def _format_trip_start_date(trip: Trip) -> str: - """Return trip start date in UK human-readable form with day name.""" - day_name = trip.start.strftime("%A") - month_year = trip.start.strftime("%B %Y") - return f"{day_name} {trip.start.day} {month_year}" - - -def _flight_iata(detail: StrDict, key: str) -> str | None: - """Extract an IATA code from flight detail.""" - airport = typing.cast(StrDict | None, detail.get(key)) - if not airport: - return None - return typing.cast(str | None, airport.get("iata")) - - -def _flight_label(detail: StrDict) -> str | None: - """Return a compact label for flight elements.""" - from_code = _flight_iata(detail, "from_airport") - to_code = _flight_iata(detail, "to_airport") - if from_code and to_code: - return f"{from_code}→{to_code}" - if from_code: - return from_code - if to_code: - return to_code - return None - - -def _trip_element_label(element: TripElement) -> str: - """Return a succinct label describing the element.""" - if element.element_type == "flight": - if label := _flight_label(element.detail): - return label - if element.element_type == "conference": - conference_name = element.detail.get("name") - if isinstance(conference_name, str): - return conference_name - - start_loc = element.start_loc - end_loc = element.end_loc - if isinstance(start_loc, str) and isinstance(end_loc, str): - return f"{start_loc} → {end_loc}" - if isinstance(start_loc, str): - return start_loc - if isinstance(end_loc, str): - return end_loc - return element.title - - -def _trip_element_summary(trip: Trip, element: TripElement) -> str: - """Build the calendar summary text.""" - label = _trip_element_label(element) - if element.element_type == "conference": - return f"{element.element_type}: {label}" - return f"{element.element_type}: {label} [{trip.title}]" - - -def _trip_element_uid(trip: Trip, element: TripElement, index: int) -> str: - """Generate a deterministic UID for calendar clients.""" - raw = "|".join( - [ - trip.start.isoformat(), - trip.title, - element.element_type, - element.title, - str(index), - ] - ) - digest = hashlib.sha1(raw.encode("utf-8")).hexdigest() - return f"trip-{digest}@agenda-codex" - - -def build_trip_ical(trips: list[Trip]) -> bytes: - """Return an iCal feed containing all trip elements.""" - lines = [ - "BEGIN:VCALENDAR", - "VERSION:2.0", - "PRODID:-//Agenda Codex//Trips//EN", - "CALSCALE:GREGORIAN", - "METHOD:PUBLISH", - "X-WR-CALNAME:Trips", - ] - generated = datetime.now(tz=timezone.utc) - - for trip in trips: - for index, element in enumerate(trip.elements()): - lines.append("BEGIN:VEVENT") - ical.append_property(lines, "UID", _trip_element_uid(trip, element, index)) - ical.append_property(lines, "DTSTAMP", ical.format_datetime_utc(generated)) - if element.all_day: - start_date, end_date = _event_all_day_dates(element) - ical.append_property( - lines, "DTSTART;VALUE=DATE", ical.format_date(start_date) - ) - ical.append_property( - lines, "DTEND;VALUE=DATE", ical.format_date(end_date) - ) - else: - start_dt, end_dt = _event_datetimes(element) - ical.append_property( - lines, "DTSTART", ical.format_datetime_utc(start_dt) - ) - ical.append_property(lines, "DTEND", ical.format_datetime_utc(end_dt)) - summary = ical.escape_text(_trip_element_summary(trip, element)) - ical.append_property(lines, "SUMMARY", summary) - - description = ical.escape_text(_trip_element_description(trip, element)) - ical.append_property(lines, "DESCRIPTION", description) - - if location := _trip_element_location(element): - ical.append_property(lines, "LOCATION", ical.escape_text(location)) - - lines.append("END:VEVENT") - - lines.append("END:VCALENDAR") - ical_text = "\r\n".join(lines) + "\r\n" - return ical_text.encode("utf-8") - - -def get_current_trip(today: date) -> Trip | None: - """Get current trip.""" - trip_list = get_trip_list(route_distances=None) - - current = [ - item - for item in trip_list - if item.start <= today and (item.end or item.start) >= today - ] - assert len(current) < 2 - return current[0] if current else None diff --git a/agenda/trip_debug.py b/agenda/trip_debug.py deleted file mode 100644 index 663a9b5..0000000 --- a/agenda/trip_debug.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Serialize trip details for the debug page.""" - -import json - -import yaml - -from agenda.types import Trip - - -def serialize_trip(trip: Trip) -> tuple[str, str]: - """Return JSON and YAML including computed trip properties.""" - # Convert trip object to dictionary for display - trip_dict = { - "start": trip.start.isoformat(), - "name": trip.name, - "private": trip.private, - "travel": trip.travel, - "accommodation": trip.accommodation, - "conferences": trip.conferences, - "events": trip.events, - "flight_bookings": trip.flight_bookings, - "computed_properties": { - "title": trip.title, - "end": trip.end.isoformat() if trip.end else None, - "countries": [ - {"name": c.name, "alpha_2": c.alpha_2, "flag": c.flag} - for c in trip.countries - ], - "locations": [ - { - "location": loc, - "country": {"name": country.name, "alpha_2": country.alpha_2}, - } - for loc, country in trip.locations() - ], - "total_distance": trip.total_distance(), - "total_co2_kg": trip.total_co2_kg(), - "distances_by_transport_type": trip.distances_by_transport_type(), - "co2_by_transport_type": trip.co2_by_transport_type(), - }, - "schengen_compliance": ( - { - "total_days_used": trip.schengen_compliance.total_days_used, - "days_remaining": trip.schengen_compliance.days_remaining, - "is_compliant": trip.schengen_compliance.is_compliant, - "current_180_day_period": [ - trip.schengen_compliance.current_180_day_period[0].isoformat(), - trip.schengen_compliance.current_180_day_period[1].isoformat(), - ], - "days_over_limit": trip.schengen_compliance.days_over_limit, - } - if trip.schengen_compliance - else None - ), - } - - # Convert to JSON for pretty printing - trip_json = json.dumps(trip_dict, indent=2, default=str) - trip_yaml = yaml.safe_dump(json.loads(trip_json), sort_keys=False) - - return trip_json, trip_yaml diff --git a/agenda/trip_schengen.py b/agenda/trip_schengen.py deleted file mode 100644 index 926d022..0000000 --- a/agenda/trip_schengen.py +++ /dev/null @@ -1,279 +0,0 @@ -"""Integration of Schengen calculator with the existing trip system.""" - -import logging -import typing -from datetime import date, timedelta - -import flask - -from . import get_country, trip -from .schengen import ( - SCHENGEN_COUNTRIES, - calculate_schengen_time, - extract_schengen_stays_from_travel, -) -from .types import SchengenCalculation, SchengenStay, StrDict, Trip - - -def trip_includes_schengen(trip: Trip) -> bool: - return bool({c.alpha_2.lower() for c in trip.countries} & SCHENGEN_COUNTRIES) - - -def add_schengen_compliance_to_trip(trip: Trip) -> Trip: - """Add Schengen compliance information to a trip object.""" - if not trip_includes_schengen(trip): - return trip - - try: - # Calculate Schengen compliance for the trip - calculation = calculate_schengen_time(trip.travel) - - # Add the calculation to the trip object - trip.schengen_compliance = calculation - except Exception as e: - # Log the error but don't fail the trip loading - logging.warning( - f"Failed to calculate Schengen compliance for trip {trip.start}: {e}" - ) - trip.schengen_compliance = None - - return trip - - -def get_schengen_compliance_for_all_trips( - trip_list: list[Trip], -) -> dict[date, SchengenCalculation]: - """Calculate Schengen compliance for all trips.""" - compliance_by_date = {} - - # Collect all travel items across all trips - all_travel_items = [] - for trip_obj in trip_list: - all_travel_items.extend(trip_obj.travel) - - # Calculate compliance for each trip's start date - for trip_obj in trip_list: - calculation = calculate_schengen_time(all_travel_items, trip_obj.start) - compliance_by_date[trip_obj.start] = calculation - - return compliance_by_date - - -def generate_schengen_warnings(trip_list: list[Trip]) -> list[dict[str, typing.Any]]: - """Generate warnings for potential Schengen violations.""" - warnings = [] - - # Get compliance for all trips - compliance_by_date = get_schengen_compliance_for_all_trips(trip_list) - - for trip_date, calculation in compliance_by_date.items(): - if not calculation.is_compliant: - warnings.append( - { - "type": "schengen_violation", - "date": trip_date, - "message": f"Schengen violation on {trip_date}: {calculation.days_over_limit} days over limit", - "severity": "error", - "calculation": calculation, - } - ) - elif calculation.days_remaining < 10: - warnings.append( - { - "type": "schengen_warning", - "date": trip_date, - "message": f"Low Schengen allowance on {trip_date}: only {calculation.days_remaining} days remaining", - "severity": "warning", - "calculation": calculation, - } - ) - - return warnings - - -def extract_schengen_stays_with_trip_info( - trip_list: list[Trip], -) -> list[SchengenStay]: - """Extract Schengen stays from travel items with trip information.""" - # Create a mapping of travel items to their trips - travel_to_trip_map = {} - for trip_obj in trip_list: - for travel_item in trip_obj.travel: - travel_to_trip_map[id(travel_item)] = trip_obj - - # Collect all travel items - all_travel_items = [] - for trip_obj in trip_list: - all_travel_items.extend(trip_obj.travel) - - # Get stays with trip information - stays = extract_schengen_stays_from_travel(all_travel_items) - - # Try to associate stays with trips based on entry dates - for stay in stays: - # Find the trip that contains this stay's entry date - for trip_obj in trip_list: - if trip_obj.start <= stay.entry_date <= (trip_obj.end or stay.entry_date): - stay.trip_date = trip_obj.start - stay.trip_name = trip_obj.title - break - - # If no exact match, find the closest trip by start date - if stay.trip_date is None: - closest_trip = min( - trip_list, - key=lambda t: abs((t.start - stay.entry_date).days), - default=None, - ) - if closest_trip: - stay.trip_date = closest_trip.start - stay.trip_name = closest_trip.title - - return stays - - -def schengen_dashboard_data(data_dir: str | None = None) -> dict[str, typing.Any]: - """Generate dashboard data for Schengen compliance.""" - if data_dir is None: - data_dir = flask.current_app.config["PERSONAL_DATA"] - - # Load all trips - trip_list = [ - trip for trip in trip.build_trip_list(data_dir) if trip_includes_schengen(trip) - ] - - # Calculate current compliance with trip information - all_travel_items = [] - for trip_obj in trip_list: - all_travel_items.extend(trip_obj.travel) - - current_calculation = calculate_schengen_time(all_travel_items) - - # Get stays with trip information - stays_with_trip_info = extract_schengen_stays_with_trip_info(trip_list) - - # Update current calculation with trip information - current_calculation.stays_in_period = [ - stay - for stay in stays_with_trip_info - if stay.entry_date >= current_calculation.current_180_day_period[0] - and stay.entry_date <= current_calculation.current_180_day_period[1] - ] - - # Generate warnings - warnings = generate_schengen_warnings(trip_list) - - # Get compliance history for the last year - compliance_history = [] - current_date = date.today() - for i in range(365, 0, -7): # Weekly snapshots for the last year - snapshot_date = current_date - timedelta(days=i) - calculation = calculate_schengen_time(all_travel_items, snapshot_date) - compliance_history.append( - { - "date": snapshot_date, - "days_used": calculation.total_days_used, - "is_compliant": calculation.is_compliant, - } - ) - - return { - "current_compliance": current_calculation, - "warnings": warnings, - "compliance_history": compliance_history, - "trips_with_compliance": get_schengen_compliance_for_all_trips(trip_list), - } - - -def flask_route_schengen_report() -> str: - """Flask route for Schengen compliance report.""" - data_dir = flask.current_app.config["PERSONAL_DATA"] - dashboard_data = schengen_dashboard_data(data_dir) - - # Load trips for the template - trip_list = trip.build_trip_list(data_dir) - - return flask.render_template( - "schengen_report.html", - trip_list=trip_list, - get_country=get_country, - **dashboard_data, - ) - - -def export_schengen_data_for_external_calculator( - travel_items: list[StrDict], -) -> list[dict[str, typing.Any]]: - """Export travel data in format suitable for external Schengen calculators.""" - - stays = extract_schengen_stays_from_travel(travel_items) - - export_data = [] - for stay in stays: - export_data.append( - { - "entry_date": stay.entry_date.strftime("%Y-%m-%d"), - "exit_date": ( - stay.exit_date.strftime("%Y-%m-%d") if stay.exit_date else None - ), - "country": stay.country.upper(), - "days": stay.days, - } - ) - - return export_data - - -# Integration with existing trip.py functions -def enhanced_build_trip_list( - data_dir: str | None = None, - route_distances: typing.Any = None, - include_schengen: bool = True, -) -> list[Trip]: - """Enhanced version of build_trip_list that includes Schengen compliance.""" - # Use the original function - trip_list = trip.build_trip_list(data_dir, route_distances) - - if include_schengen: - # Add Schengen compliance to each trip - for trip_obj in trip_list: - add_schengen_compliance_to_trip(trip_obj) - - return trip_list - - -def check_schengen_compliance_for_new_trip( - existing_trips: list[Trip], - new_trip_dates: tuple[date, date], - destination_country: str, -) -> SchengenCalculation: - """Check if a new trip would violate Schengen rules.""" - # Collect all existing travel - all_travel_items = [] - for trip_obj in existing_trips: - all_travel_items.extend(trip_obj.travel) - - # Add the new trip as mock travel items - entry_date, exit_date = new_trip_dates - - # Mock entry to Schengen - all_travel_items.append( - { - "type": "flight", - "depart": entry_date, - "to_airport": {"country": destination_country}, - } - ) - - # Mock exit from Schengen - all_travel_items.append( - { - "type": "flight", - "depart": exit_date, - "from_airport": {"country": destination_country}, - "to_airport": {"country": "gb"}, # Assuming return to UK - } - ) - - # Calculate compliance at the exit date - return calculate_schengen_time(all_travel_items, exit_date) diff --git a/agenda/trip_timezones.py b/agenda/trip_timezones.py deleted file mode 100644 index e8b86b0..0000000 --- a/agenda/trip_timezones.py +++ /dev/null @@ -1,341 +0,0 @@ -"""Resolve trip destination timezones and local journey display times.""" - -import functools -import importlib -import json -import os.path -import typing -from collections import defaultdict -from datetime import date, datetime, timedelta, timezone -from zoneinfo import ZoneInfo - -import pytz -from pycountry.db import Country - -import agenda.trip -from agenda.types import StrDict, Trip - - -def _timezone_name_from_datetime(value: typing.Any) -> str | None: - """Get IANA timezone name from a datetime value if available.""" - if not isinstance(value, datetime) or value.tzinfo is None: - return None - - key = getattr(value.tzinfo, "key", None) - if isinstance(key, str): - return key - - zone = getattr(value.tzinfo, "zone", None) - if isinstance(zone, str): - return zone - - return None - - -def _format_offset_from_uk(offset_minutes: int) -> str: - """Format offset from UK in +/-HH:MM.""" - if offset_minutes == 0: - return "No difference" - sign = "+" if offset_minutes > 0 else "-" - hours, mins = divmod(abs(offset_minutes), 60) - return f"{sign}{hours:02d}:{mins:02d} vs UK" - - -def _trip_offset_minutes( - trip_start: date, trip_end: date, destination_timezone: str -) -> list[int]: - """Unique UTC offset differences vs UK across the trip date range.""" - destination_tz = ZoneInfo(destination_timezone) - uk_timezone = ZoneInfo("Europe/London") - current = trip_start - offsets: set[int] = set() - - while current <= trip_end: - instant = datetime( - current.year, current.month, current.day, 12, tzinfo=timezone.utc - ) - destination_offset = instant.astimezone(destination_tz).utcoffset() - uk_offset = instant.astimezone(uk_timezone).utcoffset() - if destination_offset is not None and uk_offset is not None: - offsets.add(int((destination_offset - uk_offset).total_seconds() // 60)) - current += timedelta(days=1) - - return sorted(offsets) - - -def _format_trip_offset_display(offsets: list[int]) -> str: - """Format trip-range offsets; include variation if DST changes during trip.""" - if not offsets: - return "Timezone unknown" - if len(offsets) == 1: - return _format_offset_from_uk(offsets[0]) - return ( - "Varies during trip: " - f"{_format_offset_from_uk(offsets[0])} to {_format_offset_from_uk(offsets[-1])}" - ) - - -def _timezone_from_coordinates(latitude: float, longitude: float) -> str | None: - """Resolve IANA timezone name from coordinates.""" - timezone_finder = _get_timezone_finder() - if timezone_finder is None: - return None - - for method_name in ("timezone_at", "certain_timezone_at", "closest_timezone_at"): - finder_method = getattr(timezone_finder, method_name, None) - if not callable(finder_method): - continue - tz_name = finder_method(lng=longitude, lat=latitude) - if isinstance(tz_name, str): - return tz_name - return None - - -@functools.lru_cache(maxsize=1) -def _get_timezone_finder() -> typing.Any: - """Get timezone finder instance if dependency is available.""" - try: - timezonefinder_module = importlib.import_module("timezonefinder") - except ModuleNotFoundError: - return None - - timezone_finder_cls = getattr(timezonefinder_module, "TimezoneFinder", None) - if timezone_finder_cls is None: - return None - - return timezone_finder_cls() - - -def _coordinates_from_location(location: typing.Any) -> tuple[float, float] | None: - """Extract latitude/longitude from a location mapping.""" - if not isinstance(location, dict): - return None - - latitude = location.get("latitude") - longitude = location.get("longitude") - if not isinstance(latitude, (int, float)) or not isinstance( - longitude, (int, float) - ): - return None - - return (float(latitude), float(longitude)) - - -def _route_endpoints_for_car_item( - item: StrDict, - data_dir: str, - route_cache: dict[str, tuple[tuple[float, float], tuple[float, float]] | None], -) -> tuple[tuple[float, float], tuple[float, float]] | None: - """Return route endpoints for a car journey when available.""" - route_filename = item.get("geojson_filename") - if not isinstance(route_filename, str): - return None - - if route_filename not in route_cache: - geojson_text = agenda.trip.read_geojson( - data_dir, os.path.join("car_routes", route_filename) - ) - endpoints = agenda.trip.geojson_route_endpoints(json.loads(geojson_text)) - route_cache[route_filename] = endpoints - - return route_cache[route_filename] - - -def _timezone_name_for_car_item( - item: StrDict, - data_dir: str, - route_cache: dict[str, tuple[tuple[float, float], tuple[float, float]] | None], - timezone_cache: dict[tuple[float, float], str | None], -) -> str | None: - """Resolve the local timezone for a car journey from endpoints.""" - candidate_coords: list[tuple[float, float]] = [] - - for field in ("from_location", "to_location"): - coord = _coordinates_from_location(item.get(field)) - if coord is not None: - candidate_coords.append(coord) - - if not candidate_coords: - endpoints = _route_endpoints_for_car_item(item, data_dir, route_cache) - if endpoints is not None: - candidate_coords.extend(endpoints) - - for coord in candidate_coords: - if coord not in timezone_cache: - timezone_cache[coord] = _timezone_from_coordinates(coord[0], coord[1]) - timezone_name = timezone_cache[coord] - if timezone_name: - return timezone_name - - return None - - -def localize_trip_car_journey_display_times(trip: Trip, data_dir: str) -> None: - """Add route-local display timestamps for car journeys on the trip page.""" - route_cache: dict[str, tuple[tuple[float, float], tuple[float, float]] | None] = {} - timezone_cache: dict[tuple[float, float], str | None] = {} - - for item in trip.travel: - if item.get("type") != "car": - continue - - depart = item.get("depart") - arrive = item.get("arrive") - if not isinstance(depart, datetime) or not isinstance(arrive, datetime): - continue - - timezone_name = _timezone_name_for_car_item( - item, data_dir, route_cache, timezone_cache - ) - if not timezone_name: - continue - - item["display_depart"] = depart.astimezone(ZoneInfo(timezone_name)) - item["display_arrive"] = arrive.astimezone(ZoneInfo(timezone_name)) - - -def get_destination_timezones(trip: Trip) -> list[StrDict]: - """Build destination timezone metadata for the trip page.""" - per_location: dict[tuple[str, str], list[str]] = defaultdict(list) - location_coords: dict[tuple[str, str], tuple[float, float]] = {} - for item in trip.accommodation + trip.conferences + trip.events: - location = item.get("location") - country = item.get("country") - if not isinstance(location, str) or not isinstance(country, str): - continue - - key = (location, country.lower()) - timezone_name = item.get("timezone") - if isinstance(timezone_name, str): - per_location[key].append(timezone_name) - - latitude = item.get("latitude") - longitude = item.get("longitude") - if isinstance(latitude, (int, float)) and isinstance(longitude, (int, float)): - location_coords[key] = (float(latitude), float(longitude)) - - for field in ( - "from", - "to", - "date", - "start", - "end", - "attend_start", - "attend_end", - ): - candidate = _timezone_name_from_datetime(item.get(field)) - if candidate: - per_location[key].append(candidate) - - # Also collect airport locations from flights, for transit countries - flight_locations: list[tuple[str, Country]] = [] - seen_flight_keys: set[tuple[str, str]] = set() - for item in trip.travel: - if item.get("type") != "flight": - continue - for airport_key in ("from_airport", "to_airport"): - airport = item.get(airport_key) - if not isinstance(airport, dict): - continue - city = airport.get("city") - country_code = airport.get("country") - if not isinstance(city, str) or not isinstance(country_code, str): - continue - if country_code == "gb": - continue - key = (city, country_code.lower()) - lat = airport.get("latitude") - lon = airport.get("longitude") - if isinstance(lat, (int, float)) and isinstance(lon, (int, float)): - location_coords.setdefault(key, (float(lat), float(lon))) - if key not in seen_flight_keys: - seen_flight_keys.add(key) - flight_country = agenda.get_country(country_code) - if flight_country: - flight_locations.append((city, flight_country)) - - existing_location_keys = {(loc, c.alpha_2.lower()) for loc, c in trip.locations()} - all_locations = list(trip.locations()) + [ - (city, country) - for city, country in flight_locations - if (city, country.alpha_2.lower()) not in existing_location_keys - ] - - destination_times: list[StrDict] = [] - trip_end = trip.end or trip.start - - for location, country in all_locations: - country_code = country.alpha_2.lower() - key = (location, country_code) - timezone_name = None - - for candidate in per_location.get(key, []): - try: - ZoneInfo(candidate) - timezone_name = candidate - break - except Exception: - continue - - if not timezone_name and key in location_coords: - latitude, longitude = location_coords[key] - coordinate_timezone = _timezone_from_coordinates(latitude, longitude) - if coordinate_timezone: - timezone_name = coordinate_timezone - - if not timezone_name: - country_timezones = pytz.country_timezones.get(country_code, []) - if len(country_timezones) == 1: - timezone_name = country_timezones[0] - - offset_display = "Timezone unknown" - if timezone_name: - offset_display = _format_trip_offset_display( - _trip_offset_minutes(trip.start, trip_end, timezone_name) - ) - - destination_times.append( - { - "location": location, - "country_name": country.name, - "country_flag": country.flag, - "timezone": timezone_name, - "offset_display": offset_display, - } - ) - - grouped: list[StrDict] = [] - grouped_index: dict[tuple[str, str, str | None], int] = {} - for item in destination_times: - group_key = (item["country_name"], item["country_flag"], item["timezone"]) - if group_key in grouped_index: - existing = grouped[grouped_index[group_key]] - existing_locations = typing.cast(list[str], existing["locations"]) - existing_locations.append(typing.cast(str, item["location"])) - existing["location_count"] = ( - typing.cast(int, existing["location_count"]) + 1 - ) - continue - - grouped_index[group_key] = len(grouped) - grouped.append( - { - **item, - "locations": [item["location"]], - "location_count": 1, - } - ) - - for item in grouped: - location_count = typing.cast(int, item["location_count"]) - country_name = typing.cast(str, item["country_name"]) - country_flag = typing.cast(str, item["country_flag"]) - if location_count > 1: - label = f"{country_name} ({location_count} locations)" - else: - label = f"{item['location']} ({country_name})" - if trip.show_flags: - label = f"{label} {country_flag}" - item["destination_label"] = label - - return grouped diff --git a/agenda/types.py b/agenda/types.py index 5450313..ce58d3a 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -1,498 +1,104 @@ """Types.""" -import collections +import dataclasses import datetime -import functools -import typing -from collections import defaultdict -from dataclasses import dataclass, field -from datetime import date -import emoji -from pycountry.db import Country -import agenda -from agenda import format_list_with_ampersand - -from . import utils - -StrDict = dict[str, typing.Any] -DateOrDateTime = datetime.datetime | datetime.date - - -@dataclass -class TripElement: - """Trip element.""" - - start_time: DateOrDateTime - title: str - element_type: str - detail: StrDict - end_time: DateOrDateTime | None = None - start_loc: str | None = None - end_loc: str | None = None - start_country: Country | None = None - end_country: Country | None = None - all_day: bool = False - - def get_emoji(self) -> str | None: - """Emoji for trip element.""" - emoji_map = { - "check-in": ":hotel:", - "check-out": ":hotel:", - "train": ":train:", - "flight": ":airplane:", - "ferry": ":ferry:", - "coach": ":bus:", - "bus": ":bus:", - "car": ":automobile:", - } - - alias = emoji_map.get(self.element_type) - return emoji.emojize(alias, language="alias") if alias else None - - -def airport_label(airport: StrDict) -> str: - """Airport label: name and iata.""" - name = airport.get("alt_name") or airport["city"] - return f"{name} ({airport['iata']})" - - -def is_generated_drive_stop(label: str) -> bool: - """Return whether a car stop label is synthetic timeline filler.""" - prefix = "Drive stop " - return label.startswith(prefix) and label[len(prefix) :].isdigit() - - -@dataclass -class SchengenStay: - """Represents a stay in the Schengen area.""" - - entry_date: date - exit_date: date | None # None if currently in Schengen - country: str - days: int - trip_date: date | None = None # Trip start date for linking - trip_name: str | None = None # Trip name for display - - def __post_init__(self) -> None: - """Post init.""" - if self.exit_date is None: - # Currently in Schengen, calculate days up to today - self.days = (date.today() - self.entry_date).days + 1 - else: - self.days = (self.exit_date - self.entry_date).days + 1 - - -@dataclass -class SchengenCalculation: - """Result of Schengen time calculation.""" - - total_days_used: int - days_remaining: int - is_compliant: bool - current_180_day_period: tuple[date, date] # (start, end) - stays_in_period: list[SchengenStay] - next_reset_date: typing.Optional[date] # When the 180-day window resets - - @property - def days_over_limit(self) -> int: - """Days over the 90-day limit.""" - return max(0, self.total_days_used - 90) - - -@dataclass -class Trip: - """Trip.""" - - start: datetime.date - travel: list[StrDict] = field(default_factory=list) - accommodation: list[StrDict] = field(default_factory=list) - conferences: list[StrDict] = field(default_factory=list) - events: list[StrDict] = field(default_factory=list) - flight_bookings: list[StrDict] = field(default_factory=list) - name: str | None = None - private: bool = False - schengen_compliance: SchengenCalculation | None = None - - @property - def title(self) -> str: - """Trip title.""" - if self.name: - return self.name - titles: list[str] = [conf["name"] for conf in self.conferences] + [ - event["title"] for event in self.events - ] or self.titles_from_travel() - - if not titles: - titles = [acc["location"] for acc in self.accommodation] - - return format_list_with_ampersand(titles) or "[unnamed trip]" - - def titles_from_travel(self) -> list[str]: - """Titles from travel.""" - titles = [] - for travel in self.travel: - if not (depart := (travel["depart"] and utils.as_date(travel["depart"]))): - continue - for when, from_or_to in ((self.start, "from"), (self.end, "to")): - if depart == when: - continue - label = travel.get(from_or_to) - if not isinstance(label, str) or is_generated_drive_stop(label): - continue - if label not in titles: - titles.append(label) - return titles - - @property - def end(self) -> datetime.date | None: - """End date for trip.""" - max_conference_end = ( - max(utils.as_date(item["end"]) for item in self.conferences) - if self.conferences - else datetime.date.min - ) - assert isinstance(max_conference_end, datetime.date) - - arrive = [ - utils.as_date(item["arrive"]) for item in self.travel if "arrive" in item - ] - travel_end = max(arrive) if arrive else datetime.date.min - assert isinstance(travel_end, datetime.date) - - accommodation_end = ( - max(utils.as_date(item["to"]) for item in self.accommodation) - if self.accommodation - else datetime.date.min - ) - assert isinstance(accommodation_end, datetime.date) - - max_date = max(max_conference_end, travel_end, accommodation_end) - return max_date if max_date != datetime.date.min else None - - def locations(self) -> list[tuple[str, Country]]: - """Locations for trip.""" - seen: set[tuple[str, str]] = set() - items = [] - - for item in self.conferences + self.accommodation + self.events: - if "country" not in item or "location" not in item: - continue - key = (item["location"], item["country"]) - if key in seen: - continue - seen.add(key) - - country = agenda.get_country(item["country"]) - assert country - items.append((item["location"], country)) - - return items - - @property - def countries(self) -> list[Country]: - """Countries visited as part of trip, in order.""" - seen: set[str] = set() - items: list[Country] = [] - for item in self.conferences + self.accommodation + self.events: - if "country" not in item: - continue - if item["country"] in seen: - continue - seen.add(item["country"]) - country = agenda.get_country(item["country"]) - assert country - items.append(country) - - for item in self.travel: - travel_countries = set() - if item["type"] == "flight": - for key in "from_airport", "to_airport": - c = item[key]["country"] - travel_countries.add(c) - if item["type"] == "train": - for leg in item["legs"]: - for key in "from_station", "to_station": - c = leg[key]["country"] - travel_countries.add(c) - - for c in travel_countries - seen: - seen.add(c) - country = agenda.get_country(c) - assert country - items.append(country) - - # Don't include GB in countries visited unless entire trip was GB based - return [c for c in items if c.alpha_2 != "GB"] or items - - @functools.cached_property - def show_flags(self) -> bool: - """Show flags for international trips.""" - return len({c for c in self.countries if c.name != "United Kingdom"}) > 1 - - @property - def countries_str(self) -> str: - """List of countries visited on this trip.""" - return format_list_with_ampersand( - [f"{c.name} {c.flag}" for c in self.countries] - ) - - @property - def locations_str(self) -> str: - """List of countries visited on this trip.""" - return format_list_with_ampersand( - [ - f"{location} ({c.name})" + (f" {c.flag}" if self.show_flags else "") - for location, c in self.locations() - ] - ) - - @property - def country_flags(self) -> str: - """Countries flags for trip.""" - return "".join(c.flag for c in self.countries) - - def total_distance(self) -> float: - """Total distance for trip. - - Sums distances for travel items where a distance value is present. - Ignores legs with missing or falsy distances rather than returning None. - """ - total = 0.0 - for t in self.travel: - distance = t.get("distance") - if distance: - total += distance - return total - - def total_co2_kg(self) -> float | None: - """Total CO₂ for trip.""" - return sum(float(t.get("co2_kg", 0)) for t in self.travel) - - @property - def flights(self) -> list[StrDict]: - """Flights.""" - return [item for item in self.travel if item["type"] == "flight"] - - def distances_by_transport_type(self) -> list[tuple[str, float]]: - """Calculate the total distance travelled for each type of transport. - - Any travel item with a missing or None 'distance' field is ignored. - """ - transport_distances: defaultdict[str, float] = defaultdict(float) - - for item in self.travel: - distance = item.get("distance") - if distance: - transport_type: str = item.get("type", "unknown") - transport_distances[transport_type] += distance - - return list(transport_distances.items()) - - def co2_by_transport_type(self) -> list[tuple[str, float]]: - """Calculate the total CO₂ emissions for each type of transport. - - Any travel item with a missing or None 'co2_kg' field is ignored. - """ - transport_co2: defaultdict[str, float] = defaultdict(float) - - for item in self.travel: - co2_kg = item.get("co2_kg") - if co2_kg: - transport_type: str = item.get("type", "unknown") - transport_co2[transport_type] += float(co2_kg) - - return list(transport_co2.items()) - - def elements(self) -> list[TripElement]: - """Trip elements ordered by time.""" - elements: list[TripElement] = [] - - for item in self.accommodation: - title = "Airbnb" if item.get("operator") == "airbnb" else item["name"] - start = TripElement( - start_time=item["from"], - title=title, - detail=item, - element_type="check-in", - ) - - elements.append(start) - - end = TripElement( - start_time=item["to"], - title=title, - detail=item, - element_type="check-out", - ) - - elements.append(end) - - for item in self.conferences: - location_label = item.get("venue") or item.get("location") or item["name"] - country = agenda.get_country(item.get("country")) - - elements.append( - TripElement( - start_time=item["start"], - end_time=item["end"], - title=item["name"], - detail=item, - element_type="conference", - start_loc=location_label, - start_country=country, - end_country=country, - all_day=True, - ) - ) - - for item in self.travel: - if item["type"] == "flight": - name = ( - f"{airport_label(item['from_airport'])} → " - + f"{airport_label(item['to_airport'])}" - ) - - from_country = agenda.get_country(item["from_airport"]["country"]) - to_country = agenda.get_country(item["to_airport"]["country"]) - - elements.append( - TripElement( - start_time=item["depart"], - end_time=item.get("arrive"), - title=name, - detail=item, - element_type="flight", - start_loc=airport_label(item["from_airport"]), - end_loc=airport_label(item["to_airport"]), - start_country=from_country, - end_country=to_country, - ) - ) - if item["type"] == "train": - for leg in item["legs"]: - from_country = agenda.get_country(leg["from_station"]["country"]) - to_country = agenda.get_country(leg["to_station"]["country"]) - - assert from_country and to_country - name = f"{leg['from']} → {leg['to']}" - elements.append( - TripElement( - start_time=leg["depart"], - end_time=leg["arrive"], - title=name, - detail=leg, - element_type="train", - start_loc=leg["from"], - end_loc=leg["to"], - start_country=from_country, - end_country=to_country, - ) - ) - if item["type"] == "ferry": - from_country = agenda.get_country(item["from_terminal"]["country"]) - to_country = agenda.get_country(item["to_terminal"]["country"]) - - name = f"{item['from']} → {item['to']}" - elements.append( - TripElement( - start_time=item["depart"], - end_time=item["arrive"], - title=name, - detail=item, - element_type="ferry", - start_loc=item["from"], - end_loc=item["to"], - start_country=from_country, - end_country=to_country, - ) - ) - if item["type"] in ("coach", "bus"): - from_country = agenda.get_country(item["from_station"]["country"]) - to_country = agenda.get_country(item["to_station"]["country"]) - name = f"{item['from']} → {item['to']}" - elements.append( - TripElement( - start_time=item["depart"], - end_time=item.get("arrive"), - title=name, - detail=item, - element_type=item["type"], - start_loc=item["from"], - end_loc=item["to"], - start_country=from_country, - end_country=to_country, - ) - ) - if item["type"] == "car": - start_location = item.get("from_location", {}) - end_location = item.get("to_location", {}) - from_country = agenda.get_country(start_location.get("country")) - to_country = agenda.get_country(end_location.get("country")) - name = f"{item.get('from', 'Car journey')} → {item.get('to', 'destination')}" - elements.append( - TripElement( - start_time=item["depart"], - end_time=item.get("arrive"), - title=name, - detail=item, - element_type="car", - start_loc=item.get("from"), - end_loc=item.get("to"), - start_country=from_country, - end_country=to_country, - ) - ) - - return sorted(elements, key=lambda e: utils.as_datetime(e.start_time)) - - def elements_grouped_by_day(self) -> list[tuple[datetime.date, list[TripElement]]]: - """Group trip elements by day.""" - # Create a dictionary to hold lists of TripElements grouped by their date - grouped_elements: collections.defaultdict[datetime.date, list[TripElement]] = ( - collections.defaultdict(list) - ) - - for element in self.elements(): - # Extract the date part of the 'when' attribute - day = utils.as_date(element.start_time) - grouped_elements[day].append(element) - - # Sort elements within each day - for day in grouped_elements: - grouped_elements[day].sort( - key=lambda e: ( - e.element_type == "check-in", # check-out elements last - e.element_type != "check-out", # check-in elements first - utils.as_datetime(e.start_time), # then sort by time - ) - ) - - # Convert the dictionary to a sorted list of tuples - grouped_elements_list = sorted(grouped_elements.items()) - - return grouped_elements_list - - -# Example usage: -# You would call the function with your travel list here to get the results. - - -@dataclass +@dataclasses.dataclass class Holiday: """Holiay.""" name: str country: str date: datetime.date - local_name: str | None = None + + +@dataclasses.dataclass +class Event: + """Event.""" + + name: str + date: datetime.date | datetime.datetime + end_date: datetime.date | datetime.datetime | None = None + title: str | None = None + url: str | None = None + going: bool | None = None @property - def display_name(self) -> str: - """Format name for display.""" + def as_datetime(self) -> datetime.datetime: + """Date/time of event.""" + d = self.date + t0 = datetime.datetime.min.time() return ( - f"{self.name} ({self.local_name})" - if self.local_name and self.local_name != self.name - else self.name + d + if isinstance(d, datetime.datetime) + else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) ) + + @property + def has_time(self) -> bool: + """Event has a time associated with it.""" + return isinstance(self.date, datetime.datetime) + + @property + def as_date(self) -> datetime.date: + """Date of event.""" + return ( + self.date.date() if isinstance(self.date, datetime.datetime) else self.date + ) + + @property + def end_as_date(self) -> datetime.date: + """Date of event.""" + return ( + ( + self.end_date.date() + if isinstance(self.end_date, datetime.datetime) + else self.end_date + ) + if self.end_date + else self.as_date + ) + + @property + def display_time(self) -> str | None: + """Time for display on web page.""" + return ( + self.date.strftime("%H:%M") + if isinstance(self.date, datetime.datetime) + else None + ) + + @property + def display_timezone(self) -> str | None: + """Timezone for display on web page.""" + return ( + self.date.strftime("%z") + if isinstance(self.date, datetime.datetime) + else None + ) + + def delta_days(self, today: datetime.date) -> str: + """Return number of days from today as a string.""" + delta = (self.as_date - today).days + + match delta: + case 0: + return "today" + case 1: + return "1 day" + case _: + return f"{delta:,d} days" + + @property + def display_date(self) -> str: + """Date for display on web page.""" + if isinstance(self.date, datetime.datetime): + return self.date.strftime("%a, %d, %b %Y %H:%M %z") + else: + return self.date.strftime("%a, %d, %b %Y") + + @property + def display_title(self) -> str: + """Name for display.""" + return self.title or self.name diff --git a/agenda/uk_holiday.py b/agenda/uk_holiday.py index 3bdbfd2..d1ae03e 100644 --- a/agenda/uk_holiday.py +++ b/agenda/uk_holiday.py @@ -3,38 +3,29 @@ import json import os from datetime import date, datetime, timedelta +from time import time import httpx from dateutil.easter import easter -from .types import Holiday, StrDict - -url = "https://www.gov.uk/bank-holidays.json" +from .types import Holiday -def json_filename(data_dir: str) -> str: - """Filename for cached bank holidays.""" - assert os.path.exists(data_dir) - return os.path.join(data_dir, "bank-holidays.json") - - -async def get_holiday_list(data_dir: str) -> list[StrDict]: - """Download holiday list and save cache.""" - filename = json_filename(data_dir) - - async with httpx.AsyncClient() as client: - r = await client.get(url) - events: list[StrDict] = r.json()["england-and-wales"]["events"] # check valid - open(filename, "w").write(r.text) - return events - - -def bank_holiday_list(start_date: date, end_date: date, data_dir: str) -> list[Holiday]: +async def bank_holiday_list( + start_date: date, end_date: date, data_dir: str +) -> list[Holiday]: """Date and name of the next UK bank holiday.""" - filename = json_filename(data_dir) + url = "https://www.gov.uk/bank-holidays.json" + filename = os.path.join(data_dir, "bank-holidays.json") + mtime = os.path.getmtime(filename) + if (time() - mtime) > 60 * 60 * 6: # six hours + async with httpx.AsyncClient() as client: + r = await client.get(url) + open(filename, "w").write(r.text) + events = json.load(open(filename))["england-and-wales"]["events"] hols: list[Holiday] = [] - for event in json.load(open(filename))["england-and-wales"]["events"]: + for event in events: event_date = datetime.strptime(event["date"], "%Y-%m-%d").date() if event_date < start_date: continue diff --git a/agenda/uk_school_holiday.py b/agenda/uk_school_holiday.py deleted file mode 100644 index 1215da9..0000000 --- a/agenda/uk_school_holiday.py +++ /dev/null @@ -1,249 +0,0 @@ -"""UK school holidays (Bristol) via iCalendar.""" - -from __future__ import annotations - -import datetime -import json -import os - -import httpx - -from .event import Event - -school_holiday_page_url = ( - "https://www.bristol.gov.uk/residents/schools-learning-and-early-years/" - "school-term-and-holiday-dates" -) -school_holiday_ics_url = ( - "https://www.bristol.gov.uk/files/documents/" - "4641-bristol-school-term-and-holiday-dates-2021-2022-and-2022-2023-and-2023-" - "2024-calendar" -) - - -def ics_filename(data_dir: str) -> str: - """Filename for cached school-holiday ICS.""" - assert os.path.exists(data_dir) - return os.path.join(data_dir, "bristol-school-holidays.ics") - - -def json_filename(data_dir: str) -> str: - """Filename for cached parsed school-holiday data.""" - assert os.path.exists(data_dir) - return os.path.join(data_dir, "bristol-school-holidays.json") - - -def _unescape_ics_text(value: str) -> str: - """Decode escaped ICS text values.""" - return ( - value.replace("\\n", " ") - .replace("\\N", " ") - .replace("\\,", ",") - .replace("\\;", ";") - .replace("\\\\", "\\") - ).strip() - - -def unfold_ics_lines(ics_text: str) -> list[str]: - """Unfold folded ICS lines (RFC5545).""" - unfolded: list[str] = [] - for raw_line in ics_text.splitlines(): - line = raw_line.rstrip("\r\n") - if not line: - continue - if unfolded and line[:1] in {" ", "\t"}: - unfolded[-1] += line[1:] - else: - unfolded.append(line) - return unfolded - - -def _parse_ics_date(value: str) -> datetime.date: - """Parse date/date-time values in ICS.""" - value = value.strip() - if "T" in value: - date_part = value.split("T", 1)[0] - return datetime.datetime.strptime(date_part, "%Y%m%d").date() - return datetime.datetime.strptime(value, "%Y%m%d").date() - - -def _is_school_holiday_summary(summary: str) -> bool: - """Return True if summary looks like a school holiday event.""" - lower = summary.lower() - if "holiday" not in lower: - return False - if "bank holiday" in lower: - return False - return True - - -def _clean_summary(summary: str) -> str: - """Normalise holiday summary text for display.""" - summary = _unescape_ics_text(summary) - # The feed embeds long policy notes in parentheses after the name. - if " (" in summary: - summary = summary.split(" (", 1)[0] - return summary.strip() - - -def parse_school_holidays_from_ics(ics_text: str) -> list[Event]: - """Parse school holiday ranges from an ICS file as Events.""" - events: list[Event] = [] - current: dict[str, str] = {} - - def flush_current() -> None: - summary = current.get("SUMMARY") - dtstart = current.get("DTSTART") - dtend = current.get("DTEND") - if not summary or not dtstart or not dtend: - return - - clean_summary = _clean_summary(summary) - if not _is_school_holiday_summary(clean_summary): - return - - start_date = _parse_ics_date(dtstart) - end_exclusive = _parse_ics_date(dtend) - end_date = end_exclusive - datetime.timedelta(days=1) - if end_date < start_date: - return - - events.append( - Event( - name="uk_school_holiday", - date=start_date, - end_date=end_date, - title=clean_summary, - url=school_holiday_page_url, - ) - ) - - for line in unfold_ics_lines(ics_text): - if line == "BEGIN:VEVENT": - current = {} - continue - if line == "END:VEVENT": - flush_current() - current = {} - continue - - if ":" not in line: - continue - - key_part, value = line.split(":", 1) - key = key_part.split(";", 1)[0].upper() - - if key in {"SUMMARY", "DTSTART", "DTEND"}: - current[key] = value.strip() - - # De-duplicate by title/date-range. - unique: dict[tuple[str, datetime.date, datetime.date], Event] = {} - for event in events: - end_date = event.end_as_date - unique[(event.title or event.name, event.as_date, end_date)] = event - - return sorted(unique.values(), key=lambda item: (item.as_date, item.end_as_date)) - - -def write_school_holidays_json(events: list[Event], data_dir: str) -> None: - """Write parsed school-holiday events to JSON cache.""" - filename = json_filename(data_dir) - payload: list[dict[str, str]] = [ - { - "name": event.name, - "title": event.title or event.name, - "start": event.as_date.isoformat(), - "end": event.end_as_date.isoformat(), - "url": event.url or "", - } - for event in events - ] - with open(filename, "w", encoding="utf-8") as out: - json.dump(payload, out, indent=2) - - -def read_school_holidays_json(data_dir: str) -> list[Event]: - """Read parsed school-holiday events from JSON cache.""" - filename = json_filename(data_dir) - if not os.path.exists(filename): - return [] - - with open(filename, encoding="utf-8") as in_file: - loaded = json.load(in_file) - if not isinstance(loaded, list): - return [] - - parsed_events: list[Event] = [] - for raw_item in loaded: - if not isinstance(raw_item, dict): - continue - title = raw_item.get("title") - start_value = raw_item.get("start") - end_value = raw_item.get("end") - if not ( - isinstance(title, str) - and isinstance(start_value, str) - and isinstance(end_value, str) - ): - continue - - try: - start_date = datetime.date.fromisoformat(start_value) - end_date = datetime.date.fromisoformat(end_value) - except ValueError: - continue - - event_url = raw_item.get("url") - parsed_events.append( - Event( - name="uk_school_holiday", - date=start_date, - end_date=end_date, - title=title, - url=event_url if isinstance(event_url, str) and event_url else None, - ) - ) - - return sorted(parsed_events, key=lambda item: (item.as_date, item.end_as_date)) - - -def school_holiday_list( - start_date: datetime.date, - end_date: datetime.date, - data_dir: str, -) -> list[Event]: - """Get cached school-holiday events overlapping the supplied range.""" - items = read_school_holidays_json(data_dir) - return [ - item - for item in items - if item.as_date <= end_date and item.end_as_date >= start_date - ] - - -async def get_holiday_list(data_dir: str) -> list[Event]: - """Download, parse and cache school-holiday data.""" - headers = { - "User-Agent": ( - "Mozilla/5.0 (X11; Linux x86_64) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36" - ), - "Accept": "text/calendar,*/*;q=0.9", - "Referer": school_holiday_page_url, - } - - async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client: - response = await client.get(school_holiday_ics_url, headers=headers) - response.raise_for_status() - - content_type = response.headers.get("content-type", "") - ics_text = response.text - if "text/calendar" not in content_type and "BEGIN:VCALENDAR" not in ics_text: - raise ValueError("School holiday ICS download did not return calendar content") - - with open(ics_filename(data_dir), "w", encoding="utf-8") as out: - out.write(ics_text) - - events = parse_school_holidays_from_ics(ics_text) - write_school_holidays_json(events, data_dir) - return events diff --git a/agenda/utils.py b/agenda/utils.py deleted file mode 100644 index 8c92790..0000000 --- a/agenda/utils.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Utility functions.""" - -import os -import typing -from datetime import date, datetime, time, timedelta, timezone -from time import time as unixtime -from zoneinfo import ZoneInfo - -StrDict = dict[str, typing.Any] - - -def as_date(d: datetime | date) -> date: - """Convert datetime to date.""" - match d: - case datetime(): - return d.date() - case date(): - return d - case _: - raise TypeError(f"Unsupported type: {type(d)}") - - -def as_datetime(d: datetime | date) -> datetime: - """Date/time of event.""" - match d: - case datetime(): - return d - case date(): - return datetime.combine(d, datetime.min.time()).replace(tzinfo=timezone.utc) - case _: - raise TypeError(f"Unsupported type: {type(d)}") - - -def timedelta_display(delta: timedelta) -> str: - """Format timedelta as a human readable string.""" - total_seconds = int(delta.total_seconds()) - days, remainder = divmod(total_seconds, 24 * 60 * 60) - hours, remainder = divmod(remainder, 60 * 60) - mins, secs = divmod(remainder, 60) - - return " ".join( - f"{v} {label}" - for v, label in ((days, "days"), (hours, "hrs"), (mins, "mins")) - if v - ) - - -def plural(value: int, unit: str) -> str: - """Value + unit with unit written as singular or plural as appropriate.""" - return f"{value} {unit}{'s' if value > 1 else ''}" - - -def human_readable_delta(future_date: date) -> str | None: - """ - Calculate the human-readable time delta for a given future date. - - Args: - future_date (date): The future date as a datetime.date object. - - Returns: - str: Human-readable time delta. - """ - # Ensure the input is a future date - if future_date <= date.today(): - return None - - # Calculate the delta - delta = future_date - date.today() - - # Convert delta to a more human-readable format - months, days = divmod(delta.days, 30) - weeks, days = divmod(days, 7) - - # Formatting the output - parts = [ - plural(value, unit) - for value, unit in ((months, "month"), (weeks, "week"), (days, "day")) - if value > 0 - ] - return " ".join(parts) if parts else None - - -def filename_timestamp(filename: str, ext: str) -> tuple[datetime, str] | None: - """Get datetime from filename.""" - try: - ts = datetime.strptime(filename, f"%Y-%m-%d_%H:%M:%S.{ext}") - except ValueError: - return None - return (ts, filename) - - -def get_most_recent_file(directory: str, ext: str) -> str | None: - """Get most recent file from directory.""" - existing = [ - x for x in (filename_timestamp(f, ext) for f in os.listdir(directory)) if x - ] - if not existing: - return None - existing.sort(reverse=True) - return os.path.join(directory, existing[0][1]) - - -def make_waste_dir(data_dir: str) -> None: - """Make waste dir if missing.""" - waste_dir = os.path.join(data_dir, "waste") - if not os.path.exists(waste_dir): - os.mkdir(waste_dir) - - -async def time_function( - name: str, - func: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], - *args: typing.Any, - **kwargs: typing.Any, -) -> tuple[str, typing.Any, float, Exception | None]: - """Time the execution of an asynchronous function.""" - start_time, result, exception = unixtime(), None, None - try: - result = await func(*args, **kwargs) - except Exception as e: - exception = e - end_time = unixtime() - return name, result, end_time - start_time, exception - - -def depart_datetime(item: StrDict) -> datetime: - """Return a datetime for this travel item. - - If the travel item already has a datetime return that, otherwise if the - departure time is just a date return midnight UTC for that date. - """ - depart = item["depart"] - if isinstance(depart, datetime): - return depart - return datetime.combine(depart, time.min).replace(tzinfo=ZoneInfo("UTC")) diff --git a/agenda/waste_schedule.py b/agenda/waste_schedule.py new file mode 100644 index 0000000..4f4dade --- /dev/null +++ b/agenda/waste_schedule.py @@ -0,0 +1,209 @@ +"""Waste collection schedules.""" + +import json +import os +import re +import typing +from collections import defaultdict +from datetime import date, datetime, time, timedelta + +import httpx +import lxml.html + +from . import uk_time +from .types import Event + +ttl_hours = 12 + + +def make_waste_dir(data_dir: str) -> None: + """Make waste dir if missing.""" + waste_dir = os.path.join(data_dir, "waste") + if not os.path.exists(waste_dir): + os.mkdir(waste_dir) + + +async def get_html(data_dir: str, postcode: str, uprn: str) -> str: + """Get waste schedule.""" + now = datetime.now() + waste_dir = os.path.join(data_dir, "waste") + + make_waste_dir(data_dir) + + existing_data = os.listdir(waste_dir) + existing = [f for f in existing_data if f.endswith(".html")] + if existing: + recent_filename = max(existing) + recent = datetime.strptime(recent_filename, "%Y-%m-%d_%H:%M.html") + delta = now - recent + + if existing and delta < timedelta(hours=ttl_hours): + return open(os.path.join(waste_dir, recent_filename)).read() + + now_str = now.strftime("%Y-%m-%d_%H:%M") + filename = f"{waste_dir}/{now_str}.html" + + forms_base_url = "https://forms.n-somerset.gov.uk" + # url2 = "https://forms.n-somerset.gov.uk/Waste/CollectionSchedule/ViewSchedule" + url = "https://forms.n-somerset.gov.uk/Waste/CollectionSchedule" + async with httpx.AsyncClient() as client: + r = await client.post( + url, + data={ + "PreviousHouse": "", + "PreviousPostcode": "-", + "Postcode": postcode, + "SelectedUprn": uprn, + }, + ) + form_post_html = r.text + pattern = r'

Object moved to here<\/a>\.<\/h2>' + m = re.search(pattern, form_post_html) + if m: + r = await client.get(forms_base_url + m.group(1)) + html = r.text + open(filename, "w").write(html) + return html + + +def parse_waste_schedule_date(day_and_month: str) -> date: + """Parse waste schedule date.""" + today = date.today() + this_year = today.year + date_format = "%A %d %B %Y" + d = datetime.strptime(f"{day_and_month} {this_year}", date_format).date() + if d < today: + d = datetime.strptime(f"{day_and_month} {this_year + 1}", date_format).date() + return d + + +def parse(root: lxml.html.HtmlElement) -> list[Event]: + """Parse waste schedule.""" + tbody = root.find(".//table/tbody") + assert tbody is not None + by_date = defaultdict(list) + for e_service, e_next_date, e_following in tbody: + assert e_service.text and e_next_date.text and e_following.text + service = e_service.text + next_date = parse_waste_schedule_date(e_next_date.text) + following_date = parse_waste_schedule_date(e_following.text) + + by_date[next_date].append(service) + by_date[following_date].append(service) + + return [ + Event( + name="waste_schedule", + date=uk_time(d, time(6, 30)), + title="🗑️ Backwell: " + ", ".join(services), + ) + for d, services in by_date.items() + ] + + +BristolSchedule = list[dict[str, typing.Any]] + + +async def get_bristol_data(data_dir: str, uprn: str) -> BristolSchedule: + """Get Bristol Waste schedule, with cache.""" + now = datetime.now() + waste_dir = os.path.join(data_dir, "waste") + + make_waste_dir(data_dir) + + existing_data = os.listdir(waste_dir) + existing = [f for f in existing_data if f.endswith(f"_{uprn}.json")] + if existing: + recent_filename = max(existing) + recent = datetime.strptime(recent_filename, f"%Y-%m-%d_%H:%M_{uprn}.json") + delta = now - recent + + def get_from_recent() -> BristolSchedule: + json_data = json.load(open(os.path.join(waste_dir, recent_filename))) + return typing.cast(BristolSchedule, json_data["data"]) + + if existing and delta < timedelta(hours=ttl_hours): + return get_from_recent() + + try: + r = await get_bristol_gov_uk_data(uprn) + except httpx.ReadTimeout: + return get_from_recent() + + with open(f'{waste_dir}/{now.strftime("%Y-%m-%d_%H:%M")}_{uprn}.json', "wb") as out: + out.write(r.content) + + return typing.cast(BristolSchedule, r.json()["data"]) + + +async def get_bristol_gov_uk_data(uprn: str) -> httpx.Response: + """Get JSON from Bristol City Council.""" + UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" + HEADERS = { + "Accept": "*/*", + "Accept-Language": "en-GB,en;q=0.9", + "Connection": "keep-alive", + "Ocp-Apim-Subscription-Key": "47ffd667d69c4a858f92fc38dc24b150", + "Ocp-Apim-Trace": "true", + "Origin": "https://bristolcouncil.powerappsportals.com", + "Referer": "https://bristolcouncil.powerappsportals.com/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", + "Sec-GPC": "1", + "User-Agent": UA, + } + + _uprn = str(uprn).zfill(12) + + async with httpx.AsyncClient(timeout=20) as client: + # Initialise form + payload = {"servicetypeid": "7dce896c-b3ba-ea11-a812-000d3a7f1cdc"} + response = await client.get( + "https://bristolcouncil.powerappsportals.com/completedynamicformunauth/", + headers=HEADERS, + params=payload, + ) + + host = "bcprdapidyna002.azure-api.net" + + # Set the search criteria + payload = {"Uprn": "UPRN" + _uprn} + response = await client.post( + f"https://{host}/bcprdfundyna001-llpg/DetailedLLPG", + headers=HEADERS, + json=payload, + ) + + # Retrieve the schedule + payload = {"uprn": _uprn} + response = await client.post( + f"https://{host}/bcprdfundyna001-alloy/NextCollectionDates", + headers=HEADERS, + json=payload, + ) + + return response + + +async def get_bristol_gov_uk(start_date: date, data_dir: str, uprn: str) -> list[Event]: + """Get waste collection schedule from Bristol City Council.""" + data = await get_bristol_data(data_dir, uprn) + + by_date: defaultdict[date, list[str]] = defaultdict(list) + + for item in data: + service = item["containerName"] + service = "Recycling" if "Recycling" in service else service.partition(" ")[2] + for collection in item["collection"]: + for collection_date_key in ["nextCollectionDate", "lastCollectionDate"]: + d = date.fromisoformat(collection[collection_date_key][:10]) + if d < start_date: + continue + if service not in by_date[d]: + by_date[d].append(service) + + return [ + Event(name="waste_schedule", date=d, title="🗑️ Bristol: " + ", ".join(services)) + for d, services in by_date.items() + ] diff --git a/agenda/weather.py b/agenda/weather.py deleted file mode 100644 index ad0fc0c..0000000 --- a/agenda/weather.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Weather forecast using OpenWeatherMap One Call API.""" - -import json -import os -from datetime import datetime - -import pyowm - - -def _cache_path(data_dir: str, lat: float, lon: float) -> str: - """Path for weather cache file.""" - weather_dir = os.path.join(data_dir, "weather") - os.makedirs(weather_dir, exist_ok=True) - return os.path.join(weather_dir, f"{lat:.2f}_{lon:.2f}.json") - - -def _is_fresh(path: str, max_age_hours: int = 24) -> bool: - """Return True if the cache file exists and is recent enough.""" - if not os.path.exists(path): - return False - age = datetime.now().timestamp() - os.path.getmtime(path) - return age < max_age_hours * 3600 - - -def get_forecast( - data_dir: str, - api_key: str, - lat: float, - lon: float, - cache_only: bool = False, -) -> list[dict]: - """Return 8-day daily forecast for lat/lon, caching results for 24 hours. - - If cache_only=True, return cached data if available (even if stale) and - never call the API. Returns [] if no cache exists. - """ - cache_file = _cache_path(data_dir, lat, lon) - - if _is_fresh(cache_file): - with open(cache_file) as f: - return json.load(f) # type: ignore[no-any-return] - - if cache_only: - if os.path.exists(cache_file): - with open(cache_file) as f: - return json.load(f) # type: ignore[no-any-return] - return [] - - owm = pyowm.OWM(api_key) - mgr = owm.weather_manager() - result = mgr.one_call(lat=lat, lon=lon) - - forecasts = [] - for day in result.forecast_daily: - dt = datetime.fromtimestamp(day.ref_time) - temp = day.temperature("celsius") - forecasts.append( - { - "date": dt.date().isoformat(), - "status": day.status, - "detailed_status": day.detailed_status, - "temp_min": round(temp["min"]), - "temp_max": round(temp["max"]), - "precipitation_probability": day.precipitation_probability, - "icon": day.weather_icon_name, - } - ) - - with open(cache_file, "w") as f: - json.dump(forecasts, f) - - return forecasts - - -def trip_latlon(trip: object) -> tuple[float, float] | None: - """Return (lat, lon) for the primary destination of a trip, or None.""" - from agenda.types import Trip - - assert isinstance(trip, Trip) - for item in list(trip.accommodation) + list(trip.conferences): - if "latitude" in item and "longitude" in item: - return (float(item["latitude"]), float(item["longitude"])) - return None - - -def get_trip_weather( - data_dir: str, - api_key: str, - trip: object, - cache_only: bool = False, -) -> dict[str, dict]: - """Return forecast for a trip keyed by date ISO string. - - Returns an empty dict if no location is known or the API call fails. - If cache_only=True, never call the API (returns stale or empty data). - """ - latlon = trip_latlon(trip) - if not latlon: - return {} - lat, lon = latlon - try: - forecasts = get_forecast(data_dir, api_key, lat, lon, cache_only=cache_only) - except Exception: - return {} - return {f["date"]: f for f in forecasts} diff --git a/docs/personal-data-yaml.md b/docs/personal-data-yaml.md deleted file mode 100644 index c0e2ec5..0000000 --- a/docs/personal-data-yaml.md +++ /dev/null @@ -1,1002 +0,0 @@ -# Personal Data YAML Formats - -This document describes the YAML files read from `../personal-data/`. It is intended for humans and LLMs generating new entries. - -## General Rules - -- Use YAML lists for most files. `airports.yaml` is a mapping keyed by IATA code. -- Use ISO-like YAML dates and datetimes: - - Date: `2026-03-14` - - Datetime with timezone: `2026-03-14 09:30:00+01:00` -- Use lowercase ISO 3166-1 alpha-2 country codes, for example `gb`, `be`, `us`. -- Use quoted strings for prices and identifiers that might otherwise be parsed as numbers: `'154.34'`, `'06525269'`, `'0042'`. -- Currencies must be in `config.CURRENCIES` or `GBP`. -- Travel and trip-related entries are grouped by the `trip` date. That date should match an entry in `trips.yaml` when a named trip is needed, but trip groups can also be created from travel/accommodation/conference entries. -- Keep chronological files sorted by their natural start field. `validate_yaml.py` checks ordering for trips, flights, trains, ferries, conferences, and accommodation. -- Preserve the existing whitespace style. Long top-level list files such as `accommodation.yaml`, `buses.yaml`, `car_journeys.yaml`, `coaches.yaml`, `conferences.yaml`, `ferries.yaml`, `flights.yaml`, `stations.yaml`, `trains.yaml`, and `trips.yaml` use one blank line between top-level items. Mapping files such as `airports.yaml` do not use this list-item spacing. -- Coordinates are `latitude` then `longitude`, both numeric. - -## Cross-File References - -- `flights.yaml` flight `airline` values reference `airlines.yaml` `iata`. -- `flights.yaml` flight `from` and `to` values reference `airports.yaml` keys. -- `trains.yaml` journey and leg `from` and `to` values reference `stations.yaml` `name`. -- `ferries.yaml` `from` and `to` values reference `ferry_terminals.yaml` `name`. -- `buses.yaml` `from` and `to` values reference `bus_stops.yaml` `name`. -- `coaches.yaml` `from` and `to` values reference `coach_stations.yaml` `name`. -- `car_journeys.yaml` `route` values name files in `car_routes/`. The `.geojson` extension is optional. -- Station, stop, and terminal `routes` values name GeoJSON files without the `.geojson` extension. - -## `accommodation.yaml` - -Top-level shape: list of accommodation stays. - -Used by: agenda events, trip pages, trip maps, busy/location logic. - -Required fields: - -- `type`: accommodation category such as `hotel`, `apartment`, `airbnb`. -- `name`: property name. -- `country`: lowercase country code. -- `location`: city or place name. -- `trip`: trip start date. -- `from`: check-in datetime. -- `to`: check-out datetime. - -Common optional fields: - -- Booking: `operator`, `booking_reference`, `confirmation_code`, `booking_url`, `url`, `email`, `phone`. -- Money: `price`, `currency`, `room_rate`, `estimated_taxes`, `estimated_additional_fees`. -- Room/stay: `address`, `room_type`, `room_name`, `room_number`, `number_of_adults`, `breakfast_included`, `breakfast`, `cancellation_policy`, `free_cancellation`, `refundable`. -- Coordinates/IDs: `latitude`, `longitude`, `timezone`, `osm_node`, `wikidata`. -- Loyalty: `rewards`, `radisson_rewards_number`. - -Example: - -```yaml -- type: hotel - operator: Example Hotels - name: Example Central Hotel - location: Brussels - country: be - trip: 2026-02-06 - from: 2026-02-06 15:00:00+01:00 - to: 2026-02-09 11:00:00+01:00 - address: 1 Example Street, Brussels - confirmation_code: ABC123 - price: '312.50' - currency: EUR - number_of_adults: 1 - room_type: Standard double - breakfast_included: true - latitude: 50.8466 - longitude: 4.3528 -``` - -## `airlines.yaml` - -Top-level shape: list of airlines. - -Used by: flight loading and display. - -Required fields: - -- `iata`: two-character IATA airline code. -- `icao`: three-character ICAO airline code. -- `name`: airline name. - -Optional fields: - -- `flight_number_prefer_icao`: boolean. When true, display flight numbers with the ICAO code instead of the IATA code. - -Example: - -```yaml -- iata: BA - icao: BAW - name: British Airways -- iata: U2 - icao: EZY - name: easyJet - flight_number_prefer_icao: true -``` - -## `airports.yaml` - -Top-level shape: mapping keyed by IATA airport code. - -Used by: flight loading, distance calculation, maps, unbooked route hints. - -Required fields for each airport: - -- `iata`: IATA code. Should match the mapping key. -- `name`: airport name. -- `city`: city or main served place. -- `country`: lowercase country code. -- `latitude`, `longitude`: numeric coordinates. -- `qid`: Wikidata QID. - -Optional fields: - -- `alt_name`: display name override used in labels. -- `elevation`: metres. -- `website`, `url`. - -Example: - -```yaml -BRU: - iata: BRU - name: Brussels Airport - city: Brussels - country: be - qid: Q220613 - latitude: 50.9014 - longitude: 4.4844 - elevation: 56 - website: https://www.brusselsairport.be/ -``` - -## `bus_stops.yaml` - -Top-level shape: list of bus stops. - -Used by: bus trip loading, maps, route rendering. - -Required fields: - -- `name`: stop name referenced by `buses.yaml`. -- `city`: city or place. -- `country`: lowercase country code. -- `latitude`, `longitude`: numeric coordinates. -- `routes`: mapping from destination stop name to GeoJSON filename without `.geojson`. - -Optional fields: - -- `Atco`: UK ATCO stop code. -- `osm_node`. - -Example: - -```yaml -- name: West Street - city: Bristol - country: gb - Atco: '0100BRA10073' - osm_node: 485403178 - latitude: 51.4393854 - longitude: -2.6017977 - routes: - Bristol Airport: West_Street_to_Bristol_Airport -``` - -## `buses.yaml` - -Top-level shape: list of bus journeys. - -Used by: trip loading, maps, trip timeline. Bus journeys are not counted for Schengen tracking. - -Required fields: - -- `trip`: trip start date. -- `depart`: departure datetime. -- `arrive`: arrival datetime. `validate_yaml.py` requires arrival after departure and duration no more than 12 hours. -- `from`, `to`: names from `bus_stops.yaml`. - -Optional fields: - -- `operator`, `price`, `currency`. - -Example: - -```yaml -- trip: 2026-03-14 - depart: 2026-03-14 08:20:00+00:00 - arrive: 2026-03-14 08:55:00+00:00 - from: West Street - to: Bristol Airport - operator: First Bus - price: '2.00' - currency: GBP -``` - -## `coach_stations.yaml` - -Top-level shape: list of coach stations. - -Used by: coach trip loading, maps, route rendering. - -Fields are the same pattern as `bus_stops.yaml`, except entries describe coach stations. - -Example: - -```yaml -- name: Example Coach Station - city: Example City - country: gb - latitude: 51.4500 - longitude: -2.5800 - routes: - Other Coach Station: example_city_to_other_city -``` - -## `coaches.yaml` - -Top-level shape: list of coach journeys. - -Used by: trip loading, maps, trip timeline. Coach journeys are not counted for Schengen tracking. - -Required fields: - -- `trip`, `depart`, `arrive`, `from`, `to`. -- `from` and `to` must be names from `coach_stations.yaml`. - -Optional fields: - -- `operator`, `class`, `booking_reference`, `price`, `currency`, `price_details`. - -Example: - -```yaml -- booking_reference: ABC123 - trip: 2026-05-25 - price: '55.00' - currency: GBP - depart: 2026-05-26 14:45:00+01:00 - arrive: 2026-05-26 18:30:00+01:00 - from: Example Coach Station - to: Other Coach Station - operator: Example Coaches - class: Standard - price_details: - base_fare: '55.00' -``` - -## `car_journeys.yaml` - -Top-level shape: list of car journeys. - -Used by: trip loading, maps, trip timeline. Car journeys are for driving your own car or a rental car. - -Required fields: - -- `trip`: trip start date. -- `depart`: departure date or datetime. -- `arrive`: arrival date or datetime. -- `route`: GeoJSON filename in `car_routes/`, with or without the `.geojson` extension. - -Optional fields: - -- `from`, `to`: endpoint labels. If omitted and the route filename uses `A_to_B`, labels are inferred from the filename. -- `show_markers`: boolean. When true, render both car endpoint markers on maps. Defaults to false because car endpoints often duplicate airport, accommodation, ferry terminal, or conference pins. Home endpoints still render by default. -- `from_show_marker`, `to_show_marker`: booleans. Per-endpoint marker overrides. -- `from_type`, `to_type`: marker type override used when that endpoint marker is shown. Use `home` to render a house icon. Otherwise car endpoints render as car markers. -- `operator`, `vehicle`, `price`, `currency`, `distance`. - -The route distance is calculated from the GeoJSON when `distance` is not present. Car routes render on maps by default. Non-home endpoint pins are opt-in. If an endpoint label is `home`, `PCH`, or `Picture House Court`, the marker is rendered as a house by default. - -Example: - -```yaml -- trip: 2026-07-16 - depart: 2026-07-16 - arrive: 2026-07-16 - route: PCH_to_EMF.geojson - -- trip: 2026-07-16 - depart: 2026-07-19 - arrive: 2026-07-19 - route: EMF_to_PCH.geojson -``` - -## `conferences.yaml` - -Top-level shape: list of conferences and conference-like events. - -Used by: agenda events, trip pages, trip maps, conference list, CFP reminders. - -Required fields: - -- `name`: event name. -- `topic`: topic/category. -- `location`: city or location label. -- Date information, either as legacy top-level `start` and `end`, or preferred nested `dates`. - -Preferred `dates` fields: - -- `status`: one of `exact`, `tentative`, or `approximate`. -- For `exact` and `tentative`: `start` and `end` dates/datetimes. `end` must be no earlier than `start`, and duration must be under 20 days. -- For `approximate`: `earliest` and `latest` dates for sorting/past-future filtering. -- `label`: optional human-readable date text. Recommended for `tentative` and `approximate`, for example `likely first weekend of February 2027` or `March 2027`. -- `basis`: optional explanation of why a tentative date is expected. - -Date status behavior: - -- `exact`: confirmed dates. These create agenda events, iCalendar entries, and timeline bars. -- `tentative`: guessed or unconfirmed exact dates. These appear on the conference list with a status badge, but do not create agenda/iCalendar events or timeline bars. -- `approximate`: only a broad date range is known. These appear on the conference list with a status badge, but do not create agenda/iCalendar events or timeline bars. - -Legacy fields: - -- Existing top-level `start` and `end` are still supported and are treated as `exact` unless `date_status` says otherwise. - -Common optional fields: - -- Series: `series`, a key from `conference_series.yaml`. -- Trip/location: `trip`, `country`, `venue`, `address`, `latitude`, `longitude`. -- Attendance: `going`, `registered`, `speaking`, `online`, `accommodation_booked`, `transport_booked`. -- Partial attendance: `attend_start`, `attend_end`. These may be dates or timezone-aware datetimes and are used on trip pages instead of official dates. -- Web/CFP: `url`, `cfp_end`, `cfp_url`, `hashtag`, `description`. -- Money/tickets: `free`, `price`, `currency`, `ticket_type`. -- Other flags: `hackathon`, `attendees`. - -Exact example: - -```yaml -- name: FOSDEM - series: fosdem - topic: FOSDEM - location: Brussels - country: be - trip: 2026-02-06 - dates: - status: exact - start: 2026-02-07 - end: 2026-02-08 - attend_start: 2026-02-07 14:00:00+01:00 - attend_end: 2026-02-08 - going: true - registered: true - accommodation_booked: true - transport_booked: true - url: https://fosdem.org/2026/ - venue: Universite Libre de Bruxelles - address: Av. Franklin Roosevelt 50, 1050 Bruxelles, Belgium - latitude: 50.8132 - longitude: 4.3822 -``` - -Tentative example: - -```yaml -- name: FOSDEM - series: fosdem - topic: FOSDEM - location: Brussels - country: be - dates: - status: tentative - start: 2027-01-30 - end: 2027-01-31 - label: likely first weekend of February 2027 - basis: FOSDEM is usually on the weekend where Sunday is the first Sunday in February - url: https://fosdem.org/2027/ -``` - -Approximate examples: - -```yaml -- name: Wikimedia Hackathon 2027 - series: wikimedia-hackathon - topic: Wikimedia - location: Albania - country: al - dates: - status: approximate - label: mid-April 2027 - earliest: 2027-04-11 - latest: 2027-04-20 - hackathon: true - -- name: PyCascades 2027 - series: pycascades - topic: Python - location: TBC - dates: - status: approximate - label: March 2027 - earliest: 2027-03-01 - latest: 2027-03-31 -``` - -## `conference_series.yaml` - -Top-level shape: mapping from stable series ID to series metadata. - -Used by: conference list pages, conference series index/detail pages, and validation of `conferences.yaml` `series` references. - -Required fields for each series: - -- `name`: display name for the series. - -Common optional fields: - -- `topic`: default topic/category. -- `cadence`: for example `annual` or `recurring`. -- `usual_location`: common city/place when the event usually stays in one place. -- `country`: common lowercase country code when stable. -- `url`: series homepage. -- `notes`: free-text generation or scheduling notes. - -Example: - -```yaml -fosdem: - name: FOSDEM - topic: FOSDEM - cadence: annual - usual_location: Brussels - country: be - url: https://fosdem.org/ - notes: Usually the weekend where Sunday is the first Sunday in February. - -geomob-london: - name: Geomob London - topic: Maps - cadence: recurring - usual_location: London - country: gb - url: https://thegeomob.com/ -``` - -## `entities.yaml` - -Top-level shape: list of people/entities. - -Used by: birthday events. - -Required fields for birthday support: - -- `name`: full name. -- `label`: display name. -- `type`: for example `human`. -- `birthday`: mapping with `day`, `month`, and optionally `year`. - -Optional fields: - -- `relation`, `email`. - -If `birthday.year` is omitted, age is shown as unknown. - -Example: - -```yaml -- name: Ada Example - label: Ada - type: human - relation: friend - birthday: - day: 10 - month: 12 - year: 1990 -``` - -## `events.yaml` - -Top-level shape: list of general events. - -Used by: agenda events and trip pages. - -Required fields: - -- `name`: event type. -- One date source: - - `date`: single event date/datetime, or - - `start_date`: used for events with a separate `end_date`, or - - `rrule`: recurrence rule string. - -Optional fields: - -- `title`: display title. -- `end_date`: explicit end date/datetime. -- `duration`: ISO 8601 duration such as `PT2H`, `P1D`. -- `url`. -- Trip/map fields: `trip`, `location`, `country`, `venue`, `address`, `latitude`, `longitude`. - -Special cases: - -- For `name: travel_insurance`, the event date field is `end_date`; no `end_date` is attached to the generated event. -- For recurring events, if the `rrule` has no `BYHOUR`, `BYMINUTE`, or `BYSECOND`, generated events are all-day dates. Otherwise generated datetimes are localized to UK time. -- `skip_trips=True` consumers ignore entries with `trip`. - -Examples: - -```yaml -- name: travel_insurance - start_date: 2026-05-04 - end_date: 2027-05-03 - -- name: meetup - title: Example Geo Meetup - date: 2026-06-18 18:30:00+01:00 - duration: PT2H - url: https://example.org/meetup - location: Bristol - country: gb - latitude: 51.4545 - longitude: -2.5879 - -- name: market - title: Monthly Example Market - rrule: FREQ=MONTHLY;BYDAY=1SA -``` - -## `ferries.yaml` - -Top-level shape: list of ferry journeys. - -Used by: trip loading, maps, trip timeline, Schengen tracking. - -Required fields: - -- `trip`: trip start date. -- `depart`, `arrive`: datetimes. Ferry `arrive` is required. -- `from`, `to`: names from `ferry_terminals.yaml`. - -Common optional fields: - -- `operator`, `ferry`, `direction`, `class`, `booking_reference`, `price`, `currency`. -- `price_details`: free-form mapping of fare components. -- `vehicle`: mapping with fields such as `type`, `registration`, `height`, `length`, `extras`. - -Example: - -```yaml -- booking_reference: ABC123 - trip: 2026-05-04 - price: '302.00' - currency: GBP - depart: 2026-05-04 23:00:00+01:00 - arrive: 2026-05-05 08:00:00+02:00 - from: Portsmouth - to: Cherbourg - operator: Brittany Ferries - class: Commodore cabin - price_details: - base_fare: '153.00' - cabin: '149.00' - vehicle: - type: Example car - registration: AB12CDE - height: 1.63m - length: 4.15m -``` - -## `ferry_terminals.yaml` - -Top-level shape: list of ferry terminals. - -Used by: ferry loading and route rendering. - -Required fields: - -- `name`: terminal name referenced by `ferries.yaml`. -- `city`, `country`. -- `latitude`, `longitude`. -- `routes`: mapping from destination terminal name to GeoJSON filename without `.geojson`. Ferry route rendering expects a GeoJSON route. - -Optional fields: - -- `osm_node`, `osm_way`. - -Example: - -```yaml -- name: Portsmouth - city: Portsmouth - country: gb - osm_way: 123456 - latitude: 50.8120 - longitude: -1.0880 - routes: - Cherbourg: portsmouth_cherbourg -``` - -## `flight_destinations.yaml` - -Top-level shape: list of origin rules for unbooked conference flight route hints. - -Used by: trip maps when a trip has conferences but no booked travel. - -Required fields: - -- `origin`: origin airport IATA code. -- `airline`: airline IATA code. Currently loaded for validation/description but not used in origin selection. -- `destinations`: list of destination airport IATA codes. - -Example: - -```yaml -- origin: BRS - airline: U2 - destinations: - - AMS - - BCN - - CDG -``` - -## `flights.yaml` - -Top-level shape: list of flight bookings. Each booking contains one or more flight legs. - -Used by: agenda transport events, trip loading, maps, distance calculation. - -Required booking fields: - -- `trip`: trip start date. -- `flights`: list of flight leg mappings. - -Common optional booking fields: - -- `booking_reference`, `price`, `currency`. - -Required flight leg fields: - -- `depart`: departure datetime. -- `from`, `to`: airport IATA codes from `airports.yaml`. -- `flight_number`: numeric/string flight number without airline prefix. -- `airline`: airline IATA code from `airlines.yaml`. - -Common optional flight leg fields: - -- Time/location: `arrive`, `from_terminal`, `to_terminal`, `duration`. -- Seat/cabin: `seat`, `seat_type`, `class`, `cabin`. -- Aircraft: `plane`, `registration`. -- Tracking: `distance`, `co2_kg`, `openflights_trip`, `reason`. -- Ticket/passenger: `e_ticket_number`, `ticket_number`, `frequent_flyer_number`, `passenger_name`, `passengers`, `baggage`, `payment_details`. - -`validate_yaml.py` checks that every booking has `trip`, all flight airlines exist in `airlines.yaml`, bookings are sorted by first departure, and currencies are configured. It reports flights missing `co2_kg`. - -Example: - -```yaml -- booking_reference: ABC123 - trip: 2026-04-22 - price: '62.50' - currency: GBP - flights: - - depart: 2026-04-22 17:20:00+01:00 - arrive: 2026-04-22 20:20:00+02:00 - from: LHR - to: BRU - flight_number: '1234' - airline: BA - duration: 01:00 - seat: 5F - seat_type: W - class: C - cabin: business - plane: Airbus A320 - registration: G-ABCD - co2_kg: 154 -``` - -## `follow_launches.yaml` - -Top-level shape: list of SpaceDevs launch slugs. - -Used by: no current in-repo reader was found, but the file appears intended as a watch list for launch update tooling. - -Example: - -```yaml -- starship-integrated-flight-test-5 -- artemis-ii -``` - -## `stations.yaml` - -Top-level shape: list of railway stations. - -Used by: train loading, maps, route rendering. - -Required fields: - -- `name`: station name referenced by `trains.yaml`. -- `country`: lowercase country code. -- `latitude`, `longitude`. -- `routes`: mapping from destination station name to GeoJSON filename without `.geojson`. - -Common optional fields: - -- `uic`, `alpha3`, `wikidata`, `osm_node`. - -Note: the code reads `routes`, not `rotues`; `rotues` appears to be a typo in existing data and should not be used for new entries. - -Example: - -```yaml -- name: London St Pancras - uic: 7015400 - alpha3: STP - wikidata: Q720102 - latitude: 51.531921 - longitude: -0.126361 - country: gb - routes: - Brussels Midi: london_brussels_eurostar -``` - -## `subscriptions.yaml` - -Top-level shape: list of subscriptions. - -Used by: subscription renewal agenda events when `renewal_date` is present. - -Required fields: - -- `name`: subscription name. - -Common optional fields: - -- Dates: `start`, `start_date`, `renewal_date`. -- `price`: mapping with `amount` and `currency`. -- `term`: mapping with `duration` and `unit` or `term_unit`. -- Account: `email`, `account_url`, `account_number`. - -Only items with `renewal_date` create agenda events. - -Example: - -```yaml -- name: Example Magazine - start_date: 2026-01-01 - renewal_date: 2027-01-01 - price: - amount: 99 - currency: GBP - term: - duration: 1 - unit: year - email: me@example.com - account_url: https://example.com/account - account_number: '001234' -``` - -## `trains.yaml` - -Top-level shape: list of train journeys. Each journey contains one or more legs. - -Used by: agenda transport events, trip loading, trip timeline, maps, stats. - -Required journey fields: - -- `operator`: booking/operator label. -- `from`, `to`: station names from `stations.yaml`. -- `trip`: trip start date. -- `depart`, `arrive`: journey datetimes or dates. -- `legs`: list of leg mappings. - -Common optional journey fields: - -- `class`, `number`, `tickets`, `ticket_code`, `total_price`, `co2_kg`. - -Required leg fields: - -- `from`, `to`: station names from `stations.yaml`. -- `depart`, `arrive`. -- `operator`. - -Common optional leg fields: - -- `train`, `number`, `service`, `service_number`, `service_numbers`, `reporting_number`, `mode`. -- Seat/reservation: `coach`, `seat`, `seat_type`, `seat_features`, `reservation_number`, `platform`. -- `class`, `trip`, `url`. - -Ticket fields are free-form but commonly include `booking_reference`, `url`, `price`, `currency`, `booking_date`, `ticket`, `ticket_code`, `ticket_type`, `from`, `to`, `class`, `validity`, `route`, `fare`, `quantity`, `seat_reservation`. - -Example: - -```yaml -- operator: eurostar - from: London St Pancras - to: Brussels Midi - trip: 2026-02-06 - depart: 2026-02-06 15:04:00+00:00 - arrive: 2026-02-06 18:12:00+01:00 - class: Standard Premier - tickets: - - booking_reference: ABCDEF - url: https://example.com/booking/ABCDEF - price: '89.00' - currency: GBP - legs: - - from: London St Pancras - to: Brussels Midi - depart: 2026-02-06 15:04:00+00:00 - arrive: 2026-02-06 18:12:00+01:00 - coach: 1 - seat: 41 - operator: Eurostar -``` - -## `travel_rewards.yaml` - -Top-level shape: list of travel loyalty accounts. - -Used by: no current in-repo reader was found. The file is structured as account metadata. - -Common fields: - -- `name`: programme name. -- `type`: category such as `hotel`, `airline`, `rail`. -- `member_number`: membership identifier. -- `balance`: current points/miles balance. -- `expiry`: expiry date or null. -- `url`: account URL. -- `person`: account holder key/name. -- `email`, `note`. - -Example: - -```yaml -- name: Example Rewards - type: hotel - member_number: '123456789' - balance: 3665 - expiry: 2027-08-15 - url: https://example.com/rewards - person: edward -``` - -## `trips.yaml` - -Top-level shape: list of trip metadata. - -Used by: trip grouping and trip titles. - -Required fields: - -- `trip`: trip start date. This is the grouping key used by travel, accommodation, conferences, and trip events. - -Optional fields: - -- `name`: explicit trip title. -- `private`: boolean. Private trips are hidden from unauthenticated users. - -Example: - -```yaml -- trip: 2026-02-06 - name: Brussels for FOSDEM - private: false -``` - -### Conference detail pages and airport suggestions - -Conference titles in the list and series pages link to -`/conference/YYYY-MM-DD/series-or-title-location`. The date is the conference's -start date (or the earliest date for an approximate event). The detail page keeps -an external website link and displays the venue, address, coordinates, attendance -fields and remaining conference metadata. - -Airport suggestions start with `agenda/conference_airports.json`. You can override -or extend these in `personal-data/conference_airports.yaml`, using lowercase -country codes and casefolded location names: - -```yaml -"be:brussels": CRL -"us:pasadena, california": LAX -"se:malmö": [MMX, CPH] -"ch:bern": [BRN, BSL] -"de:bonn": [CGN, DUS] -"dk:funen": [BLL, CPH] -"it:bologna": [BLQ, VRN, VCE] -``` - -Mappings may name several airports. Malmö (also entered as Malmo or Malmö, -Sweden) searches MMX and CPH together, including cross-border airports explicitly -listed in the mapping. Bern searches BRN and Basel (BSL) together; Bonn searches Cologne/Bonn (CGN) -and Düsseldorf (DUS) together. Funen includes Billund (BLL) and Copenhagen (CPH). Bologna includes BLQ, Verona -(VRN) and Venice Marco Polo (VCE). You can also enter comma-separated IATA codes such as -`MMX,CPH`. Each route/date uses one combined Google search, so Bristol's direct -flights to CPH are considered without doubling the number of requests. Results -show the actual airport for each flight leg. - -Otherwise the page matches cities in `airports.yaml`, then suggests the nearest -known airport within 150 km of the venue, limited to the same country when known. -The flight lookup form accepts IATA codes, cities and airport names, with local -autocomplete from the bundled public-domain OurAirports index -(`agenda/airport_index.json`, downloaded from https://ourairports.com/data/). -Names from `PERSONAL_DATA/airports.yaml` (normally -`~/src/personal-data/airports.yaml`) override bundled labels for individual airports, -combined destinations and autocomplete. Missing or blank names fall back to the -bundled index, and edits take effect on the next request. Both personal and -bundled names remain searchable. -Scheduled-service airports are preferred; three-letter inputs are treated as -IATA codes. London resolves to LON (all London airports). The highest-ranked match is -used and its name/code shown; pick an autocomplete result for a specific airport. -Country suffixes narrow the matches; US state abbreviations such as CA are -recognized too. For example, -`Copenhagen, Denmark` resolves to CPH. A nearby airport may -still require a substantial ground transfer. Unknown locations need an IATA code -or city/airport name entered manually. - -“Open in Google Flights” opens a prefilled round trip in a new tab, with the -current airport group, outbound/return dates, stops, one adult in economy and -GBP/en-GB settings. Before a lookup it uses the initial Bristol or London dates; -for cached results each origin has its own link using the displayed flights' -departure dates (or the last checked dates when no flights were found). Links -are generated locally and work during the shared server cooldown. - -Only the flight lookup button contacts Google Flights from the server. Searches use Python -Playwright with installed Chromium, one adult in economy, language -`en-GB`, country `GB` -and currency `GBP`. Overseas European destinations within 3,500 km of London -prefer direct Bristol flights. Start with the day before and the day after the -conference, expanding each direction independently to at most four days away -only when no suitable flights are found. Stop searching a direction at the first -date with flights; when both nearest dates work, this needs just two searches. If either direction has no suitable results, London is -searched too. Other destinations start with London. London means LHR, LGW, STN, -LTN, LCY and SEN together, initially with at most one stop; departures are two days -before the conference to allow overnight travel, trying three days before if -no suitable outbound flights are found. Returns are the day after. Outbound -flights must arrive before the opening date. Prices are **one-way fares**, shown -separately for outbound and return travel. Searches use Google’s best-flight group first, -then prefer direct flights and BA within each stop count (all BA legs first, then -some BA legs, then other airlines). Operating airline is used when available, -otherwise the advertised airline. Google’s order is preserved within each group. -If a London search has no direct or one-stop results, it retries with at most two -stops. Bristol always remains direct-only. - -Results live in `DATA_DIR/conference-flights` (normally -`/home/edward/lib/data/conference-flights`), keyed by dates, destination and search -policy. Updates reuse results for six hours. Empty results are also cached; -errors have a 15-minute retry cooldown and preserve any previous successful -results. Failures show the route/date and underlying exception type/message; -full tracebacks are logged on the server. Explicit Google date-range rejections -ask you to retry closer to departure, rather than after 15 minutes. Confirmed -“no flights found” and “no non-stop flights found” pages count as empty results; -Bristol searches can then continue to nearby dates and show “No direct flights -found” with the checked dates if none work; other unusable responses remain -errors and include the requested date. No fixed airline booking horizon is assumed. Legacy cached failures without details -can be retried immediately. A shared process lock prevents simultaneous conference flight searches. -The page shows when results were last fetched, the destination airport names, -and the departure dates checked when no suitable flights were found. Belfast is an exception to the UK exclusion: it searches Belfast International -(BFS) and Belfast City (BHD) together, starting with direct flights from Bristol. -Conferences with tentative start/end dates can also search for flights; the flight -section displays those provisional dates. Other domestic, past, online and -approximate-date conferences display details but do not offer flight lookup. - - -Individual route/date searches (including empty results) are cached for six hours -under `conference-flights/days`, so overlapping conferences reuse the same Google -lookup. Browser searches are spaced at least three seconds apart using shared disk state -across Apache workers. Chromium loads the page’s assets normally. HTTP 429 is not immediately -retried: it pauses all uncached Google flight requests for 30 minutes. The page -shows the shared cooldown and continues to display cached results. Failed navigations are not automatically retried. - - -Search URL encoding and result parsing follow the format documented by -https://github.com/AWeirdDev/flights (MIT attribution in -`licenses/fast-flights-MIT.txt`). `flights` is no longer an application dependency. `run.fcgi` uses `/usr/bin/python3`; a project virtualenv is no longer -needed. System Python must provide Playwright and the existing Flask dependencies, -and Chromium must be installed. On Debian we launch -`/usr/lib/chromium/chromium` directly: Apache's `ProcSubset=pid` hides -`/proc/cpuinfo`, which causes `/usr/bin/chromium`'s shell-wrapper CPU check to -fail incorrectly. Other installations fall back to `chromium` on PATH. The browser is started only on a cache miss, -reused for that lookup’s dates, and closed after the lookup. Its persistent profile -in `conference-flights/browser-profile` preserves consent choices. The shared -search lock prevents multiple workers opening that profile simultaneously. - -Google’s result format is undocumented, and using a browser does not guarantee -avoidance of rate limits or verification challenges. Existing successful cached -fares remain usable; old empty results and failures can be refreshed using the -browser, subject to the existing shared cooldown. - -Failed browser searches save compressed JSON diagnostics in -`DATA_DIR/conference-flights/errors/*.json.gz`. Each contains the UTC timestamp, -route, requested date, stop limit, locale/currency, request and final page URLs, -HTTP status, exception details, raw flight-data script, visible page text, page -HTML, and up to five Google HTTP error response bodies. Capture uses the loaded -page and received responses; it makes no extra Google requests. Capture failures -are logged and cannot replace the original lookup error. Local cooldown refusals -and successful searches do not create snapshots. - -Snapshots are retained for 30 days, with at most 100 files; cleanup runs when a -new snapshot is written. Captures are capped at 2 MiB of HTML, 1 MiB of flight -script, 64 KiB of visible text and 256 KiB per HTTP error body, with any truncation -recorded. Snapshot files have mode 0640. Previous failures cannot be recovered -from these snapshots; recording begins with this change. Use Python's `gzip.open` -to read the JSON for analysis or replay the saved `flight_script` through -`agenda.google_flights.parse_results`. diff --git a/frontend/index.js b/frontend/index.js deleted file mode 100644 index e69de29..0000000 diff --git a/get_airport.py b/get_airport.py deleted file mode 100755 index 3e6e29a..0000000 --- a/get_airport.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/python3 - -import sys -from typing import Any, Dict, List - -import requests -import yaml - -# Define the base URL for the Wikidata API -WIKIDATA_API_URL = "https://www.wikidata.org/w/api.php" - - -def get_entity_label(qid: str) -> str | None: - """ - Fetches the English label for a given Wikidata entity QID. - - Args: - qid (str): The Wikidata entity ID (e.g., "Q6106"). - - Returns: - Optional[str]: The English label of the entity, or None if not found. - """ - params: Dict[str, str] = { - "action": "wbgetentities", - "ids": qid, - "format": "json", - "props": "labels", - "languages": "en", - } - try: - response = requests.get(WIKIDATA_API_URL, params=params) - response.raise_for_status() - entity = response.json().get("entities", {}).get(qid, {}) - return entity.get("labels", {}).get("en", {}).get("value") - except requests.exceptions.RequestException as e: - print(f"Error fetching label for QID {qid}: {e}", file=sys.stderr) - return None - - -def get_entity_details(qid: str) -> dict[str, Any] | None: - """ - Fetches and processes detailed information for a given airport QID. - - Args: - qid (str): The QID of the airport Wikidata entity. - - Returns: - Optional[Dict[str, Any]]: A dictionary containing the detailed airport data. - """ - params: Dict[str, str] = { - "action": "wbgetentities", - "ids": qid, - "format": "json", - "props": "claims|labels", - } - try: - response = requests.get(WIKIDATA_API_URL, params=params) - response.raise_for_status() - entity = response.json().get("entities", {}).get(qid, {}) - if not entity: - return None - - claims = entity.get("claims", {}) - - # Helper to safely extract claim values - def get_simple_claim_value(prop_id: str) -> str | None: - claim = claims.get(prop_id) - if not claim: - return None - v = claim[0].get("mainsnak", {}).get("datavalue", {}).get("value") - assert isinstance(v, str) or v is None - return v - - # Get IATA code, name, and website - iata = get_simple_claim_value("P238") - name = entity.get("labels", {}).get("en", {}).get("value") - website = get_simple_claim_value("P856") - - # Get City Name by resolving its QID - city_qid_claim = claims.get("P131") - city_name = None - if city_qid_claim: - city_qid = ( - city_qid_claim[0] - .get("mainsnak", {}) - .get("datavalue", {}) - .get("value", {}) - .get("id") - ) - if city_qid: - city_name = get_entity_label(city_qid) - - # Get coordinates - coords_claim = claims.get("P625") - latitude, longitude = None, None - if coords_claim: - coords = ( - coords_claim[0] - .get("mainsnak", {}) - .get("datavalue", {}) - .get("value", {}) - ) - latitude = coords.get("latitude") - longitude = coords.get("longitude") - - # Get elevation - elevation_claim = claims.get("P2044") - elevation = None - if elevation_claim: - amount_str = ( - elevation_claim[0] - .get("mainsnak", {}) - .get("datavalue", {}) - .get("value", {}) - .get("amount") - ) - if amount_str: - elevation = float(amount_str) if "." in amount_str else int(amount_str) - - # Get Country Code - country_claim = claims.get("P17") - country_code = None - if country_claim: - country_qid = ( - country_claim[0] - .get("mainsnak", {}) - .get("datavalue", {}) - .get("value", {}) - .get("id") - ) - if country_qid: - # Fetch the ISO 3166-1 alpha-2 code (P297) for the country entity - country_code_params = { - "action": "wbgetclaims", - "entity": country_qid, - "property": "P297", - "format": "json", - } - country_res = requests.get(WIKIDATA_API_URL, params=country_code_params) - country_res.raise_for_status() - country_claims = country_res.json().get("claims", {}).get("P297") - if country_claims: - code = ( - country_claims[0] - .get("mainsnak", {}) - .get("datavalue", {}) - .get("value") - ) - if code: - country_code = code.lower() - - data = { - "iata": iata, - "name": name, - "city": city_name, - "qid": qid, - "latitude": latitude, - "longitude": longitude, - "elevation": elevation, - "website": website, - "country": country_code, - } - - # Return the final structure, filtering out null values for cleaner output - return {iata: {k: v for k, v in data.items() if v is not None}} - - except requests.exceptions.RequestException as e: - print(f"Error fetching entity details for QID {qid}: {e}", file=sys.stderr) - return None - - -def find_airport_by_iata(iata_code: str) -> dict[str, Any] | None: - """ - Finds an airport by its IATA code using Wikidata's search API. - - Args: - iata_code (str): The IATA code of the airport (e.g., "PDX"). - - Returns: - Optional[Dict[str, Any]]: A dictionary with the airport data or None. - """ - params: Dict[str, str] = { - "action": "query", - "list": "search", - "srsearch": f"haswbstatement:P238={iata_code.upper()}", - "format": "json", - } - try: - response = requests.get(WIKIDATA_API_URL, params=params) - response.raise_for_status() - search_results: List[Dict[str, Any]] = ( - response.json().get("query", {}).get("search", []) - ) - - if not search_results: - print(f"No airport found with IATA code: {iata_code}", file=sys.stderr) - return None - - qid = search_results[0]["title"] - return get_entity_details(qid) - - except requests.exceptions.RequestException as e: - print(f"Error searching on Wikidata API: {e}", file=sys.stderr) - return None - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python airport_lookup.py ", file=sys.stderr) - sys.exit(1) - - iata_code_arg = sys.argv[1] - airport_data = find_airport_by_iata(iata_code_arg) - - if airport_data: - print( - yaml.safe_dump( - airport_data, - default_flow_style=False, - allow_unicode=True, - sort_keys=False, - ), - end="", - ) diff --git a/licenses/fast-flights-MIT.txt b/licenses/fast-flights-MIT.txt deleted file mode 100644 index 72c3f97..0000000 --- a/licenses/fast-flights-MIT.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024-PRESENT fast-flights Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/package.json b/package.json deleted file mode 100644 index 47a8c57..0000000 --- a/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "agenda", - "version": "1.0.0", - "directories": { - "test": "tests" - }, - "repository": { - "type": "git", - "url": "https://git.4angle.com/edward/agenda.git" - }, - "license": "ISC", - "devDependencies": { - "copy-webpack-plugin": "^12.0.2", - "eslint": "^9.2.0", - "webpack": "^5.91.0", - "webpack-cli": "^5.1.4" - }, - "dependencies": { - "@fullcalendar/core": "^6.1.11", - "@fullcalendar/daygrid": "^6.1.11", - "@fullcalendar/list": "^6.1.11", - "@fullcalendar/timegrid": "^6.1.11", - "bootstrap": "^5.3.3", - "es-module-shims": "^1.8.3", - "leaflet": "^1.9.4", - "leaflet.geodesic": "^2.7.1" - } -} diff --git a/parse_airbnb.py b/parse_airbnb.py deleted file mode 100755 index 0f9843f..0000000 --- a/parse_airbnb.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/python3 - -import sys - -import yaml - -from agenda.airbnb import parse_multiple_files - - -def main() -> None: - """Main function.""" - - filenames = sys.argv[1:] - bookings = parse_multiple_files(filenames) - print(yaml.dump(bookings, sort_keys=False)) - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt index d856b79..ba7a169 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,3 @@ dateutil ephem flask requests -emoji -timezonefinder -playwright diff --git a/run.fcgi b/run.fcgi index 0c4677f..851231a 100755 --- a/run.fcgi +++ b/run.fcgi @@ -1,9 +1,8 @@ #!/usr/bin/python3 -from flipflop import WSGIServer # type: ignore[import-untyped] +from flipflop import WSGIServer import sys +sys.path.append('/home/edward/src/2021/agenda') +from web_view import app -sys.path.append("/home/edward/src/agenda") # isort:skip -from web_view import app # isort:skip - -if __name__ == "__main__": +if __name__ == '__main__': WSGIServer(app).run() diff --git a/scripts/add-car-journeys-for-trip b/scripts/add-car-journeys-for-trip deleted file mode 100755 index 9117068..0000000 --- a/scripts/add-car-journeys-for-trip +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 - -import os -import sys - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from agenda.car_journey_yaml import main - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/add-new-conference b/scripts/add-new-conference deleted file mode 100755 index 0190c07..0000000 --- a/scripts/add-new-conference +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 - -import os -import sys - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from agenda.add_new_conference import main - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/build_airport_yaml b/scripts/build_airport_yaml deleted file mode 100755 index cb7c9d6..0000000 --- a/scripts/build_airport_yaml +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 - -import os -import sys - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from agenda.build_place_yaml import airport_main - -if __name__ == "__main__": - raise SystemExit(airport_main()) diff --git a/scripts/build_station_yaml b/scripts/build_station_yaml deleted file mode 100755 index 9285a8f..0000000 --- a/scripts/build_station_yaml +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 - -import os -import sys - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from agenda.build_place_yaml import station_main - -if __name__ == "__main__": - raise SystemExit(station_main()) diff --git a/scripts/generate-flight-booking-yaml b/scripts/generate-flight-booking-yaml deleted file mode 100755 index ba9c161..0000000 --- a/scripts/generate-flight-booking-yaml +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 - -import os -import sys - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from agenda.generate_booking_yaml import flight_main - -if __name__ == "__main__": - raise SystemExit(flight_main()) diff --git a/scripts/generate-train-booking-yaml b/scripts/generate-train-booking-yaml deleted file mode 100755 index 1c0a7ba..0000000 --- a/scripts/generate-train-booking-yaml +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 - -import os -import sys - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from agenda.generate_booking_yaml import train_main - -if __name__ == "__main__": - raise SystemExit(train_main()) diff --git a/scripts/import-car-journeys-from-timeline b/scripts/import-car-journeys-from-timeline deleted file mode 100644 index aeb5daa..0000000 --- a/scripts/import-car-journeys-from-timeline +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 - -import os -import sys - -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from agenda.car_journey_timeline import main - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/static/css/trips.css b/static/css/trips.css deleted file mode 100644 index 7e1cbbc..0000000 --- a/static/css/trips.css +++ /dev/null @@ -1,381 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Source+Sans+3:ital,wght@0,300;0,400;0,600;1,300&family=JetBrains+Mono:wght@400;500&display=swap'); - -:root { - --t-navy: #1e2d4a; - --t-slate: #374869; - --t-gold: #b8860b; - --t-amber: #e8a820; - --t-cream: #f9f7f3; - --t-white: #ffffff; - --t-muted: #7a8aa8; - --t-border: #dde3ed; - --t-text: #1e2533; - --t-shadow: rgba(30, 45, 74, 0.08); -} - -/* Text pane background on list page */ -.text-content { - background: var(--t-cream) !important; - padding-left: 12px; - padding-top: 12px; -} - -/* =========================== - TRIP CARDS (list view) - =========================== */ - -.trip-card { - background: var(--t-white); - border-radius: 8px; - border: 1px solid var(--t-border); - border-left: 3px solid #8098c0; - box-shadow: 0 1px 4px var(--t-shadow); - padding: 14px 18px; - margin-bottom: 14px; - transition: box-shadow 0.15s ease, transform 0.15s ease; -} - -.trip-card:hover { - box-shadow: 0 4px 16px rgba(30, 45, 74, 0.14); - transform: translateY(-1px); -} - -.trip-card.trip-current { - border-left-color: var(--t-gold); -} - -/* Trip name heading */ -.trip-name { - margin-bottom: 2px; - font-size: 1.1rem; - line-height: 1.3; -} - -.trip-name a { - font-family: 'Playfair Display', Georgia, 'Times New Roman', serif; - font-weight: 700; - color: var(--t-navy); - text-decoration: none; -} - -.trip-name a:hover { - color: var(--t-gold); -} - -.trip-name small { - font-family: 'JetBrains Mono', 'Courier New', monospace; - font-size: 0.7rem; - color: var(--t-muted); - font-weight: 400; -} - -/* Countries as inline chips */ -.trip-countries { - display: flex !important; - flex-wrap: wrap; - gap: 5px; - margin: 6px 0; - padding: 0; -} - -.trip-countries li { - display: inline-flex; - align-items: center; - gap: 4px; - background: #eef2f8; - border: 1px solid #d5dce8; - border-radius: 20px; - padding: 1px 10px; - font-size: 0.8rem; - color: var(--t-text); -} - -/* Dates */ -.trip-dates { - font-size: 0.83rem; - color: var(--t-muted); - margin: 2px 0; -} - -/* Stats row — pill chips */ -.trip-stats { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin: 7px 0; -} - -.trip-stat { - display: inline-block; - background: #f3f6fa; - border: 1px solid #dde3ed; - border-radius: 20px; - padding: 2px 10px; - font-size: 0.74rem; - color: var(--t-slate); - white-space: nowrap; - font-family: 'JetBrains Mono', monospace; -} - -/* School holiday info */ -.school-holiday-info { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - margin: 4px 0 8px; - font-size: 0.82rem; -} - -/* Inline weather chip in day headers */ -.trip-weather-inline { - font-size: 0.78rem; - color: var(--t-muted); - font-weight: 400; - letter-spacing: 0; - text-transform: none; - display: inline-flex; - align-items: center; - gap: 3px; - vertical-align: middle; -} - -/* Day sub-headers within a trip card */ -.trip-day-header { - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--t-navy); - margin: 18px 0 6px; - padding: 4px 0 4px 10px; - border-left: 3px solid var(--t-amber); - background: linear-gradient(to right, rgba(232, 168, 32, 0.07), transparent); -} - -/* Condensed check-out line */ -.trip-checkout { - font-size: 0.84rem; - padding: 4px 8px; - color: var(--t-muted); - border-left: 2px solid #aacde8; - margin: 3px 0 3px 1px; -} - -/* Transport/accommodation element rows */ -.trip-element { - font-size: 0.84rem; - padding: 2px 0; - color: var(--t-text); - line-height: 1.5; -} - -/* =========================== - INLINE CONFERENCE CARDS - (within trip_item macro) - =========================== */ - -.trip-conference-card { - background: #fffef4; - border: 1px solid #e4d46c; - border-radius: 6px; - padding: 9px 13px; - margin: 5px 0; -} - -.trip-conference-card .card-body { - padding: 0; -} - -.trip-conference-card .card-title { - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 3px; - color: var(--t-navy); -} - -.trip-conference-card .card-text { - font-size: 0.82rem; - margin-bottom: 0; - color: var(--t-text); -} - -/* =========================== - TRIP PAGE ACCOMMODATION - =========================== */ - -.trip-accommodation-card { - background: #f3faff; - border: 1px solid #aacde8; - border-radius: 6px; - padding: 9px 13px; - margin: 5px 0; -} - -.trip-accommodation-card .card-body { padding: 0; } - -.trip-accommodation-card .card-title { - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 3px; - color: var(--t-navy); -} - -.trip-accommodation-card .card-text { - font-size: 0.82rem; - margin-bottom: 0; -} - -/* =========================== - TRIP PAGE TRANSPORT - =========================== */ - -.trip-transport-card { - background: #f7f9fc; - border: 1px solid #d0dbe8; - border-radius: 6px; - padding: 9px 13px; - margin: 5px 0; -} - -.trip-transport-card .card-body { padding: 0; } - -.trip-transport-card .card-title { - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 3px; - color: var(--t-navy); -} - -.trip-transport-card .card-text { - font-size: 0.82rem; - margin-bottom: 0; -} - -/* =========================== - TRIP PAGE EVENTS - =========================== */ - -.trip-event-card { - background: #fdf5ff; - border: 1px solid #d4a8e8; - border-radius: 6px; - padding: 9px 13px; - margin: 5px 0; -} - -.trip-event-card .card-body { padding: 0; } - -.trip-event-card .card-title { - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 3px; - color: var(--t-navy); -} - -.trip-event-card .card-text { - font-size: 0.82rem; - margin-bottom: 0; -} - -/* =========================== - TRIP PAGE HEADER & TYPOGRAPHY - =========================== */ - -.trip-page-title { - font-family: 'Playfair Display', Georgia, 'Times New Roman', serif; - font-weight: 700; - font-size: 1.8rem; - color: var(--t-navy); - margin-bottom: 4px; -} - -/* Section divider headings on the trip detail page */ -h3.trip-section-h, -h4.trip-section-h { - font-size: 0.72rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.14em; - color: var(--t-muted); - margin-top: 22px; - margin-bottom: 8px; - padding-bottom: 4px; - border-bottom: 1px solid var(--t-border); -} - -/* Prev/next nav */ -.trip-prev-next { - font-size: 0.83rem; - color: var(--t-muted); - margin-bottom: 16px; -} - -.trip-prev-next a { - color: var(--t-slate); - text-decoration: none; - font-weight: 500; -} - -.trip-prev-next a:hover { - color: var(--t-gold); -} - -/* =========================== - TRIP LIST PAGE SUMMARY BOX - =========================== */ - -.trip-list-summary { - background: var(--t-navy); - color: #c8d4e8; - border-radius: 8px; - padding: 14px 18px; - margin-bottom: 18px; - margin-top: 8px; -} - -.trip-list-summary h2 { - color: #f0f4fa; - font-family: 'Playfair Display', Georgia, serif; - font-size: 1.25rem; - margin-bottom: 4px; - font-weight: 700; -} - -.trip-list-summary a { - color: var(--t-amber); - text-decoration: none; - font-size: 0.82rem; -} - -.trip-list-summary a:hover { - text-decoration: underline; -} - -.summary-stats-row { - display: flex; - flex-wrap: wrap; - gap: 18px; - margin-top: 10px; -} - -.summary-stat { - display: flex; - flex-direction: column; -} - -.summary-stat-label { - font-size: 0.62rem; - text-transform: uppercase; - letter-spacing: 0.1em; - opacity: 0.55; - line-height: 1; - margin-bottom: 2px; -} - -.summary-stat-value { - font-family: 'JetBrains Mono', monospace; - font-size: 0.82rem; - color: var(--t-amber); - font-weight: 500; -} diff --git a/static/js/map.js b/static/js/map.js deleted file mode 100644 index 3469fed..0000000 --- a/static/js/map.js +++ /dev/null @@ -1,270 +0,0 @@ -if (![].at) { - Array.prototype.at = function(pos) { return this.slice(pos, pos + 1)[0]; }; -} - -var emojiByType = { - "station": "🚉", - "airport": "✈️", - "ferry_terminal": "🚢", - "coach_station": "🚌", - "bus_stop": "🚏", - "home": "🏠", - "car_stop": "🚗", - "accommodation": "🏨", - "conference": "🖥️", - "event": "🍷" -}; - -function getIconMetrics(zoom) { - var outerSize; - if (zoom <= 3) { - outerSize = 20; - } else if (zoom <= 5) { - outerSize = 26; - } else if (zoom <= 8) { - outerSize = 32; - } else { - outerSize = 38; - } - var innerSize = outerSize - 6; - var fontSize = Math.max(12, Math.round(innerSize * 0.65)); - - return { - outerSize: outerSize, - fontSize: fontSize, - anchor: [outerSize / 2, outerSize / 2] - }; -} - -function emojiIcon(emoji, zoom) { - var symbol = emoji || "📍"; - var metrics = getIconMetrics(zoom); - var iconStyle = [ - "
", - "
", - symbol, - "
" - ].join(""); - - return L.divIcon({ - className: "custom-div-icon", - html: iconStyle, - iconSize: [metrics.outerSize, metrics.outerSize], - iconAnchor: metrics.anchor - }); -} - - -function build_map(map_id, coordinates, routes) { - var bounds = coordinates.map(function(station) { return [station.latitude, station.longitude]; }); - - function addGeoJsonCoordinatesToBounds(geojsonCoordinates) { - if (!geojsonCoordinates) return; - if (typeof geojsonCoordinates[0] === "number" && typeof geojsonCoordinates[1] === "number") { - bounds.push([geojsonCoordinates[1], geojsonCoordinates[0]]); - return; - } - geojsonCoordinates.forEach(addGeoJsonCoordinatesToBounds); - } - - function addGeoJsonGeometryToBounds(geometry) { - if (!geometry) return; - if (geometry.type === "GeometryCollection") { - geometry.geometries.forEach(addGeoJsonGeometryToBounds); - return; - } - addGeoJsonCoordinatesToBounds(geometry.coordinates); - } - - function addGeoJsonToBounds(geojson) { - if (!geojson) return; - if (geojson.type === "FeatureCollection") { - geojson.features.forEach(function(feature) { - addGeoJsonGeometryToBounds(feature.geometry); - }); - return; - } - if (geojson.type === "Feature") { - addGeoJsonGeometryToBounds(geojson.geometry); - return; - } - addGeoJsonGeometryToBounds(geojson); - } - - routes.forEach(function(r) { - if (r.from) bounds.push(r.from); - if (r.to) bounds.push(r.to); - if (r.geojson) addGeoJsonToBounds(JSON.parse(r.geojson)); - }); - - var map = bounds.length > 0 - ? L.map(map_id).fitBounds(bounds) - : L.map(map_id).setView([20, 0], 2); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '©
OpenStreetMap contributors' - }).addTo(map); - - var markers = []; - var offset_lines = []; - - function getIconBounds(latlng, zoom) { - var iconSize = getIconMetrics(zoom).outerSize; - if (!latlng) return null; - var pixel = map.project(latlng, zoom); - var sw = map.unproject([pixel.x - iconSize / 2, pixel.y + iconSize / 2], zoom); - var ne = map.unproject([pixel.x + iconSize / 2, pixel.y - iconSize / 2], zoom); - return L.latLngBounds(sw, ne); - } - - function calculateCentroid(markers) { - var latSum = 0, lngSum = 0, count = 0; - markers.forEach(function(marker) { - latSum += marker.getLatLng().lat; - lngSum += marker.getLatLng().lng; - count += 1; - }); - return count > 0 ? L.latLng(latSum / count, lngSum / count) : null; - } - - // Function to detect and group overlapping markers - function getOverlappingGroups(zoom) { - var groups = []; - var visited = new Set(); - - markers.forEach(function(marker) { - if (visited.has(marker)) { - return; - } - var group = []; - var markerBounds = getIconBounds(marker.getLatLng(), zoom); - - markers.forEach(function(otherMarker) { - var otherBounds = getIconBounds(otherMarker.getLatLng(), zoom); - if (marker !== otherMarker && markerBounds && otherBounds && markerBounds.intersects(otherBounds)) { - group.push(otherMarker); - visited.add(otherMarker); - } - }); - - if (group.length > 0) { - group.push(marker); // Add the original marker to the group - groups.push(group); - visited.add(marker); - } - }); - - return groups; - } - - function displaceMarkers(group, zoom) { - var markerPixelSize = Math.max(18, getIconMetrics(zoom).outerSize); - var mapRef = group[0]._map; // Assuming all markers are on the same map - - var centroid = calculateCentroid(group); - if (!centroid) { - return; - } - var centroidPoint = mapRef.project(centroid, zoom); - - var radius = markerPixelSize * 1.1; - var angleIncrement = (2 * Math.PI) / group.length; - - group.forEach(function(marker, index) { - var angle = index * angleIncrement; - var newX = centroidPoint.x + radius * Math.cos(angle); - var newY = centroidPoint.y + radius * Math.sin(angle); - var newPoint = L.point(newX, newY); - var newLatLng = mapRef.unproject(newPoint, zoom); - - var originalPos = marker.originalLatLng; - marker.setLatLng(newLatLng); - - marker.polyline = L.polyline([originalPos, newLatLng], {color: "#909090", weight: 1, dashArray: "2,4"}).addTo(mapRef); - offset_lines.push(marker.polyline); - }); - } - - function updateMarkerIcons(zoom) { - markers.forEach(function(marker) { - marker.setIcon(emojiIcon(marker.emoji, zoom)); - }); - } - - coordinates.forEach(function(item) { - var latlng = L.latLng(item.latitude, item.longitude); - var marker = L.marker(latlng, { icon: emojiIcon(emojiByType[item.type], map.getZoom()) }).addTo(map); - marker.bindPopup(item.name); - marker.originalLatLng = latlng; - marker.emoji = emojiByType[item.type]; - markers.push(marker); - }); - - function resetMarkerPositions() { - markers.forEach(function(marker) { - marker.setLatLng(marker.originalLatLng); - if (marker.polyline) { - map.removeLayer(marker.polyline); - marker.polyline = null; - } - }); - offset_lines.forEach(function(polyline) { - map.removeLayer(polyline); - }); - offset_lines = []; - } - - map.on('zoomend', function() { - resetMarkerPositions(); - updateMarkerIcons(map.getZoom()); - - var overlappingGroups = getOverlappingGroups(map.getZoom()); - - overlappingGroups.forEach(function(group) { return displaceMarkers(group, map.getZoom()); }); - }); - - updateMarkerIcons(map.getZoom()); - - var initialGroups = getOverlappingGroups(map.getZoom()); - initialGroups.forEach(function(group) { return displaceMarkers(group, map.getZoom()); }); - - - // Draw routes - routes.forEach(function(route) { - var color = {"train": "blue", "flight": "red", "unbooked_flight": "orange", "coach": "green", "bus": "purple", "car": "#555"}[route.type]; - var style = { weight: 3, opacity: 0.5, color: color }; - if (route.geojson) { - L.geoJSON(JSON.parse(route.geojson), { - style: function(feature) { return style; } - }).addTo(map); - } else if (route.type === "flight" || route.type === "unbooked_flight") { - var flightPath = new L.Geodesic([[route.from, route.to]], style).addTo(map); - } else { - L.polyline([route.from, route.to], style).addTo(map); - } - }); - - var mapElement = document.getElementById(map_id); - - document.getElementById('toggleMapSize').addEventListener('click', function() { - var mapElement = document.getElementById(map_id); - var isFullWindow = mapElement.classList.contains('full-window-map'); - - if (isFullWindow) { - mapElement.classList.remove('full-window-map'); - mapElement.classList.add('half-map'); - mapElement.style.position = 'relative'; - } else { - mapElement.classList.add('full-window-map'); - mapElement.classList.remove('half-map'); - mapElement.style.position = ''; - } - - // Ensure the map adjusts to the new container size - map.invalidateSize(); - }); - - return map; -} diff --git a/templates/accommodation.html b/templates/accommodation.html index dda241d..ee98ef4 100644 --- a/templates/accommodation.html +++ b/templates/accommodation.html @@ -1,12 +1,9 @@ {% extends "base.html" %} -{% from "macros.html" import trip_link, accommodation_row with context %} -{% block title %}Accommodation - Edward Betts{% endblock %} {% block style %} -{% set column_count = 9 %} {% endblock %} +{% macro row(item, badge) %} +
{{ item.from.strftime("%a, %d %b %Y") }}
+
{{ item.to.strftime("%a, %d %b") }}
+
{{ (item.to.date() - item.from.date()).days }}
+
{{ item.name }}
+
{{ item.operator }}
+
{{ item.location }}
+{% endmacro %} + {% macro section(heading, item_list, badge) %} {% if item_list %}

{{heading}}

-{% for item in item_list %} - {{ accommodation_row(item, badge) }} -
{% if item.linked_trip %} trip: {{ trip_link(item.linked_trip) }} {% endif %}
-{% endfor %} +{% for item in item_list %}{{ row(item, badge) }}{% endfor %} {% endif %} {% endmacro %} @@ -37,24 +40,13 @@

Accommodation

-

Statistics

- {{ section("Current", current) }} - {{ section("Future", future) }} - {{ section("Past", past) }} + {{ section("Accommodation", items) }}
diff --git a/templates/base.html b/templates/base.html index d51e83c..94fe993 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,7 +7,7 @@ {% block title %}{% endblock %} - + {% block style %} {% endblock %} @@ -16,9 +16,8 @@ {% block nav %}{{ navbar() }}{% endblock %} -{% include "flash_messages.html" %} {% block content %}{% endblock %} {% block scripts %}{% endblock %} - + diff --git a/templates/birthday_list.html b/templates/birthday_list.html deleted file mode 100644 index 8443685..0000000 --- a/templates/birthday_list.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "base.html" %} -{% from "macros.html" import display_date %} -{% block title %}Birthdays - Edward Betts{% endblock %} - -{% block content %} -
-

Birthdays

- - - - - - - - - - {% for event in items %} - - - - - - {% endfor %} - -
DateEventDays
{{event.as_date.strftime("%a, %d, %b %Y")}}{{ event.title }}{{ event.delta_days(today) }}
-
-{% endblock %} diff --git a/templates/calendar.html b/templates/calendar.html deleted file mode 100644 index a4656aa..0000000 --- a/templates/calendar.html +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - Agenda - Edward Betts - - - - - - - - -{% set event_labels = { - "economist": "📰 The Economist", - "mothers_day": "Mothers' day", - "fathers_day": "Fathers' day", - "uk_financial_year_end": "End of financial year", - "bank_holiday": "UK bank holiday", - "us_holiday": "US holiday", - "uk_clock_change": "UK clock change", - "us_clock_change": "US clock change", - "us_presidential_election": "US pres. election", - "xmas_last_second": "Christmas last posting 2nd class", - "xmas_last_first": "Christmas last posting 1st class", - "up_series": "Up documentary", - "waste_schedule": "Waste schedule", - "gwr_advance_tickets": "GWR advance tickets", - "critical_mass": "Critical Mass", -} -%} - -{% from "navbar.html" import navbar with context %} - - {{ navbar() }} - -
-

Agenda

-

- ← personal tools -

- - {% if errors %} - {% for error in errors %} - - {% endfor %} - {% endif %} - -
- Markets: - Hide while away - | Show all - | Hide all -
- - -
-
- - - -
-

-
- - - -
-
- - -
- - {# -
-
Page generation time
-
    -
  • Data gather took {{ "%.1f" | format(data_gather_seconds) }} seconds
  • -
  • Stock market open/close took - {{ "%.1f" | format(stock_market_times_seconds) }} seconds
  • - {% for name, seconds in timings %} -
  • {{ name }} took {{ "%.1f" | format(seconds) }} seconds
  • - {% endfor %} -
-
- #} - -
- - - - - - - - - diff --git a/templates/conference_detail.html b/templates/conference_detail.html deleted file mode 100644 index 237bf76..0000000 --- a/templates/conference_detail.html +++ /dev/null @@ -1,152 +0,0 @@ -{% extends "base.html" %} -{% from "macros.html" import trip_link with context %} -{% block title %}{{ conf.name }} - Edward Betts{% endblock %} -{% block style %} - -{% endblock %} -{% block content %} -
- ← Conferences -

{{ conf.name }}

-

{{ conf.display_date }}{% if not conf.has_exact_dates %} {{ conf.date_status }}{% endif %}

- {% if conf.url %}

Conference website ↗

{% endif %} -
-
-

Location and venue

-

{{ conf.location }}{% if conf.country and get_country(conf.country) %} · {{ get_country(conf.country).flag }} {{ get_country(conf.country).name }}{% endif %}

- {% if conf.venue %}

{{ conf.venue }}

{% endif %} - {% if conf.address %}

{{ conf.address }}

{% endif %} - {% if conf.latitude is defined and conf.longitude is defined and conf.latitude is not none and conf.longitude is not none %} -

View venue on map ({{ conf.latitude }}, {{ conf.longitude }})

- {% elif conf.venue or conf.address %} -

Find venue on map

- {% endif %} - {% if conf.series_detail %}

Series: {{ conf.series_detail.name }}

{% endif %} - {% if conf.linked_trip %}

{{ trip_link(conf.linked_trip) }}

{% endif %} -
-
-

Conference details

-
- {% for label, value in fields %} -
{{ label }}
-
{% if value is sameas true %}Yes{% elif value is sameas false %}No{% elif value is string and (value.startswith('https://') or value.startswith('http://')) %}{{ value }}{% else %}{{ value|conference_date }}{% endif %}
- {% endfor %} -
-
-
-
-

Flights

- {% if cooldown %}

{{ cooldown }}

{% endif %} - {% if eligible %} - {% if conf.date_status == 'tentative' %}

Using tentative conference dates: {{ conf.start_date|conference_date }} – {{ conf.end_date|conference_date }}.

{% endif %} -

{% if short_haul %}Direct flights from Bristol first. Start the day before and return the day after; if no flights are found, check progressively further dates, up to four days each side. Each direction stops at the nearest date with flights. London is checked if either direction has no Bristol flights.{% else %}Flights from London, departing two days before and returning the day after the conference. If none are found, try departing one day earlier. Direct or one-stop flights first.{% endif %}

- - {% if resolved_airport %}

Destination: {{ resolved_name }}{% if resolved_name != resolved_airport %} ({{ resolved_airport }}){% endif %}

{% endif %} - {% if default_google_flights and (not cached or not cached.searches) %} -

Open in Google Flights ↗Round trip: {{ default_google_flights.departure|conference_date }} – {{ default_google_flights.return|conference_date }}

- {% endif %} - {% if resolution_error %}

{{ resolution_error }}

{% endif %} -

{% if suggested %}Suggested {{ suggested }}: {{ airport_reason }}. Enter a city, airport name or IATA code to choose another airport.{% else %}{{ airport_reason }}{% endif %}

-

Direct flights first, with British Airways preferred within each stop count. Other airlines are included. Two-stop flights from London are checked only if no direct or one-stop options are found.

-

Prices in GBP for one adult, economy. Each direction is priced separately. Results are cached for six hours; updates within that time reuse the cache.

- {% else %} -

Flight lookup is available for future conferences overseas or in Belfast with confirmed or tentative dates and an in-person venue.

- {% endif %} - {% if cached %} -

Last updated:

- {% if cached.error %}

{{ cached.error }}{% if cached.searches %} Showing previously cached results.{% endif %}

{% if cached.error_details %}

{{ cached.error_details }}

{% endif %}
{% endif %} - {% for search in cached.searches %} -

{{ 'Bristol (BRS), direct only' if search.origin == 'BRS' else 'London (LON)' }} → {{ resolved_name or cached.destination }}{% if resolved_name and resolved_name != cached.destination %} ({{ cached.destination }}){% endif %}

-

Open in Google Flights ↗Round trip: {{ search.google_flights.departure|conference_date }} – {{ search.google_flights.return|conference_date }}

- {% for label, rows, days in [('Outbound', search.outbound, search.outbound_dates), ('Return', search.inbound, search.inbound_dates)] %} -

{{ label }} · one-way fares

- {% if rows %}

Departure and arrival times are local to each airport.

{% endif %} - {% if rows %}
- - {% for flight in rows %} - - - - - - - {% endfor %} -
DepartureArrivalFlightStopsDurationPrice
{% for leg in flight.legs %}{{ leg.airline }} {{ leg.number }} ({{ leg.origin }}–{{ leg.destination }}){% if not loop.last %}
{% endif %}{% endfor %}
{{ 'Direct' if flight.stops == 0 else flight.stops|string ~ (' stop' if flight.stops == 1 else ' stops') }}{% if flight.duration is defined %}{{ flight.duration // 60 }}h {{ flight.duration % 60 }}m{% else %}—{% endif %}{% if flight.price is not none %}{{ '£' if flight.currency == 'GBP' else flight.currency ~ ' ' }}{{ '%.2f'|format(flight.price) }}{% else %}Check price{% endif %} ↗
{% else %}

{{ 'No direct flights found' if search.origin == 'BRS' else 'No suitable flights found' }}{% if days %} for departures on {{ days|map('conference_date')|join(', ') }}{% else %}: no eligible departure dates{% endif %}.{% if search.origin == 'BRS' and days %} Direct flights may not operate every day.{% endif %}

{% endif %} - {% endfor %} - {% endfor %} - {% endif %} -
-
- -{% endblock %} diff --git a/templates/conference_list.html b/templates/conference_list.html index 43c77c6..5d490c1 100644 --- a/templates/conference_list.html +++ b/templates/conference_list.html @@ -1,359 +1,63 @@ {% extends "base.html" %} -{% from "macros.html" import trip_link with context %} - -{% block title %}Conferences - Edward Betts{% endblock %} - {% block style %} {% endblock %} -{% set tl_colors = ["#0d6efd","#198754","#dc3545","#fd7e14","#6f42c1","#20c997","#0dcaf0","#d63384"] %} - -{% macro render_timeline(timeline) %} -{% if timeline %} -{% set row_h = 32 %} -{% set header_h = 22 %} -{% set total_h = timeline.lane_count * row_h + header_h %} -
-

Next 90 days

-
- - {% for m in timeline.months %} -
- {{ m.label }} -
- {% endfor %} - -
- - {% for conf in timeline.confs %} - {% set color = tl_colors[conf.lane % tl_colors | length] %} - {% set top_px = conf.lane * row_h + header_h %} - - {% endfor %} - -
+{% macro row(item, badge) %} +
{{ item.start.strftime("%a, %d %b %Y") }}
+
{{ item.end.strftime("%a, %d %b") }}
+
{{ item.name }} + {% if item.going and not (item.accommodation_booked or item.travel_booked) %} + + {{ badge }} + + {% endif %} + {% if item.accommodation_booked %} + accommodation + {% endif %} + {% if item.transport_booked %} + transport + {% endif %}
-{% endif %} +
{{ item.topic }}
+
{{ item.location }}
+ {% endmacro %} -{% macro conf_rows(heading, item_list, badge) %} +{% macro section(heading, item_list, badge) %} {% if item_list %} -{% set count = item_list | length %} - - {{ heading }} {{ count }} conference{{ "" if count == 1 else "s" }} - -{% set ns = namespace(prev_month="") %} -{% for item in item_list %} - {% set month_label = item.sort_date.strftime("%B %Y") %} - {% if month_label != ns.prev_month %} - {% set ns.prev_month = month_label %} - - {{ month_label }} - - {% endif %} - - - {{ item.display_date }} - {% if item.date_status == "tentative" %} - tentative - {% elif item.date_status == "approximate" %} - approximate - {% endif %} - - - {{ item.name }} - {% if item.going and not (item.accommodation_booked or item.travel_booked) %} - {{ badge }} - {% endif %} - {% if item.accommodation_booked %} - accommodation - {% endif %} - {% if item.transport_booked %} - transport - {% endif %} - {% if item.linked_trip %} - {% set trip = item.linked_trip %} - - 🧳{% if trip.title != item.name %} {{ trip.title }}{% endif %} - - {% endif %} - - {{ item.topic }} - - {% if item.series and item.series_detail %} - {{ item.series_detail.name }} - {% elif item.series %} - {{ item.series }} - {% endif %} - - - {% set country = get_country(item.country) if item.country else None %} - {% if country %}{{ country.flag }} {{ item.location }} - {% elif item.online %}💻 Online - {% else %}{{ item.location }}{% endif %} - - - {% if item.cfp_end %}{{ item.cfp_end.strftime("%-d %b %Y") }}{% endif %} - - - {% if item.price and item.currency %} - {{ "{:,d}".format(item.price | int) }} {{ item.currency }} - {% if item.currency != "GBP" and item.currency in fx_rate %} - {{ "{:,.0f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% elif item.free %} - free - {% endif %} - - -{% endfor %} +

{{heading}}

+{% for item in item_list %}{{ row(item, badge) }}{% endfor %} {% endif %} {% endmacro %} {% block content %}
+

Conferences

- {% if country_options %} -
-
- - -
-
- - {% if selected_country %} - Clear - {% endif %} -
-
- {% endif %} - - {{ render_timeline(timeline) }} - - - - - - - - - - - - - - - - - - - - - - - - - {{ conf_rows("Current", current, "attending") }} - {{ conf_rows("Future", future, "going") }} - {{ conf_rows("Past", past|reverse|list, "went") }} - -
Conferences grouped by attendance and month
DatesConferenceTopicSeriesLocationCFP endsPrice
+
+ {{ section("Current", current, "attending") }} + {{ section("Future", future, "going") }} + {{ section("Past", past|reverse, "went") }} +
- - {% endblock %} diff --git a/templates/conference_series.html b/templates/conference_series.html deleted file mode 100644 index 34513c2..0000000 --- a/templates/conference_series.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ series.name }} - Edward Betts{% endblock %} - -{% block content %} -
-

{{ series.name }}

- -
- {% if series.topic %} -
Topic
-
{{ series.topic }}
- {% endif %} - {% if series.usual_location or series.country %} -
Usual location
-
- {% set country = get_country(series.country) if series.country else None %} - {% if country %}{{ country.flag }} {% endif %}{{ series.usual_location or "" }} -
- {% endif %} - {% if series.cadence %} -
Cadence
-
{{ series.cadence }}
- {% endif %} - {% if series.url %} -
Website
-
{{ series.url }}
- {% endif %} - {% if series.notes %} -
Notes
-
{{ series.notes }}
- {% endif %} -
- -

Editions

- - - - - - - - - - - {% for item in conferences %} - - - - - - - {% endfor %} - -
DatesConferenceLocationAttendance
- {{ item.display_date }} - {% if item.date_status == "tentative" %} - tentative - {% elif item.date_status == "approximate" %} - approximate - {% endif %} - - {{ item.name }} - - {% set country = get_country(item.country) if item.country else None %} - {% if country %}{{ country.flag }} {% endif %}{{ item.location }} - - {% if item.going %} - going - {% endif %} - {% if item.registered %} - registered - {% endif %} - {% if item.linked_trip %} - {% set trip = item.linked_trip %} - - trip{% if trip.title != item.name %}: {{ trip.title }}{% endif %} - - {% endif %} -
-
-{% endblock %} diff --git a/templates/conference_series_list.html b/templates/conference_series_list.html deleted file mode 100644 index f9ccc5f..0000000 --- a/templates/conference_series_list.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Conference Series - Edward Betts{% endblock %} - -{% block content %} -
-

Conference Series

- - - - - - - - - - - - - {% for item in series_list %} - - - - - - - - {% endfor %} - -
SeriesTopicUsual locationConferencesNext
- {{ item.name }} - {% if item.attended %} - attended - {% endif %} - {% if item.url %} - ↗ - {% endif %} - {{ item.topic or "" }} - {% set country = get_country(item.country) if item.country else None %} - {% if country %}{{ country.flag }} {% endif %}{{ item.usual_location or "" }} - {{ item.count }} - {% if item.next_conf %} - {{ item.next_conf.display_date }} · {{ item.next_conf.name }} - {% endif %} -
-
-{% endblock %} diff --git a/templates/fixture/map.html b/templates/fixture/map.html deleted file mode 100644 index 9ae6da7..0000000 --- a/templates/fixture/map.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - -Map fixture - - - - - - - - - - - - -
- - - - - - - - - - - diff --git a/templates/flash_messages.html b/templates/flash_messages.html deleted file mode 100644 index 9c45802..0000000 --- a/templates/flash_messages.html +++ /dev/null @@ -1,12 +0,0 @@ -{% with messages = get_flashed_messages() %} -{% if messages %} -
- {% for message in messages %} - - {% endfor %} -
-{% endif %} -{% endwith %} diff --git a/templates/gaps.html b/templates/gaps.html index ceeed19..ae24883 100644 --- a/templates/gaps.html +++ b/templates/gaps.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% block title %}Gaps - Edward Betts{% endblock %} {% block content %}
@@ -18,31 +17,11 @@ {% for gap in gaps %} - - {% for event in gap.before %} -
- {% if event.url %} - {{ event.title_with_emoji }} - {% else %} - {{ event.title_with_emoji }} - {% endif %} -
- {% endfor %} - + {% for event in gap.before %}{% if not loop.first %}
{% endif %}{{ event.title or event.name }}{% endfor %} {{ gap.start.strftime("%A, %-d %b %Y") }} {{ (gap.end - gap.start).days }} days {{ gap.end.strftime("%A, %-d %b %Y") }} - - {% for event in gap.after %} -
- {% if event.url %} - {{ event.title_with_emoji }} - {% else %} - {{ event.title_with_emoji }} - {% endif %} -
- {% endfor %} - + {% for event in gap.after %}{% if not loop.first %}
{% endif %}{{ event.title or event.name }}{% endfor %} {% endfor %} diff --git a/templates/holiday_list.html b/templates/holiday_list.html deleted file mode 100644 index a627771..0000000 --- a/templates/holiday_list.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "base.html" %} -{% from "macros.html" import display_date %} -{% block title %}Holidays - Edward Betts{% endblock %} - -{% block content %} -
-

Public holidays

- - {% for item in items %} - {% set country = get_country(item.country) %} - - {% if loop.first or item.date != loop.previtem.date %} - - - {% else %} - - {% endif %} - - - - {% endfor %} -
{{ display_date(item.date) }}in {{ (item.date - today).days }} days{{ country.flag }} {{ country.name }}{{ item.display_name }}
- -

- UK school holidays (Bristol) - source -

- - {% for item in school_holidays %} - - - - - - - {% endfor %} -
{{ display_date(item.as_date) }}to {{ display_date(item.end_as_date) }}in {{ (item.as_date - today).days }} days{{ item.title }}
-
-{% endblock %} diff --git a/templates/event_list.html b/templates/index.html similarity index 52% rename from templates/event_list.html rename to templates/index.html index 6adb8da..5dcf8c3 100644 --- a/templates/event_list.html +++ b/templates/index.html @@ -3,11 +3,72 @@ - Agenda - Edward Betts - + Agenda + - + + + + @@ -27,7 +88,6 @@ "waste_schedule": "Waste schedule", "gwr_advance_tickets": "GWR advance tickets", "critical_mass": "Critical Mass", - "uk_school_holiday": "UK school holiday", } %} @@ -35,18 +95,15 @@ "bank_holiday": "bg-success-subtle", "conference": "bg-primary-subtle", "us_holiday": "bg-secondary-subtle", - "uk_school_holiday": "bg-warning-subtle", "birthday": "bg-info-subtle", "waste_schedule": "bg-danger-subtle", } %} -{% from "macros.html" import trip_link, display_date_no_year, trip_item with context %} {% from "navbar.html" import navbar with context %} {{ navbar() }} - {% include "flash_messages.html" %}

Agenda

@@ -70,49 +127,16 @@ Sunset: {{ sunset.strftime("%H:%M:%S") }} - {% if home_weather %} -
- Bristol weather: - {% for day in home_weather %} - - {{ day.date_obj.strftime("%-d %b") }} - {{ day.status }} - {{ day.temp_min }}–{{ day.temp_max }}°C - - {% endfor %} -
- {% endif %} - - {% if errors %} - {% for error in errors %} - - {% endfor %} - {% endif %} -

Stock markets

{% for market in stock_markets %}

{{ market }}

{% endfor %} - {% if current_trip %} -
-

Current trip

- {{ trip_item(current_trip) }} -
- {% endif %} +

Agenda

-
- Markets: - Hide while away - | Show all - | Hide all -
- - {% for event in events if start_event_list <= event.as_date <= end_event_list %} + {% for event in events if event.as_date >= two_weeks_ago %} {% if loop.first or event.date.year != loop.previtem.date.year or event.date.month != loop.previtem.date.month %}
@@ -142,12 +166,11 @@
{% if event.end_date %} - {% set duration = event.display_duration() %} - {% if duration %} + {% if event.end_as_date == event.as_date and event.has_time %} end: {{event.end_date.strftime("%H:%M") }} - (duration: {{duration}}) + (duration: {{event.end_date - event.date}}) {% elif event.end_date != event.date %} - to {{ event.end_as_date.strftime("%a, %d, %b") }} + {{event.end_date}} {% endif %} {% endif %}
@@ -155,7 +178,7 @@
@@ -165,21 +188,7 @@ {% endif %} {% endfor %} -
-
Page generation time
-
    -
  • Data gather took {{ "%.1f" | format(data_gather_seconds) }} seconds
  • -
  • Stock market open/close took - {{ "%.1f" | format(stock_market_times_seconds) }} seconds
  • - {% for name, seconds in timings %} -
  • {{ name }} took {{ "%.1f" | format(seconds) }} seconds
  • - {% endfor %} -
  • Render time: {{ "%.1f" | format(render_time) }} seconds
  • - -
- -
- + diff --git a/templates/launches.html b/templates/launches.html index f2576dc..c048d5e 100644 --- a/templates/launches.html +++ b/templates/launches.html @@ -1,63 +1,11 @@ {% extends "base.html" %} -{% block title %}Space launches - Edward Betts{% endblock %} - {% block content %}

Space launches

-

Filters

- -

Mission type: - - {% if request.args.type %}🗙{% endif %} - - {% for t in mission_types | sort %} - {% if t == request.args.type %} - {{ t }} - {% else %} - - {{ t }} - - {% endif %} - {% if not loop.last %} | {% endif %} - {% endfor %} -

- -

Vehicle: - {% if request.args.rocket %}🗙{% endif %} - - {% for r in rockets | sort %} - {% if r == request.args.rockets %} - {{ r }} - {% else %} - - {{ r }} - - {% endif %} - {% if not loop.last %} | {% endif %} - {% endfor %} -

- -

Orbit: - {% if request.args.orbit %}🗙{% endif %} - - {% for name, abbrev in orbits | sort %} - {% if abbrev == request.args.orbit %} - {{ name }} - {% else %} - - {{ name }} - - {% endif %} - {% if not loop.last %} | {% endif %} - {% endfor %} -

- - {% for launch in launches %} - {% set highlight =" bg-primary-subtle" if launch.slug in config.FOLLOW_LAUNCHES else "" %} - {% set country = get_country(launch.country_code) %} -
+ {% for launch in rockets %} +
{{ launch.t0_date }}
@@ -68,20 +16,8 @@
launch status: {{ launch.status.abbrev }} - {% if launch.is_active_crewed %} - In space - {% endif %} - {% if launch.is_future and launch.probability %}{{ launch.probability }}%{% endif %}
-
-
- {% if launch.image %} -
- -
- {% endif %} - {{ country.flag }} - {{ launch.rocket.full_name }} +
{{ launch.rocket }} – {{launch.mission.name }} – @@ -94,29 +30,14 @@ ({{ launch.launch_provider_type }}) — {{ launch.orbit.name }} ({{ launch.orbit.abbrev }}) - — - {{ launch.mission.type }} -
-
- {% if launch.pad_wikipedia_url %} - {{ launch.pad_name }} - {% else %} - {{ launch.pad_name }} {% if launch.pad_name != "Unknown Pad" %}(no Wikipedia article){% endif %} - {% endif %} - — {{ launch.location }} -
- {% if launch.mission.agencies | count %} -
- {% for agency in launch.mission.agencies %} - {% set agency_country = get_country(agency.country_code) %} - {%- if not loop.first %}, {% endif %} - {{agency.name }} - {{ agency_country.flag }} - ({{ agency.type }}) {# #} - {% endfor %} -
+
+ {% if launch.pad_wikipedia_url %} + {{ launch.pad_name }} + {% else %} + {{ launch.pad_name }} {% if launch.pad_name != "Unknown Pad" %}(no Wikipedia article){% endif %} {% endif %} -
+ — {{ launch.location }}
+ {% if launch.mission %} {% for line in launch.mission.description.splitlines() %}

{{ line }}

@@ -124,13 +45,7 @@ {% else %}

No description.

{% endif %} - {% if launch.weather_concerns and launch.status.name != "Launch Successful" %} -

Weather concerns

- {% for line in launch.weather_concerns.splitlines() %} -

{{ line }}

- {% endfor %} - {% endif %} -
+
{% endfor %} diff --git a/templates/macros.html b/templates/macros.html deleted file mode 100644 index cb96e12..0000000 --- a/templates/macros.html +++ /dev/null @@ -1,534 +0,0 @@ -{% macro display_datetime(dt) %}{{ dt.strftime("%a, %d, %b %Y %H:%M %z") }}{% endmacro %} -{% macro display_time(dt) %} - {% if dt %}{{ dt.strftime("%H:%M %z") }}{% endif %} -{% endmacro %} -{% macro display_date(dt) %}{{ dt.strftime("%a %-d %b %Y") }}{% endmacro %} -{% macro display_date_no_year(dt) %}{{ dt.strftime("%a %-d %b") }}{% endmacro %} -{% macro display_conf_date_no_year(dt) %}{%- if dt.hour is defined %}{{ dt.strftime("%a %-d %b %H:%M") }}{% else %}{{ dt.strftime("%a %-d %b") }}{% endif %}{% endmacro %} - -{% macro format_distance(distance) %} - {{ "{:,.0f} km / {:,.0f} miles".format(distance, distance / 1.60934) }} -{% endmacro %} - -{% macro trip_link(trip) %} - {{ trip.title }} -{% endmacro %} - -{% macro conference_row(item, badge, show_flags=True) %} - {% set country = get_country(item.country) if item.country else None %} -
{{ item.start.strftime("%a, %d %b %Y") }}
-
{{ item.end.strftime("%a, %d %b") }}
-
- {% if item.url %} - {{ item.name }} - {% else %} - {{ item.name }} - {% endif %} - {% if item.going and not (item.accommodation_booked or item.travel_booked) %} - - {{ badge }} - - {% endif %} - {% if item.accommodation_booked %} - accommodation - {% endif %} - {% if item.transport_booked %} - transport - {% endif %} -
-
- {% if item.price and item.currency %} - {{ "{:,d}".format(item.price | int) }} {{ item.currency }} - {% if item.currency != "GBP" and item.currency in fx_rate %} - {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% elif item.free %} - free to attend - {% endif %} -
-
{{ item.topic }}
-
{{ item.location }}
-
{{ display_date(item.cfp_end) if item.cfp_end else "" }}
-
- {% if country %} - {% if show_flags %}{{ country.flag }}{% endif %} {{ country.name }} - {% elif item.online %} - 💻 Online - {% else %} - - country code {{ item.country }} not found - - {% endif %} -
-{% endmacro %} - -{% macro accommodation_row(item, badge, show_flags=True) %} - {% set country = get_country(item.country) %} - - {% set nights = (item.to.date() - item.from.date()).days %} -
{{ item.from.strftime("%a, %d %b %Y") }}
-
{{ item.to.strftime("%a, %d %b") }}
-
{% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}
-
{{ item.operator }}
-
{{ item.location }}
-
- {% if country %} - {% if show_flags %}{{ country.flag }}{% endif %} {{ country.name }} - {% else %} - - country code {{ item.country }} not found - - {% endif %} -
-
- {% if g.user.is_authenticated and item.url %} - {{ item.name }} - {% else %} - {{ item.name }} - {% endif %} -
-
- {% if g.user.is_authenticated and item.price and item.currency %} - {{ "{:,f}".format(item.price) }} {{ item.currency }} - {% if item.currency != "GBP" %} - {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% endif %} -
-{% endmacro %} - -{% macro flightradar24_url(flight) -%} -https://www.flightradar24.com/data/flights/{{ flight.airline_detail.iata | lower + flight.flight_number }} -{%- endmacro %} - -{% macro flight_booking_row(booking, show_flags=True) %} -
- {% if g.user.is_authenticated %} - {{ booking.booking_reference or "reference missing" }} - {% else %} - redacted - {% endif %} -
- -
- {% if g.user.is_authenticated and booking.price and booking.currency %} - {{ "{:,f}".format(booking.price) }} {{ booking.currency }} - {% if booking.currency != "GBP" %} - {{ "{:,.2f}".format(booking.price / fx_rate[booking.currency]) }} GBP - {% endif %} - {% endif %} -
- {% for i in range(9) %} -
- {% endfor %} - - {% for item in booking.flights %} - {% set full_flight_number = item.airline_code + item.flight_number %} - {% set radarbox_url = "https://www.radarbox.com/data/flights/" + full_flight_number %} -
-
-
{{ item.depart.strftime("%a, %d %b %Y") }}
-
{{ item.from }} → {{ item.to }}
-
{{ item.depart.strftime("%H:%M") }}
-
- {% if item.arrive %} - {{ item.arrive.strftime("%H:%M") }} - {% if item.arrive.date() != item.depart.date() %}+1 day{% endif %} - {% endif %} -
-
{{ item.duration }}
-
{{ full_flight_number }}
- -
- {% if item.distance %} - {{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }} - {% endif %} -
-
{{ "{:,.1f}".format(item.co2_kg) }} kg
- {% endfor %} -{% endmacro %} - -{% macro flight_row(item) %} - {% set full_flight_number = item.airline_code + item.flight_number %} - {% set radarbox_url = "https://www.radarbox.com/data/flights/" + full_flight_number %} -
{{ item.depart.strftime("%a, %d %b %Y") }}
-
{{ item.from }} → {{ item.to }}
-
{{ item.depart.strftime("%H:%M") }}
-
- {% if item.arrive %} - {{ item.arrive.strftime("%H:%M") }} - {% if item.arrive.date() != item.depart.date() %}+1 day{% endif %} - {% endif %} -
-
{{ item.duration }}
-
{{ full_flight_number }}
-
- {% if g.user.is_authenticated %} - {{ item.booking_reference }} - {% else %} - redacted - {% endif %} -
- -
- {% if item.distance %} - {{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }} - {% endif %} -
-
- {% if g.user.is_authenticated and item.price and item.currency %} - {{ "{:,f}".format(item.price) }} {{ item.currency }} - {% if item.currency != "GBP" %} - {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% endif %} -
-{% endmacro %} - -{% macro train_row(item) %} - {% set url = item.url %} -
{{ item.depart.strftime("%a, %d %b %Y") }}
- -
{{ item.depart.strftime("%H:%M") }}
-
- {% if item.arrive %} - {{ item.arrive.strftime("%H:%M") }} - {% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %} - {% endif %} -
-
{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins
-
{{ item.operator }}
-
- {% if g.user.is_authenticated %} - {{ item.booking_reference }} - {% else %} - redacted - {% endif %} -
-
- {% for leg in item.legs %} - {% if leg.url %} - [{{ loop.index }}] - {% endif %} - {% endfor %} -
-
- {% if item.distance %} - {{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }} - {% endif %} -
-
- {% if g.user.is_authenticated and item.price and item.currency %} - {{ "{:,f}".format(item.price) }} {{ item.currency }} - {% if item.currency != "GBP" and item.currency in fx_rate %} - {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% endif %} -
-{% endmacro %} - -{% macro coach_row(item) %} - {% set url = item.url %} -
{{ item.depart.strftime("%a, %d %b %Y") }}
- -
{{ item.depart.strftime("%H:%M") }}
-
- {% if item.arrive %} - {{ item.arrive.strftime("%H:%M") }} - {% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %} - {% endif %} -
-
{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins
-
{{ item.operator }}
-
- {% if g.user.is_authenticated %} - {{ item.booking_reference }} - {% else %} - redacted - {% endif %} -
-
-
-
- {% if item.distance %} - {{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }} - {% endif %} -
-
- {% if g.user.is_authenticated and item.price and item.currency %} - {{ "{:,f}".format(item.price) }} {{ item.currency }} - {% if item.currency != "GBP" and item.currency in fx_rate %} - {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% endif %} -
-{% endmacro %} - -{% macro bus_row(item) %} - {% set url = item.url %} -
{{ item.depart.strftime("%a, %d %b %Y") }}
- -
{{ item.depart.strftime("%H:%M") }}
-
- {% if item.arrive %} - {{ item.arrive.strftime("%H:%M") }} - {% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %} - {% endif %} -
-
{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins
-
{{ item.operator }}
-
- {% if g.user.is_authenticated %} - {{ item.booking_reference }} - {% else %} - redacted - {% endif %} -
-
-
-
- {% if item.distance %} - {{ "{:,.0f} km / {:,.0f} miles".format(item.distance, item.distance / 1.60934) }} - {% endif %} -
-
- {% if g.user.is_authenticated and item.price and item.currency %} - {{ "{:,f}".format(item.price) }} {{ item.currency }} - {% if item.currency != "GBP" and item.currency in fx_rate %} - {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% endif %} -
-{% endmacro %} - -{% macro ferry_row(item) %} -
{{ item.depart.strftime("%a, %d %b %Y") }}
-
- {{ item.from }} → {{ item.to }} -
- -
{{ item.depart.strftime("%H:%M") }}
-
- {% if item.arrive %} - {{ item.arrive.strftime("%H:%M") }} - {% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %} - {% endif %} -
- -
-
{{ item.operator }}
-
-
-
- -
- {% if g.user.is_authenticated and item.price and item.currency %} - {{ "{:,f}".format(item.price) }} {{ item.currency }} - {% if item.currency != "GBP" %} - {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP - {% endif %} - {% endif %} -
- - - {#
{{ item | pprint }}
#} -{% endmacro %} - -{% macro flag(trip, flag) %}{% if trip.show_flags %}{{ flag }}{% endif %}{% endmacro %} - -{% macro conference_list(trip) %} - {% for item in trip.conferences %} - {% set country = get_country(item.country) if item.country else None %} -
-
-
- {{ item.name }} - - {{ display_conf_date_no_year(item.attend_start if item.attend_start else item.start) }} to {{ display_conf_date_no_year(item.attend_end if item.attend_end else item.end) }} - {% if item.attend_start or item.attend_end %} - (full conference: {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }}) - {% endif %} - -
-

- Topic: {{ item.topic }} - | Venue: {{ item.venue }} - | Location: {{ item.location }} - {% if country %} - {{ flag(trip, country.flag) }} - {% elif item.online %} - 💻 Online - {% else %} - - country code {{ item.country }} not found - - {% endif %} - {% if item.free %} - | free to attend - {% elif item.price and item.currency %} - | price: {{ item.price }} {{ item.currency }} - {% endif %} -

-
-
- {% endfor %} -{% endmacro %} - -{% macro render_trip_element(e, trip) %} - {% set item = e.detail %} - {% if e.element_type == "check-in" %} - {% set nights = (item.to.date() - item.from.date()).days %} -
- {{ e.get_emoji() }} {{ item.name }} - {% if item.operator and item.operator != item.name %}{{ item.operator }}{% endif %} - ({% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}) -
- {% elif e.element_type == "check-out" %} -
- {{ e.get_emoji() }} Check out: {{ item.name }} - {% if item.operator and item.operator != item.name %}{{ item.operator }}{% endif %} -
- {% elif e.element_type != "conference" %} - {# Transport: flight, train, ferry, coach, bus, car #} - {% set has_arrive = item.arrive is defined and item.arrive %} - {# item.depart may be a date (no .date() method) or a datetime (has .date()) #} - {% set has_time = item.depart is defined and item.depart and item.depart.hour is defined %} - {% set depart_date = item.depart.date() if has_time else item.depart %} - {% set arrive_date = item.arrive.date() if (has_arrive and item.arrive.hour is defined) else item.arrive %} - {% set is_overnight = has_arrive and depart_date != arrive_date %} - {% set dur_mins = ((item.arrive - item.depart).total_seconds() // 60) | int if (has_time and has_arrive) else none %} -
- {% if is_overnight %}🌙{% else %}{{ e.get_emoji() }}{% endif %} - {{ e.start_loc }} → {{ e.end_loc }} - {% if has_time %} - · {{ item.depart.strftime("%H:%M") }}{% if has_arrive and item.arrive.hour is defined %} → {{ item.arrive.strftime("%H:%M") }}{% if is_overnight %} +1 day{% endif %}{% endif %} - {% endif %} - {% if dur_mins %} - {%- set h = dur_mins // 60 %}{%- set m = dur_mins % 60 %} - 🕒{% if h %}{{ h }}h {% endif %}{% if m %}{{ m }}m{% endif %} - {% endif %} - {% if e.element_type == "flight" %} - · {{ item.airline_name }} {{ item.airline_code }}{{ item.flight_number }} - {% elif item.operator %} - · {{ item.operator }} - {% endif %} - {% if item.distance %} - · {{ "{:,.0f} km".format(item.distance) }} - {% endif %} - {% if item.co2_kg is defined and item.co2_kg is not none %} - CO₂ {{ "{:,.1f}".format(item.co2_kg) }} kg - {% endif %} -
- {% endif %} -{% endmacro %} - -{% macro trip_item(trip) %} - {% set distances_by_transport_type = trip.distances_by_transport_type() %} - {% set total_distance = trip.total_distance() %} - {% set total_co2_kg = trip.total_co2_kg() %} - {% set end = trip.end %} - {% set trip_end = end or trip.start %} - {% set is_current = trip.start <= today and trip_end >= today %} -
-

- {{ trip_link(trip) }} - ({{ display_date(trip.start) }})

- {% set school_holidays = trip_school_holiday_map.get(trip.start.isoformat(), []) if trip_school_holiday_map is defined else [] %} - {% if school_holidays %} -
- UK school holiday - {% for item in school_holidays %} - {{ item.title }} ({{ display_date_no_year(item.as_date) }} to {{ display_date_no_year(item.end_as_date) }}) - {% endfor %} -
- {% endif %} -
    - {% for c in trip.countries %} -
  • {{ c.flag }} {{ c.name }}
  • - {% endfor %} -
- {% if end %} -
Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }} - {% if g.user.is_authenticated and trip.start <= today %} - photos - {% endif %} -
- {% else %} -
Start: {{ display_date_no_year(trip.start) }} (end date missing)
- {% endif %} -
- {% if total_distance %} - {{ format_distance(total_distance) }} - {% endif %} - {% if distances_by_transport_type %} - {% for transport_type, distance in distances_by_transport_type %} - {{ transport_type | title }}: {{format_distance(distance) }} - {% endfor %} - {% endif %} - {% if total_co2_kg %} - CO₂ {{ "{:,.1f}".format(total_co2_kg) }} kg - {% endif %} - {% set co2_by_transport = trip.co2_by_transport_type() %} - {% if co2_by_transport %} - {% for transport_type, co2_kg in co2_by_transport %} - {{ transport_type | title }} CO₂ {{ "{:,.1f}".format(co2_kg) }} kg - {% endfor %} - {% endif %} -
- - {% if trip.schengen_compliance %} -
- Schengen: - {% if trip.schengen_compliance.is_compliant %} - ✅ Compliant - {% else %} - ❌ Non-compliant - {% endif %} - ({{ trip.schengen_compliance.total_days_used }}/90 days used) -
- {% endif %} - - {{ conference_list(trip) }} - - {% set trip_weather = trip_weather_map.get(trip.start.isoformat(), {}) if trip_weather_map is defined else {} %} - {% for day, elements in trip.elements_grouped_by_day() %} - {% set weather = trip_weather.get(day.isoformat()) %} -

{{ display_date_no_year(day) }} - {% if weather %} - - {{ weather.status }} - {{ weather.temp_min }}–{{ weather.temp_max }}°C - {{ weather.detailed_status }} - - {% endif %} - {% if g.user.is_authenticated and day <= today %} - photos - {% endif %} -

- {% for e in elements %} - {{ render_trip_element(e, trip) }} - {% endfor %} - {% endfor %} - -
-{% endmacro %} diff --git a/templates/meteors.html b/templates/meteors.html deleted file mode 100644 index a9683cd..0000000 --- a/templates/meteors.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Meteor Showers 2025 - Edward Betts{% endblock %} - -{% block content %} -
-

Meteor Showers 2025

- -

Major meteor showers visible throughout 2025. All times are approximate and viewing conditions depend on light pollution, weather, and moon phase.

- - {% for meteor in meteors %} -
-
- {{ meteor.peak }} -
- {{ meteor.active }} -
-
- Rate: - {{ meteor.rate }} -
- {{ meteor.radiant }} -
-
-
-

{{ meteor.name }}

-

Moon Phase: {{ meteor.moon_phase }}

-

Best Visibility: {{ meteor.visibility }}

-

{{ meteor.description }}

-
-
-
- {% endfor %} - -
-

Viewing Tips

-
    -
  • Find a dark location away from city lights
  • -
  • Allow your eyes to adjust to darkness for 20-30 minutes
  • -
  • Look northeast for most showers, but meteors can appear anywhere
  • -
  • Best viewing is typically after midnight
  • -
  • No telescope needed - use naked eye viewing
  • -
  • Check weather conditions and moon phase before planning
  • -
-
- -
-

About Meteor Showers

-

Meteor showers occur when Earth passes through debris trails left by comets or asteroids. The debris burns up in our atmosphere, creating the streaks of light we see as "shooting stars."

- -

Data Sources: Information compiled from NASA, American Meteor Society, and astronomical observations for 2025.

-
-
-{% endblock %} \ No newline at end of file diff --git a/templates/navbar.html b/templates/navbar.html index 373c053..444fa14 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -1,34 +1,12 @@ {% macro navbar() %} -{% set pages_before_dropdowns = [ +{% set pages = [ {"endpoint": "index", "label": "Home" }, - {"endpoint": "recent", "label": "Recent" }, - {"endpoint": "calendar_page", "label": "Calendar" }, -] %} - -{% set pages_after_dropdowns = [ + {"endpoint": "conference_list", "label": "Conference" }, {"endpoint": "travel_list", "label": "Travel" }, {"endpoint": "accommodation_list", "label": "Accommodation" }, {"endpoint": "gaps_page", "label": "Gaps" }, - {"endpoint": "weekends", "label": "Weekends" }, {"endpoint": "launch_list", "label": "Space launches" }, - {"endpoint": "meteor_list", "label": "Meteor showers" }, - {"endpoint": "holiday_list", "label": "Holidays" }, - {"endpoint": "schengen_report", "label": "Schengen" }, - ] + ([{"endpoint": "birthday_list", "label": "Birthdays" }] - if g.user.is_authenticated else []) -%} - -{% set trip_pages = [ - {"endpoint": "trip_future_list", "label": "Future trips" }, - {"endpoint": "trip_past_list", "label": "Past trips" }, - {"endpoint": "trip_stats", "label": "Trip statistics" }, -] %} - -{% set conference_pages = [ - {"endpoint": "conference_list", "label": "Conferences" }, - {"endpoint": "past_conference_list", "label": "Past conferences" }, - {"endpoint": "conference_series_list", "label": "Conference series" }, ] %} @@ -40,7 +18,7 @@
diff --git a/templates/schengen_report.html b/templates/schengen_report.html deleted file mode 100644 index b34c02d..0000000 --- a/templates/schengen_report.html +++ /dev/null @@ -1,230 +0,0 @@ -{% extends "base.html" %} - -{% set heading = "Schengen area compliance report" %} - -{% block title %}{{ heading }} - Edward Betts{% endblock %} - -{% block content %} -
-

{{ heading }}

- -
-
-
-
-
Current Status
-
-
-
-
-
Compliance Status
- {% if current_compliance.is_compliant %} - ✅ COMPLIANT - {% else %} - ❌ NON-COMPLIANT - {% endif %} -
-
-
Days Used
-
{{ current_compliance.total_days_used }}/90
-
-
- -
-
- {% if current_compliance.is_compliant %} -
Days Remaining
-
{{ current_compliance.days_remaining }}
- {% else %} -
Days Over Limit
-
{{ current_compliance.days_over_limit }}
- {% endif %} -
-
- {% if current_compliance.next_reset_date %} -
Next Reset Date
-
{{ current_compliance.next_reset_date.strftime('%Y-%m-%d') }}
- {% endif %} -
-
- -
-
Current 180-day Period
-
- {{ current_compliance.current_180_day_period[0].strftime('%Y-%m-%d') }} to - {{ current_compliance.current_180_day_period[1].strftime('%Y-%m-%d') }} -
-
-
-
- - {% if current_compliance.stays_in_period %} -
-
-
Stays in Current 180-day Period
-
-
-
- - - - - - - - - - - - {% for stay in current_compliance.stays_in_period %} - - - - {% set country = get_country(stay.country) %} - - - - - {% endfor %} - -
Entry DateExit DateCountryDaysTrip
{{ stay.entry_date.strftime('%Y-%m-%d') }} - {% if stay.exit_date %} - {{ stay.exit_date.strftime('%Y-%m-%d') }} - {% else %} - ongoing - {% endif %} - {{ country.flag }} {{ country.name }}{{ stay.days }} - {% if stay.trip_date and stay.trip_name %} - - {{ stay.trip_name }} - - {% else %} - - - {% endif %} -
-
-
-
- {% endif %} -
- -
- {% if warnings %} -
-
-
Warnings
-
-
- {% for warning in warnings %} - - {% endfor %} -
-
- {% endif %} - -
-
-
Compliance History
-
-
-
- -
-
-
-
-
- - {% if trips_with_compliance %} -
-
-
Trip Compliance History
-
-
-
- - - - - - - - - - - - {% for trip_date, calculation in trips_with_compliance.items() %} - - - - - - - - {% endfor %} - -
Trip DateTrip NameDays UsedDays RemainingStatus
{{ trip_date.strftime('%Y-%m-%d') }} - - {% for trip in trip_list if trip.start == trip_date %} - {{ trip.title }} {{ trip.country_flags }} - {% endfor %} - - {{ calculation.total_days_used }}/90{{ calculation.days_remaining }} - {% if calculation.is_compliant %} - Compliant - {% else %} - Non-compliant - {% endif %} -
-
-
-
- {% endif %} -
-{% endblock %} - -{% block scripts %} - - -{% endblock %} diff --git a/templates/show_error.html b/templates/show_error.html index a3b4d63..dd79c3c 100644 --- a/templates/show_error.html +++ b/templates/show_error.html @@ -1,7 +1,5 @@ {% extends "base.html" %} -{% block title %}Agenda error - Edward Betts{% endblock %} - {% block style %} {% endblock %} diff --git a/templates/travel.html b/templates/travel.html index ebb937b..86ae8bc 100644 --- a/templates/travel.html +++ b/templates/travel.html @@ -1,23 +1,23 @@ {% extends "base.html" %} -{% from "macros.html" import flight_booking_row, train_row with context %} -{% block title %}Travel - Edward Betts{% endblock %} +{% block travel %} +{% endblock %} -{% set flight_column_count = 11 %} -{% set column_count = 10 %} +{% macro display_datetime(dt) %}{{ dt.strftime("%a, %d, %b %Y %H:%M %z") }}{% endmacro %} +{% macro display_time(dt) %}{{ dt.strftime("%H:%M %z") }}{% endmacro %} {% block style %} -{% endblock %} - -{% macro section(heading, item_list) %} - {% if item_list %} - {% set items = item_list | list %} -
-

{{ heading }}

-

Trip statistics · {{ items | count }} trips

-
-
- Total distance - {{ format_distance(total_distance) }} -
- {% for transport_type, distance in distances_by_transport_type %} -
- {{ transport_type | title }} - {{ format_distance(distance) }} -
- {% endfor %} -
- Total CO₂ - {{ "{:,.1f}".format(total_co2_kg / 1000.0) }} t -
- {% for transport_type, co2_kg in co2_by_transport_type %} -
- {{ transport_type | title }} CO₂ - {{ "{:,.1f}".format(co2_kg) }} kg -
- {% endfor %} -
-
- - {% for trip in items %} - {{ trip_item(trip) }} - {% endfor %} - {% endif %} -{% endmacro %} - - -{% block content %} -
-
- {{ section(heading, trips) }} -
-
-
-
-
-{% endblock %} - -{% block scripts %} - - - - - - - -{% endblock %} diff --git a/templates/trip/stats.html b/templates/trip/stats.html deleted file mode 100644 index 52d3558..0000000 --- a/templates/trip/stats.html +++ /dev/null @@ -1,217 +0,0 @@ -{% extends "base.html" %} - -{% from "macros.html" import format_distance with context %} - -{% set heading = "Trip statistics" %} - -{% block title %}{{ heading }} - Edward Betts{% endblock %} - -{% block style %} - -{% endblock %} - -{% macro stat_list(id, label, counter, show_top=5) %} -
- {{ label }}: {{ counter | count }} - {% if counter | count > 0 %} - -
-
- {% for item, count in counter.most_common() %} - {{ item }} {{ count }} - {% endfor %} -
-
- {% endif %} -
-{% endmacro %} - -{% macro format_co2(co2_kg) %} - {% if co2_kg >= 1000 %} - {{ "{:,.2f}".format(co2_kg / 1000.0) }} tonnes - {% else %} - {{ "{:,.0f}".format(co2_kg) }} kg - {% endif %} -{% endmacro %} - -{% block content %} -
-

Trip statistics

- -
-
Overall Summary
-
-
-
-
Trips: {{ count }}
-
Conferences: {{ conferences }}
-
Total distance: {{ format_distance(total_distance) }}
- {% for transport_type, distance in distances_by_transport_type %} -
{{ transport_type | title }}: {{ format_distance(distance) }}
- {% endfor %} - {% if total_co2_kg %} -
CO₂: {{ format_co2(total_co2_kg) }}
- {% endif %} - {% for transport_type, co2_kg in co2_by_transport_type %} -
{{ transport_type | title }} CO₂: {{ format_co2(co2_kg) }}
- {% endfor %} -
-
-
Flight segments: {{ overall_stats.flight_count }}
-
Train segments: {{ overall_stats.train_count }}
- {% if co2_by_transport_type %} -
- -
- {% endif %} -
-
- -
- - {{ stat_list("overall-airlines", "Airlines", overall_stats.airlines) }} - {{ stat_list("overall-airports", "Airports", overall_stats.airports) }} - {{ stat_list("overall-stations", "Stations", overall_stats.stations) }} -
-
- - {% for year, year_stats in yearly_stats | dictsort(reverse=True) %} - {% set countries = year_stats.countries | default([]) | sort(attribute="name") %} - {% set new_countries = year_stats.new_countries | default([]) %} - -
-
- {{ year }} - {{ year_stats.count }} trips -
-
-
-
-
Trips: {{ year_stats.count }}
-
Conferences: {{ year_stats.conferences }}
-
Distance: {{ format_distance(year_stats.total_distance or 0) }}
- {% if year_stats.distances_by_transport_type %} - {% for transport_type, distance in year_stats.distances_by_transport_type.items() %} -
{{ transport_type | title }}: {{ format_distance(distance) }}
- {% endfor %} - {% endif %} -
-
-
Flight segments: {{ year_stats.flight_count or 0 }}
-
Train segments: {{ year_stats.train_count or 0 }}
- {% if year_stats.co2_kg %} -
CO₂: {{ format_co2(year_stats.co2_kg) }}
- {% for transport_type, co2_kg in year_stats.co2_by_transport_type.items() %} -
{{ transport_type | title }} CO₂: {{ format_co2(co2_kg) }}
- {% endfor %} -
- -
- {% endif %} -
-
- -
- Countries: {{ countries | count }} - {% if new_countries %}({{ new_countries | count }} new){% endif %} -
- {% for c in countries %} - - {{ c.flag }} {{ c.name }} - {% if c in new_countries %} - new - {% endif %} - - {% endfor %} -
-
- - {% if year_stats.airlines %} - {{ stat_list("airlines-" ~ year, "Airlines", year_stats.airlines) }} - {% endif %} - {% if year_stats.airports %} - {{ stat_list("airports-" ~ year, "Airports", year_stats.airports) }} - {% endif %} - {% if year_stats.stations %} - {{ stat_list("stations-" ~ year, "Stations", year_stats.stations) }} - {% endif %} -
-
- {% endfor %} -
-{% endblock %} - -{% block scripts %} - - -{% endblock %} diff --git a/templates/trip_debug.html b/templates/trip_debug.html deleted file mode 100644 index 9d7cb3f..0000000 --- a/templates/trip_debug.html +++ /dev/null @@ -1,133 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Debug: {{ trip.title }} ({{ trip.start }}) - Edward Betts{% endblock %} - -{% block style %} - -{% endblock %} - -{% block content %} -
-
-

🐛 Trip Debug Information

-

Raw trip object data for: {{ trip.title }}

- ← Back to Trip Page - - -
- -
-
-

Trip Object (JSON)

-
{{ trip_json }}
-
-
-

Trip Object (YAML)

-
{{ trip_yaml }}
-
-
-
-{% endblock %} - -{% block scripts %} - -{% endblock %} diff --git a/templates/trip_list_text.html b/templates/trip_list_text.html deleted file mode 100644 index a5aa5b5..0000000 --- a/templates/trip_list_text.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "base.html" %} - -{% from "macros.html" import trip_link, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} - -{% set row = { "flight": flight_row, "train": train_row } %} - -{% block style %} - - - -{% set conference_column_count = 7 %} -{% set accommodation_column_count = 7 %} -{% set travel_column_count = 8 %} - -{% endblock %} - - -{% block content %} -
- -

Trips

-

{{ future | count }} trips

- {% for trip in future %} - {% set end = trip.end %} -
- {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}: - {{ trip.title }} — {{ trip.locations_str }} -
- {% endfor %} - - -
-{% endblock %} diff --git a/templates/trip_page.html b/templates/trip_page.html deleted file mode 100644 index 10adf94..0000000 --- a/templates/trip_page.html +++ /dev/null @@ -1,549 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ trip.title }} ({{ display_date(trip.start) }}) - Edward Betts{% endblock %} - -{% from "macros.html" import trip_link, display_datetime, display_date_no_year, display_date, display_conf_date_no_year, conference_row, accommodation_row, flight_row, train_row, ferry_row, coach_row, bus_row with context %} - -{% set row = {"flight": flight_row, "train": train_row, "ferry": ferry_row, "coach": coach_row, "bus": bus_row} %} - -{% macro trip_duration(depart, arrive) -%} - {%- if depart.hour is defined and arrive.hour is defined -%} - {%- set mins = ((arrive - depart).total_seconds() // 60) | int -%} - {%- set h = mins // 60 -%} - {%- set m = mins % 60 -%} - {%- if h %}{{ h }}h {% endif -%} - {%- if m %}{{ m }}m{% elif h %}0m{% endif -%} - {%- endif -%} -{%- endmacro %} - -{% macro next_and_previous() %} -

- {% if prev_trip %} - previous: {{ trip_link(prev_trip) }} ({{ (trip.start - prev_trip.end).days }} days) - {% endif %} - {% if next_trip %} - next: {{ trip_link(next_trip) }} ({{ (next_trip.start - trip.end).days }} days) - {% endif %} -

-{% endmacro %} - -{% block style %} - -{% if coordinates or routes %} - -{% endif %} - - -{% set conference_column_count = 7 %} -{% set accommodation_column_count = 7 %} -{% set travel_column_count = 9 %} - -{% endblock %} - -{% set end = trip.end %} -{% set total_distance = trip.total_distance() %} -{% set distances_by_transport_type = trip.distances_by_transport_type() %} -{% set total_co2_kg = trip.total_co2_kg() %} -{% set co2_by_transport_type = trip.co2_by_transport_type() %} - -{% block content %} -
-
-
-
{{ next_and_previous() }}
-

{{ trip.title }}

-

- {% if end %} - {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }} - ({{ (end - trip.start).days }} nights) - {% else %} - {{ display_date_no_year(trip.start) }} (end date missing) - {% endif %} -

- -
-
    - {% for location, country in trip.locations() %} -
  • {{ country.flag if trip.show_flags }} {{ location }}
  • - {% endfor %} -
- {% if destination_times %} -
- Destination time zones - - - - - - - - - - {% for item in destination_times %} - - - - - - {% endfor %} - -
DestinationTimezoneDifference from UK
{{ item.destination_label }}{{ item.timezone or "Unknown" }}{{ item.offset_display }}
-
- {% endif %} - -
- {% if total_distance %} - {{ "{:,.0f} km / {:,.0f} mi".format(total_distance, total_distance / 1.60934) }} - {% endif %} - {% if distances_by_transport_type %} - {% for transport_type, distance in distances_by_transport_type %} - {{ transport_type | title }}: {{ "{:,.0f} km".format(distance) }} - {% endfor %} - {% endif %} - {% if total_co2_kg %} - CO₂ {{ "{:,.1f}".format(total_co2_kg) }} kg - {% endif %} - {% if co2_by_transport_type %} - {% for transport_type, co2_kg in co2_by_transport_type %} - {{ transport_type | title }} CO₂ {{ "{:,.1f}".format(co2_kg) }} kg - {% endfor %} - {% endif %} -
- - - {% set delta = human_readable_delta(trip.start) %} - {% if delta %} -
How long until trip: {{ delta }}
- {% endif %} - - {% if trip.schengen_compliance %} -
- Schengen Compliance: - {% if trip.schengen_compliance.is_compliant %} - ✅ Compliant - {% else %} - ❌ Non-compliant - {% endif %} -
- {{ trip.schengen_compliance.total_days_used }}/90 days used - {% if trip.schengen_compliance.is_compliant %} - ({{ trip.schengen_compliance.days_remaining }} remaining) - {% else %} - ({{ trip.schengen_compliance.days_over_limit }} over limit) - {% endif %} -
-
- {% endif %} -
- - {# ---- Chronological itinerary ---- #} - {% for day, day_elements in trip.elements_grouped_by_day() %} - {% set weather = trip_weather.get(day.isoformat()) if trip_weather else None %} -

- {{ display_date_no_year(day) }} - {% if weather %} - - {{ weather.status }} - {{ weather.temp_min }}–{{ weather.temp_max }}°C - {{ weather.detailed_status }} - - {% endif %} - {% if g.user.is_authenticated and day <= today %} - photos - {% endif %} -

- - {% for e in day_elements %} - - {% if e.element_type == "conference" %} - {% set item = e.detail %} - {% set country = get_country(item.country) if item.country else None %} -
-
-
- {{ item.name }} - - {{ display_conf_date_no_year(item.attend_start if item.attend_start else item.start) }} to {{ display_conf_date_no_year(item.attend_end if item.attend_end else item.end) }} - {% if item.attend_start or item.attend_end %} - (full conference: {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }}) - {% endif %} - -
-

- Topic: {{ item.topic }} - Venue: {{ item.venue }} - Location: {{ item.location }} - {% if country %} - {{ country.flag if trip.show_flags }} - {% elif item.online %} - 💻 Online - {% else %} - country code {{ item.country }} not found - {% endif %} - {% if item.free %} - free to attend - {% elif item.price and item.currency %} - price: {{ item.price }} {{ item.currency }} - {% endif %} - {% set free_days = conference_free_days.get(item.start | string) %} - {% if free_days %} - {% set days_before, days_after = free_days %} - {% if days_before > 0 %} - {{ days_before }} day{{ 's' if days_before != 1 }} to explore before - {% endif %} - {% if days_after > 0 %} - {{ days_after }} day{{ 's' if days_after != 1 }} to explore after - {% endif %} - {% endif %} -

-
-
- - {% elif e.element_type == "check-in" %} - {% set item = e.detail %} - {% set country = get_country(item.country) if item.country else None %} - {% set nights = (item.to.date() - item.from.date()).days %} -
-
-
- {{ e.get_emoji() }} - {{ item.name }} - {% if item.operator and item.operator != item.name %}{{ item.operator }}{% endif %} - ({% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}) -
-

- {{ item.location }} - {% if country %} - {{ country.flag if trip.show_flags }} - {% else %} - country code {{ item.country }} not found - {% endif %} - {% if item.address %} · {{ item.address }}{% endif %} - {% if g.user.is_authenticated and item.price and item.currency %} - {{ item.price }} {{ item.currency }} - {% endif %} -

-
-
- - {% elif e.element_type == "check-out" %} - {% set item = e.detail %} -
- {{ e.get_emoji() }} Check out: {{ item.name }} - {% if item.operator and item.operator != item.name %}{{ item.operator }}{% endif %} -
- - {% elif e.element_type == "flight" %} - {% set item = e.detail %} - {% set full_flight_number = item.airline_code + item.flight_number %} - {% set radarbox_url = "https://www.radarbox.com/data/flights/" + full_flight_number %} - {% set depart_date = item.depart.date() if item.depart.hour is defined else item.depart %} - {% set arrive_date = item.arrive.date() if (item.arrive and item.arrive.hour is defined) else item.arrive %} - {% set is_overnight = item.arrive and depart_date != arrive_date %} -
-
-
- ✈️ - {{ item.from_airport.name }} ({{ item.from_airport.iata }}) - → - {{ item.to_airport.name }} ({{ item.to_airport.iata }}) -
-
-
- {{ item.airline_name }} - {{ full_flight_number }} - · {{ item.depart.strftime("%H:%M") }} - {% if item.arrive %} - → {{ item.arrive.strftime("%H:%M") }}{% if is_overnight %} +1 day{% endif %} - 🕒{{ trip_duration(item.depart, item.arrive) }} - {% endif %} - {% if item.distance %} - 🌍 {{ "{:,.0f} km".format(item.distance) }} - {% endif %} - {% if item.co2_kg is defined and item.co2_kg is not none %} - CO₂ {{ "{:,.1f}".format(item.co2_kg) }} kg - {% endif %} -
- -
-
-
- - {% elif e.element_type == "train" %} - {% set item = e.detail %} - {% set has_time = item.depart.hour is defined and item.arrive.hour is defined %} - {% set depart_date = item.depart.date() if has_time else item.depart %} - {% set arrive_date = item.arrive.date() if has_time else item.arrive %} - {% set is_overnight = has_time and depart_date != arrive_date %} -
-
-
- {% if is_overnight %}🌙{% else %}🚆{% endif %} - {{ item.from }} → {{ item.to }} - {% if item.operator %}{{ item.operator }}{% endif %} - {% if is_overnight %}Night train{% endif %} -
-

- {% if has_time %} - {{ item.depart.strftime("%H:%M") }} - → {{ item.arrive.strftime("%H:%M") }}{% if is_overnight %} +1 day{% endif %} - {% endif %} - {% if item.class %} - {{ item.class }} - {% endif %} - {% if has_time %} - 🕒{{ trip_duration(item.depart, item.arrive) }} - {% endif %} - {% if item.distance %} - 🛤️ {{ "{:,.0f} km".format(item.distance) }} - {% endif %} - {% if item.co2_kg is defined and item.co2_kg is not none %} - CO₂ {{ "{:,.1f}".format(item.co2_kg) }} kg - {% endif %} - {% if item.coach %} - {% if is_overnight %}🛏️{% else %}💺{% endif %} Coach {{ item.coach }}{% if item.seat %}, Seat {% if item.seat is iterable and item.seat is not string %}{{ item.seat | join(" & ") }}{% else %}{{ item.seat }}{% endif %}{% endif %} - {% endif %} -

-
-
- - {% elif e.element_type in ("coach", "bus", "car") %} - {% set item = e.detail %} - {% set display_depart = item.display_depart if item.display_depart is defined else item.depart %} - {% set display_arrive = item.display_arrive if item.display_arrive is defined else item.arrive %} -
-
-
- {% if e.element_type == "car" %}🚗{% else %}🚌{% endif %} - {{ item.from }} → {{ item.to }} - {% if item.operator %}{{ item.operator }}{% endif %} -
-

- {% if display_depart.hour is defined and display_arrive.hour is defined %} - {{ display_depart.strftime("%H:%M") }} → {{ display_arrive.strftime("%H:%M") }} - {% endif %} - {% if item.class %} - {{ item.class }} - {% endif %} - {% if display_depart.hour is defined and display_arrive.hour is defined %} - 🕒{{ trip_duration(display_depart, display_arrive) }} - {% endif %} - {% if item.distance %} - 🛤️ {{ "{:,.0f} km".format(item.distance) }} - {% endif %} - {% if item.co2_kg is defined and item.co2_kg is not none %} - CO₂ {{ "{:,.1f}".format(item.co2_kg) }} kg - {% endif %} -

-
-
- - {% elif e.element_type == "ferry" %} - {% set item = e.detail %} -
-
-
- ⛴️ {{ item.from }} → {{ item.to }} - {{ item.operator }}{% if item.ferry %} · {{ item.ferry }}{% endif %} -
-

-

- {{ item.depart.strftime("%H:%M") }} → {{ item.arrive.strftime("%H:%M") }} - 🕒{{ trip_duration(item.depart, item.arrive) }} - {% if item.class %} - {{ item.class }} - {% endif %} - {% if item.co2_kg is defined and item.co2_kg is not none %} - CO₂ {{ "{:,.1f}".format(item.co2_kg) }} kg - {% endif %} -
- {% if item.vehicle %} -
🚗 Vehicle: {{ item.vehicle.type }} {% if g.user.is_authenticated %}({{ item.vehicle.registration }}) {% endif %} - {% if item.vehicle.extras %} - Extras: {{ item.vehicle.extras | join(", ") }}{% endif %} -
- {% endif %} - {% if g.user.is_authenticated %} -
- {% if item.booking_reference %}Booking reference: {{ item.booking_reference }}{% endif %} - {% if item.price and item.currency %}Price: {{ item.price }} {{ item.currency }}{% endif %} -
- {% endif %} -

-
-
- - {% endif %} - {% endfor %} - {% endfor %} - - {% if trip.flight_bookings %} -

Flight bookings

- {% for item in trip.flight_bookings %} -
- {{ item.flights | map(attribute="airline_name") | unique | join(" + ") }} - {% if g.user.is_authenticated and item.booking_reference %} - booking reference: {{ item.booking_reference }} - {% endif %} - {% if g.user.is_authenticated and item.price and item.currency %} - price: {{ item.price }} {{ item.currency }} - {% endif %} -
- {% endfor %} - {% endif %} - - {% if trip.events %} -

Events

- {% for item in trip.events %} - {% set country = get_country(item.country) if item.country else None %} -
-
-
- {{ item.title }} - {{ display_date_no_year(item.date) }} -
-

- Address: {{ item.address }} - | Location: {{ item.location }} - {% if country %} - {{ country.flag if trip.show_flags }} - {% else %} - country code {{ item.country }} not found - {% endif %} - {% if g.user.is_authenticated and item.price and item.currency %} - | price: {{ item.price }} {{ item.currency }} - {% endif %} -

-
-
- {% endfor %} - {% endif %} - -
-

Holidays

- {% if holidays %} - - {% for item in holidays %} - {% set country = get_country(item.country) %} - - {% if loop.first or item.date != loop.previtem.date %} - - {% else %} - - {% endif %} - - - - {% endfor %} -
{{ display_date(item.date) }}{{ country.flag if trip.show_flags }} {{ country.name }}{{ item.display_name }}
- {% else %} -

No public holidays during trip.

- {% endif %} -
- -
-

UK school holidays (Bristol)

- {% if school_holidays %} - - {% for item in school_holidays %} - - - - - - {% endfor %} -
{{ display_date(item.as_date) }}to {{ display_date(item.end_as_date) }}{{ item.title }}
- {% else %} -

No UK school holidays during trip.

- {% endif %} -
- - {{ next_and_previous() }} - -
-
-{% if coordinates or routes %} -
- -
-
-
-{% endif %} -
-{% endblock %} - -{% block scripts %} -{% if coordinates or routes %} - - - - - - -{% endif %} -{% endblock %} diff --git a/templates/weekends.html b/templates/weekends.html deleted file mode 100644 index 665d2c9..0000000 --- a/templates/weekends.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "base.html" %} -{% block title %}Weekends - Edward Betts{% endblock %} - -{% block content %} -
- -

Weekends

- - - - - - - - - - - - - - - - {% set current_isocalendar = today.isocalendar() %} - - {% for weekend in items %} - {% set week_number = weekend.date.isocalendar().week %} - {% set iso_week_year = weekend.date.isocalendar().year %} - {% if week_number == current_week_number and iso_week_year == current_isocalendar.year %} - {% set extra_class = " bg-warning-subtle" %} - {% else %} - {% set extra_class = "" %} - {% endif %} - - - - {% for day in "saturday", "sunday" %} - {% if extra_class %} - {% if extra_class %} - {% if extra_class %} - {% endfor %} - - {% endfor %} - -
WeekDateSaturdaySaturday LocationSaturday WeatherSundaySunday LocationSunday Weather
- {{ week_number }} - - {{ weekend.date.strftime("%-d %b %Y") }} - {% else %}{% endif %} - {% if weekend[day] %} - {% for event in weekend[day] %} - {{ event.title }}{% if not loop.last %},{%endif %} - {% endfor %} - {% else %} - free - {% endif %} - {% else %}{% endif %} - {% set city, country = weekend[day + '_location'] %} - {% if city %} - {{ city }}, {{ country.flag }} {{ country.name }} - {% endif %} - {% else %}{% endif %} - {% set w = weekend[day + '_weather'] %} - {% if w %} - {{ w.status }} - {{ w.temp_min }}–{{ w.temp_max }}°C - {% endif %} -
-
- -{% endblock %} - diff --git a/tests/test_accommodation.py b/tests/test_accommodation.py deleted file mode 100644 index 451cbc6..0000000 --- a/tests/test_accommodation.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Tests for accommodation functionality.""" - -import tempfile -from datetime import date, datetime -from typing import Any - -import pytest -import yaml - -from agenda.accommodation import get_events -from agenda.event import Event - - -class TestGetEvents: - """Test the get_events function.""" - - def test_get_events_airbnb(self) -> None: - """Test getting accommodation events for Airbnb.""" - accommodation_data = [ - { - "from": date(2024, 6, 1), - "to": date(2024, 6, 5), - "location": "Paris", - "operator": "airbnb", - "url": "https://airbnb.com/rooms/123" - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(accommodation_data, f) - filepath = f.name - - try: - events = get_events(filepath) - - assert len(events) == 1 - event = events[0] - assert event.date == date(2024, 6, 1) - assert event.end_date == date(2024, 6, 5) - assert event.name == "accommodation" - assert event.title == "Paris Airbnb" - assert event.url == "https://airbnb.com/rooms/123" - - finally: - import os - os.unlink(filepath) - - def test_get_events_hotel(self) -> None: - """Test getting accommodation events for hotel.""" - accommodation_data = [ - { - "from": date(2024, 6, 1), - "to": date(2024, 6, 5), - "name": "Hilton Hotel", - "url": "https://hilton.com" - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(accommodation_data, f) - filepath = f.name - - try: - events = get_events(filepath) - - assert len(events) == 1 - event = events[0] - assert event.date == date(2024, 6, 1) - assert event.end_date == date(2024, 6, 5) - assert event.name == "accommodation" - assert event.title == "Hilton Hotel" - assert event.url == "https://hilton.com" - - finally: - import os - os.unlink(filepath) - - def test_get_events_no_url(self) -> None: - """Test getting accommodation events without URL.""" - accommodation_data = [ - { - "from": date(2024, 6, 1), - "to": date(2024, 6, 5), - "name": "Local B&B" - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(accommodation_data, f) - filepath = f.name - - try: - events = get_events(filepath) - - assert len(events) == 1 - event = events[0] - assert event.url is None - assert event.title == "Local B&B" - - finally: - import os - os.unlink(filepath) - - def test_get_events_multiple_accommodations(self) -> None: - """Test getting multiple accommodation events.""" - accommodation_data = [ - { - "from": date(2024, 6, 1), - "to": date(2024, 6, 5), - "location": "London", - "operator": "airbnb" - }, - { - "from": date(2024, 6, 10), - "to": date(2024, 6, 15), - "name": "Royal Hotel" - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(accommodation_data, f) - filepath = f.name - - try: - events = get_events(filepath) - - assert len(events) == 2 - - # Check first accommodation (Airbnb) - assert events[0].title == "London Airbnb" - assert events[0].date == date(2024, 6, 1) - - # Check second accommodation (Hotel) - assert events[1].title == "Royal Hotel" - assert events[1].date == date(2024, 6, 10) - - finally: - import os - os.unlink(filepath) - - def test_get_events_empty_file(self) -> None: - """Test getting events from empty file.""" - accommodation_data: list[dict[str, Any]] = [] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(accommodation_data, f) - filepath = f.name - - try: - events = get_events(filepath) - assert events == [] - - finally: - import os - os.unlink(filepath) - - def test_get_events_file_not_found(self) -> None: - """Test error handling when file doesn't exist.""" - with pytest.raises(FileNotFoundError): - get_events("/nonexistent/file.yaml") - - def test_get_events_datetime_objects(self) -> None: - """Test with datetime objects instead of dates.""" - accommodation_data = [ - { - "from": datetime(2024, 6, 1, 15, 0), - "to": datetime(2024, 6, 5, 11, 0), - "name": "Hotel Example" - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(accommodation_data, f) - filepath = f.name - - try: - events = get_events(filepath) - - assert len(events) == 1 - event = events[0] - assert event.date == datetime(2024, 6, 1, 15, 0) - assert event.end_date == datetime(2024, 6, 5, 11, 0) - - finally: - import os - os.unlink(filepath) \ No newline at end of file diff --git a/tests/test_add_new_conference.py b/tests/test_add_new_conference.py deleted file mode 100644 index 8038724..0000000 --- a/tests/test_add_new_conference.py +++ /dev/null @@ -1,486 +0,0 @@ -"""Tests for agenda.add_new_conference.""" - -from datetime import date, datetime -import typing - -import lxml.html -import pytest -import yaml - -from agenda import add_new_conference -from agenda.conference import ConferenceSeries - - -def test_parse_osm_url_mlat_mlon() -> None: - """OpenStreetMap URLs with mlat/mlon should parse.""" - result = add_new_conference.parse_osm_url( - "https://www.openstreetmap.org/?mlat=51.5&mlon=-0.12" - ) - assert result == (51.5, -0.12) - - -def test_extract_google_maps_latlon_at_pattern() -> None: - """Google Maps @lat,lon URLs should parse.""" - result = add_new_conference.extract_google_maps_latlon( - "https://www.google.com/maps/place/Venue/@51.5242464,-0.0997024,17z/" - ) - assert result == (51.5242464, -0.0997024) - - -def test_url_has_year_component() -> None: - """Only actual year or edition components should count as year-specific.""" - cases = [ - ("https://www.foss4gna.org/", False), - ("https://foss4g.asia/2026/", True), - ("https://2027.fossy.ca/", True), - ("https://www.socallinuxexpo.org/scale/24x/", True), - ("https://2026.stateofthebrowser.com/", True), - ] - for url, expected in cases: - assert add_new_conference.url_has_year_component(url) is expected - - -def test_insert_sorted_allows_same_url_different_year_without_year_component() -> None: - """The same non-year-specific URL can be reused for a different year.""" - conferences: list[dict[str, typing.Any]] = [ - { - "name": "OldConf", - "start": date(2025, 6, 1), - "url": "https://example.com/conf", - } - ] - new_conf: dict[str, typing.Any] = { - "name": "NewConf", - "start": date(2026, 6, 1), - "url": "https://example.com/conf", - } - - updated = add_new_conference.insert_sorted(conferences, new_conf) - - assert len(updated) == 2 - assert updated[1]["name"] == "NewConf" - - -def test_insert_sorted_supports_nested_dates() -> None: - """Nested dates should be used for sorting.""" - conferences: list[dict[str, typing.Any]] = [ - { - "name": "PyCascades", - "dates": { - "status": "approximate", - "label": "March 2027", - "earliest": date(2027, 3, 1), - "latest": date(2027, 3, 31), - }, - } - ] - new_conf: dict[str, typing.Any] = { - "name": "FOSDEM", - "dates": { - "status": "tentative", - "start": date(2027, 1, 30), - "end": date(2027, 1, 31), - }, - } - - updated = add_new_conference.insert_sorted(conferences, new_conf) - - assert [conf["name"] for conf in updated] == ["FOSDEM", "PyCascades"] - - -def test_insert_sorted_updates_inexact_existing_entry() -> None: - """Exact dates should replace an existing inexact series entry.""" - conferences: list[dict[str, typing.Any]] = [ - { - "name": "PyCascades", - "series": "pycascades", - "topic": "Python", - "location": "Seattle, Washington", - "dates": { - "status": "approximate", - "label": "March 2027", - "earliest": date(2027, 3, 1), - "latest": date(2027, 3, 31), - }, - "url": "https://2027.pycascades.com/", - } - ] - new_conf: dict[str, typing.Any] = { - "name": "PyCascades", - "series": "pycascades", - "topic": "Python", - "location": "Seattle, Washington", - "dates": { - "status": "exact", - "start": date(2027, 3, 12), - "end": date(2027, 3, 14), - }, - "url": "https://2027.pycascades.com/", - "venue": "Example Hall", - } - - updated = add_new_conference.insert_sorted(conferences, new_conf) - - assert len(updated) == 1 - assert updated[0]["dates"]["status"] == "exact" - assert updated[0]["dates"]["start"] == date(2027, 3, 12) - assert updated[0]["venue"] == "Example Hall" - - -def test_normalize_dates_field_moves_legacy_dates() -> None: - """Legacy start/end model output should be converted before writing YAML.""" - conf: dict[str, typing.Any] = { - "name": "PyCon", - "start": date(2026, 4, 10), - "end": date(2026, 4, 12), - } - - add_new_conference.normalize_dates_field(conf) - - assert "start" not in conf - assert "end" not in conf - assert conf["dates"] == { - "status": "exact", - "start": date(2026, 4, 10), - "end": date(2026, 4, 12), - } - - -def test_normalize_dates_field_parses_quoted_dates() -> None: - """Quoted ISO dates from generated YAML should become date objects.""" - conf: dict[str, typing.Any] = { - "name": "Git Merge", - "dates": { - "status": "exact", - "start": "2026-09-16", - "end": "2026-09-17", - }, - } - - add_new_conference.normalize_dates_field(conf) - - assert conf["dates"]["start"] == date(2026, 9, 16) - assert conf["dates"]["end"] == date(2026, 9, 17) - - -def test_validate_generated_conference_reports_missing_dates() -> None: - """Missing generated dates should raise a clear importer error.""" - conf: dict[str, typing.Any] = { - "name": "Git Merge", - "topic": "Git", - "location": "TBC", - } - - with pytest.raises(ValueError, match="missing valid date information"): - add_new_conference.validate_generated_conference(conf) - - -def test_webpage_to_text_includes_hidden_event_metadata() -> None: - """Schema.org event metadata should be included even when visually hidden.""" - root = lxml.html.fromstring(""" - -
- - -
- - """) - - text = add_new_conference.webpage_to_text(root) - - assert "- name: State of the Map Asia 2026 OSAKA" in text - assert "- startDate: 2026-09-06T18:30" in text - - -def test_apply_event_metadata_dates_fills_missing_generated_dates() -> None: - """Structured page dates should recover incomplete model output.""" - root = lxml.html.fromstring(""" -
- -
- """) - conf: dict[str, typing.Any] = {"name": "State of the Map Asia 2026"} - - add_new_conference.apply_event_metadata_dates(conf, root) - - assert conf["dates"] == { - "status": "exact", - "start": datetime(2026, 9, 6, 18, 30), - "end": datetime(2026, 9, 6, 18, 30), - } - - -def test_build_prompt_includes_nested_dates_and_series() -> None: - """The prompt should describe nested dates and known series IDs.""" - prompt = add_new_conference.build_prompt( - "https://example.com", - "Conference details", - None, - { - "pycascades": { - "name": "PyCascades", - "topic": "Python", - "usual_location": "Seattle, Washington", - "country": "us", - } - }, - ) - - assert "Do not output legacy top-level `start`, `end`, or `date_status`" in prompt - assert "dates.status" in prompt - assert "- pycascades: PyCascades" in prompt - assert "March 2027" in prompt - assert "For an address written in a non-Latin script" in prompt - assert "If no reliable rendering is available, omit" in prompt - assert "When `free: true`, omit `price` and `currency`" in prompt - - -def test_validate_country_normalises_name() -> None: - """Country names should be normalised to alpha-2 codes.""" - conf: dict[str, typing.Any] = {"country": "United Kingdom"} - - add_new_conference.validate_country(conf) - - assert conf["country"] == "gb" - - -def test_validate_series_rejects_unknown_generated_id() -> None: - """Generated series IDs must exist in conference_series.yaml.""" - conf: dict[str, typing.Any] = {"series": "state-of-the-map-asia"} - - with pytest.raises(ValueError, match="unknown series 'state-of-the-map-asia'"): - add_new_conference.validate_series( - conf, - {"state-of-the-map": {"name": "State of the Map"}}, - ) - - -def test_validate_series_accepts_known_id_or_missing_field() -> None: - """Known series IDs and conferences without a series should pass.""" - series: dict[str, ConferenceSeries] = { - "state-of-the-map": {"name": "State of the Map"} - } - - add_new_conference.validate_series({"series": "state-of-the-map"}, series) - add_new_conference.validate_series({}, series) - - -def test_normalize_free_event_fields_removes_price_and_currency() -> None: - """Free events should not retain redundant pricing fields.""" - conf: dict[str, typing.Any] = { - "free": True, - "price": 0, - "currency": "JPY", - } - - add_new_conference.normalize_free_event_fields(conf) - - assert conf == {"free": True} - - -def test_normalize_free_event_fields_keeps_paid_event_pricing() -> None: - """Pricing fields should remain for events that are not marked free.""" - conf: dict[str, typing.Any] = { - "free": False, - "price": 25, - "currency": "GBP", - } - - add_new_conference.normalize_free_event_fields(conf) - - assert conf == {"free": False, "price": 25, "currency": "GBP"} - - -def test_normalise_end_field_defaults_single_day_date() -> None: - """Non-Geomob conferences should default end to the start date.""" - conf: dict[str, typing.Any] = { - "name": "PyCon", - "start": date(2026, 4, 10), - } - - add_new_conference.normalise_end_field(conf, "plain text") - - assert conf["end"] == date(2026, 4, 10) - - -def test_normalise_end_field_defaults_nested_exact_date() -> None: - """Nested exact dates should get a default end date.""" - conf: dict[str, typing.Any] = { - "name": "PyCon", - "dates": { - "status": "exact", - "start": date(2026, 4, 10), - }, - } - - add_new_conference.normalise_end_field(conf, "plain text") - - assert conf["dates"]["end"] == date(2026, 4, 10) - - -def test_normalise_end_field_sets_geomob_end_time() -> None: - """Geomob conferences should default to a 22:00 end time.""" - conf: dict[str, typing.Any] = { - "name": "Geomob London", - "start": date(2026, 1, 28), - "url": "https://thegeomob.com/post/jan-28th-2026-geomoblon-details", - } - - add_new_conference.normalise_end_field(conf, "see you there") - - assert conf["end"] == datetime(2026, 1, 28, 22, 0) - - -def test_detect_page_coordinates_uses_first_supported_link() -> None: - """Page coordinate detection should inspect anchor hrefs.""" - root = lxml.html.fromstring( - ( - "" - 'Example' - 'Map' - "" - ) - ) - - assert add_new_conference.detect_page_coordinates(root) == (51.5, -0.12) - - -def test_add_new_conference_updates_yaml( - tmp_path: typing.Any, monkeypatch: pytest.MonkeyPatch -) -> None: - """The end-to-end import flow should append a generated conference.""" - yaml_path = tmp_path / "conferences.yaml" - yaml_path.write_text( - yaml.dump( - [ - { - "name": "ExistingConf", - "start": date(2026, 4, 1), - "end": date(2026, 4, 2), - "url": "https://example.com/existing", - } - ], - sort_keys=False, - ) - ) - - root = lxml.html.fromstring( - ( - "" - 'Map' - "" - ) - ) - - monkeypatch.setattr(add_new_conference, "fetch_webpage", lambda url: root) - monkeypatch.setattr( - add_new_conference, - "webpage_to_text", - lambda parsed: "Conference details", - ) - monkeypatch.setattr( - add_new_conference, - "get_from_open_ai", - lambda prompt: { - "yaml": yaml.dump( - { - "name": "NewConf", - "topic": "Tech", - "location": "New York", - "country": "United States", - "start": date(2026, 5, 3), - "url": "https://example.com/newconf", - }, - sort_keys=False, - ) - }, - ) - - added = add_new_conference.add_new_conference( - "https://example.com/newconf", str(yaml_path) - ) - - assert added is True - written = yaml.safe_load(yaml_path.read_text()) - assert len(written) == 2 - assert written[1]["name"] == "NewConf" - assert written[1]["country"] == "us" - assert written[1]["dates"] == { - "status": "exact", - "start": date(2026, 5, 3), - "end": date(2026, 5, 3), - } - assert written[1]["latitude"] == 40.0 - assert written[1]["longitude"] == -74.0 - - -def test_add_new_conference_reuses_generic_url_for_new_year( - tmp_path: typing.Any, monkeypatch: pytest.MonkeyPatch -) -> None: - """Generic URLs with digits in the domain should not be skipped early.""" - yaml_path = tmp_path / "conferences.yaml" - (tmp_path / "conference_series.yaml").write_text( - yaml.dump( - {"foss4g-north-america": {"name": "FOSS4G North America"}}, - sort_keys=False, - ) - ) - yaml_path.write_text( - yaml.dump( - [ - { - "name": "FOSS4G North America", - "series": "foss4g-north-america", - "dates": { - "status": "exact", - "start": date(2025, 11, 3), - "end": date(2025, 11, 5), - }, - "url": "https://www.foss4gna.org/", - } - ], - sort_keys=False, - ) - ) - - root = lxml.html.fromstring("Conference details") - monkeypatch.setattr(add_new_conference, "fetch_webpage", lambda url: root) - monkeypatch.setattr( - add_new_conference, - "webpage_to_text", - lambda parsed: "FOSS4G North America 2026", - ) - monkeypatch.setattr( - add_new_conference, "detect_page_coordinates", lambda parsed: None - ) - monkeypatch.setattr( - add_new_conference, - "get_from_open_ai", - lambda prompt: { - "yaml": yaml.dump( - { - "name": "FOSS4G North America", - "series": "foss4g-north-america", - "topic": "Geospatial", - "location": "St. Louis, Missouri", - "country": "us", - "dates": { - "status": "exact", - "start": date(2026, 10, 26), - "end": date(2026, 10, 29), - }, - "url": "https://www.foss4gna.org/", - }, - sort_keys=False, - ) - }, - ) - - added = add_new_conference.add_new_conference( - "https://www.foss4gna.org/", str(yaml_path) - ) - - assert added is True - written = yaml.safe_load(yaml_path.read_text()) - assert len(written) == 2 - assert [conf["dates"]["start"].year for conf in written] == [2025, 2026] diff --git a/tests/test_agenda.py b/tests/test_agenda.py index 78484cb..fd5ce5d 100644 --- a/tests/test_agenda.py +++ b/tests/test_agenda.py @@ -1,149 +1,80 @@ -"""Tests for agenda.""" - import datetime -import json from decimal import Decimal -from unittest.mock import patch import pytest -from agenda import format_list_with_ampersand, get_country, uk_time -from agenda.data import event_sort_datetime, timezone_transition -from agenda.economist import publication_dates -from agenda.event import Event -from agenda.fx import get_gbpusd -from agenda.holidays import get_all -from agenda.uk_holiday import bank_holiday_list, get_mothers_day -from agenda.utils import timedelta_display +from agenda import ( + get_gbpusd, + get_next_bank_holiday, + get_next_timezone_transition, + next_economist, + next_uk_fathers_day, + next_uk_mothers_day, + timedelta_display, + uk_financial_year_end, +) -@pytest.fixture # type: ignore[untyped-decorator] -def mock_today() -> datetime.date: - """Mock the current date for testing purposes.""" +@pytest.fixture +def mock_today(): + # Mock the current date for testing purposes return datetime.date(2023, 10, 5) -@pytest.fixture # type: ignore[untyped-decorator] -def mock_now() -> datetime.datetime: - """Mock the current date and time for testing purposes.""" +@pytest.fixture +def mock_now(): + # Mock the current date and time for testing purposes return datetime.datetime(2023, 10, 5, 12, 0, 0) -def test_get_mothers_day(mock_today: datetime.date) -> None: - """Test get_mothers_day function.""" - mothers_day = get_mothers_day(mock_today) - # UK Mother's Day 2024 is April 21st (3 weeks after Easter) - assert mothers_day == datetime.date(2024, 4, 21) +def test_next_uk_mothers_day(mock_today): + # Test next_uk_mothers_day function + next_mothers_day = next_uk_mothers_day(mock_today) + assert next_mothers_day == datetime.date(2024, 4, 21) -def test_timezone_transition(mock_now: datetime.datetime) -> None: - """Test timezone_transition function.""" - start = datetime.datetime(2023, 10, 1) - end = datetime.datetime(2023, 11, 1) - transitions = timezone_transition(start, end, "uk_clock_change", "Europe/London") - assert len(transitions) == 1 - assert transitions[0].name == "uk_clock_change" - assert transitions[0].date.date() == datetime.date(2023, 10, 29) +def test_next_uk_fathers_day(mock_today): + # Test next_uk_fathers_day function + next_fathers_day = next_uk_fathers_day(mock_today) + assert next_fathers_day == datetime.date(2024, 6, 21) -def test_event_sort_datetime_handles_mixed_timezone_awareness() -> None: - """Event sort keys should be comparable for date, naive, and aware datetimes.""" - events = [ - Event( - name="aware", - date=datetime.datetime( - 2026, 1, 1, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=1)) - ), - ), - Event(name="date", date=datetime.date(2026, 1, 1)), - Event(name="naive", date=datetime.datetime(2026, 1, 1, 8, 30)), - ] - - sorted_events = sorted(events, key=lambda e: (event_sort_datetime(e), e.name)) - - assert [event.name for event in sorted_events] == ["date", "aware", "naive"] +def test_get_next_timezone_transition(mock_now) -> None: + # Test get_next_timezone_transition function + next_transition = get_next_timezone_transition(mock_now, "Europe/London") + assert next_transition == datetime.date(2023, 10, 29) -def test_get_gbpusd_function_exists() -> None: - """Test that get_gbpusd function exists and is callable.""" - # Simple test to verify the function exists and has correct signature - from inspect import signature - - sig = signature(get_gbpusd) - assert len(sig.parameters) == 1 - assert "config" in sig.parameters - assert sig.return_annotation == Decimal +def test_get_next_bank_holiday(mock_today) -> None: + # Test get_next_bank_holiday function + next_holiday = get_next_bank_holiday(mock_today)[0] + assert next_holiday.date == datetime.date(2023, 12, 25) + assert next_holiday.title == "Christmas Day" -def test_publication_dates(mock_today: datetime.date) -> None: - """Test publication_dates function.""" - start_date = mock_today - end_date = mock_today + datetime.timedelta(days=30) - publications = publication_dates(start_date, end_date) - assert len(publications) >= 0 # Should return some publications - if publications: - assert all(pub.name == "economist" for pub in publications) +def test_get_gbpusd(mock_now): + # Test get_gbpusd function + gbpusd = get_gbpusd() + assert isinstance(gbpusd, Decimal) + # You can add more assertions based on your specific use case. -def test_timedelta_display() -> None: - """Test timedelta_display function.""" +def test_next_economist(mock_today): + # Test next_economist function + next_publication = next_economist(mock_today) + assert next_publication == datetime.date(2023, 10, 5) + + +def test_uk_financial_year_end(): + # Test uk_financial_year_end function + financial_year_end = uk_financial_year_end(datetime.date(2023, 4, 1)) + assert financial_year_end == datetime.date(2023, 4, 5) + + +def test_timedelta_display(): + # Test timedelta_display function delta = datetime.timedelta(days=2, hours=5, minutes=30) display = timedelta_display(delta) - assert display == "2 days 5 hrs 30 mins" + assert display == " 2 days 5 hrs 30 mins" -def test_format_list_with_ampersand() -> None: - """Test format_list_with_ampersand function.""" - # Test with multiple items - items = ["apple", "banana", "cherry"] - result = format_list_with_ampersand(items) - assert result == "apple, banana & cherry" - - # Test with two items - items = ["apple", "banana"] - result = format_list_with_ampersand(items) - assert result == "apple & banana" - - # Test with single item - items = ["apple"] - result = format_list_with_ampersand(items) - assert result == "apple" - - # Test with empty list - items = [] - result = format_list_with_ampersand(items) - assert result == "" - - -def test_get_country() -> None: - """Test get_country function.""" - # Test with valid alpha-2 code - country = get_country("US") - assert country is not None - assert country.name == "United States" - - # Test with valid alpha-3 code - country = get_country("GBR") - assert country is not None - assert country.name == "United Kingdom" - - # Test with None - country = get_country(None) - assert country is None - - # Test with Kosovo special case - country = get_country("xk") - assert country is not None - assert country.name == "Kosovo" - - -def test_uk_time() -> None: - """Test uk_time function.""" - test_date = datetime.date(2023, 7, 15) # Summer time - test_time = datetime.time(14, 30, 0) - - result = uk_time(test_date, test_time) - - assert isinstance(result, datetime.datetime) - assert result.date() == test_date - assert result.time() == test_time - assert result.tzinfo is not None +# You can add more test cases for other functions as needed. diff --git a/tests/test_airbnb.py b/tests/test_airbnb.py deleted file mode 100644 index 7036cbf..0000000 --- a/tests/test_airbnb.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Tests for agenda.airbnb module.""" - -import pytest -from datetime import datetime -from zoneinfo import ZoneInfo -from unittest.mock import Mock, patch, mock_open - -from agenda.airbnb import ( - build_datetime, - list_to_dict, - extract_country_code, - walk_tree, - get_ui_state, - get_reservation_data, - get_price_from_reservation, - parse_multiple_files, -) - - -class TestBuildDatetime: - def test_build_datetime_utc(self): - result = build_datetime("2025-07-28", "15:30", "UTC") - expected = datetime(2025, 7, 28, 15, 30, tzinfo=ZoneInfo("UTC")) - assert result == expected - - def test_build_datetime_local_timezone(self): - result = build_datetime("2025-12-25", "09:00", "Europe/London") - expected = datetime(2025, 12, 25, 9, 0, tzinfo=ZoneInfo("Europe/London")) - assert result == expected - - -class TestListToDict: - def test_list_to_dict_even_items(self): - items = ["key1", "value1", "key2", "value2"] - result = list_to_dict(items) - expected = {"key1": "value1", "key2": "value2"} - assert result == expected - - def test_list_to_dict_empty_list(self): - result = list_to_dict([]) - assert result == {} - - def test_list_to_dict_single_pair(self): - items = ["name", "John"] - result = list_to_dict(items) - assert result == {"name": "John"} - - -class TestExtractCountryCode: - def test_extract_country_code_uk(self): - address = "123 Main Street, London, United Kingdom" - result = extract_country_code(address) - assert result == "gb" - - def test_extract_country_code_france(self): - address = "456 Rue de la Paix, Paris, France" - result = extract_country_code(address) - assert result == "fr" - - def test_extract_country_code_usa(self): - address = "789 Broadway, New York, United States" - result = extract_country_code(address) - assert result == "us" - - def test_extract_country_code_not_found(self): - address = "123 Unknown Street, Mystery City" - result = extract_country_code(address) - assert result is None - - def test_extract_country_code_case_insensitive(self): - address = "123 Main Street, UNITED KINGDOM" - result = extract_country_code(address) - assert result == "gb" - - -class TestWalkTree: - def test_walk_tree_dict_found(self): - data = {"level1": {"level2": {"target": "found"}}} - result = walk_tree(data, "target") - assert result == "found" - - def test_walk_tree_dict_not_found(self): - data = {"level1": {"level2": {"other": "value"}}} - result = walk_tree(data, "target") - assert result is None - - def test_walk_tree_list_found(self): - data = [{"other": "value"}, {"target": "found"}] - result = walk_tree(data, "target") - assert result == "found" - - def test_walk_tree_nested_list_dict(self): - data = [{"level1": [{"target": "found"}]}] - result = walk_tree(data, "target") - assert result == "found" - - def test_walk_tree_empty_data(self): - result = walk_tree({}, "target") - assert result is None - - -class TestGetPriceFromReservation: - def test_get_price_from_reservation_valid(self): - reservation = { - "payment_summary": {"subtitle": "Total cost: £150.00"} - } - result = get_price_from_reservation(reservation) - assert result == "150.00" - - def test_get_price_from_reservation_different_amount(self): - reservation = { - "payment_summary": {"subtitle": "Total cost: £89.99"} - } - result = get_price_from_reservation(reservation) - assert result == "89.99" - - -class TestParseMultipleFiles: - @patch('agenda.airbnb.extract_booking_from_html') - def test_parse_multiple_files_single_file(self, mock_extract): - mock_booking = { - "type": "apartment", - "operator": "airbnb", - "name": "Test Apartment", - "booking_reference": "ABC123" - } - mock_extract.return_value = mock_booking - - result = parse_multiple_files(["test1.html"]) - - assert len(result) == 1 - assert result[0] == mock_booking - mock_extract.assert_called_once_with("test1.html") - - @patch('agenda.airbnb.extract_booking_from_html') - def test_parse_multiple_files_multiple_files(self, mock_extract): - mock_booking1 = {"booking_reference": "ABC123"} - mock_booking2 = {"booking_reference": "DEF456"} - mock_extract.side_effect = [mock_booking1, mock_booking2] - - result = parse_multiple_files(["test2.html", "test1.html"]) - - assert len(result) == 2 - assert result[0] == mock_booking1 - assert result[1] == mock_booking2 - - @patch('agenda.airbnb.extract_booking_from_html') - def test_parse_multiple_files_empty_list(self, mock_extract): - result = parse_multiple_files([]) - assert result == [] - mock_extract.assert_not_called() - - -class TestGetUiState: - @patch('lxml.html.etree') - def test_get_ui_state_with_mock_tree(self, mock_etree): - mock_tree = Mock() - mock_tree.xpath.return_value = ['{"test": [["uiState", {"key": "value"}]]}'] - - with patch('agenda.airbnb.walk_tree') as mock_walk: - mock_walk.return_value = [["key", "value"]] - result = get_ui_state(mock_tree) - - assert result == {"key": "value"} - mock_tree.xpath.assert_called_once_with('//*[@id="data-injector-instances"]/text()') - - -class TestGetReservationData: - def test_get_reservation_data(self): - ui_state = { - "reservation": { - "scheduled_event": { - "rows": [ - {"id": "row1", "data": "value1"}, - {"id": "row2", "data": "value2"} - ] - } - } - } - - result = get_reservation_data(ui_state) - expected = { - "row1": {"id": "row1", "data": "value1"}, - "row2": {"id": "row2", "data": "value2"} - } - assert result == expected \ No newline at end of file diff --git a/tests/test_birthday.py b/tests/test_birthday.py deleted file mode 100644 index c36a4e8..0000000 --- a/tests/test_birthday.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Tests for birthday functionality.""" - -import tempfile -from datetime import date -from typing import Any - -import pytest -import yaml - -from agenda.birthday import YEAR_NOT_KNOWN, get_birthdays, next_birthday -from agenda.event import Event - - -class TestNextBirthday: - """Test the next_birthday function.""" - - def test_birthday_this_year_future(self) -> None: - """Test birthday that hasn't occurred this year.""" - from_date = date(2024, 3, 15) - birth_date = date(1990, 6, 20) - - next_bday, age = next_birthday(from_date, birth_date) - - assert next_bday == date(2024, 6, 20) - assert age == 34 - - def test_birthday_this_year_past(self) -> None: - """Test birthday that already occurred this year.""" - from_date = date(2024, 8, 15) - birth_date = date(1990, 6, 20) - - next_bday, age = next_birthday(from_date, birth_date) - - assert next_bday == date(2025, 6, 20) - assert age == 35 - - def test_birthday_today(self) -> None: - """Test when today is the birthday.""" - from_date = date(2024, 6, 20) - birth_date = date(1990, 6, 20) - - next_bday, age = next_birthday(from_date, birth_date) - - assert next_bday == date(2024, 6, 20) - assert age == 34 - - def test_birthday_unknown_year(self) -> None: - """Test birthday with unknown year.""" - from_date = date(2024, 3, 15) - birth_date = date(YEAR_NOT_KNOWN, 6, 20) - - next_bday, age = next_birthday(from_date, birth_date) - - assert next_bday == date(2024, 6, 20) - assert age is None - - def test_birthday_unknown_year_past(self) -> None: - """Test birthday with unknown year that already passed.""" - from_date = date(2024, 8, 15) - birth_date = date(YEAR_NOT_KNOWN, 6, 20) - - next_bday, age = next_birthday(from_date, birth_date) - - assert next_bday == date(2025, 6, 20) - assert age is None - - def test_leap_year_birthday(self) -> None: - """Test birthday on leap day.""" - from_date = date(2024, 1, 15) # 2024 is a leap year - birth_date = date(2000, 2, 29) # Born on leap day - - next_bday, age = next_birthday(from_date, birth_date) - - assert next_bday == date(2024, 2, 29) - assert age == 24 - - -class TestGetBirthdays: - """Test the get_birthdays function.""" - - def test_get_birthdays_with_year(self) -> None: - """Test getting birthdays with known birth years.""" - birthday_data = [ - { - "label": "John Doe", - "birthday": {"year": 1990, "month": 6, "day": 20} - }, - { - "label": "Jane Smith", - "birthday": {"year": 1985, "month": 12, "day": 15} - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(birthday_data, f) - filepath = f.name - - try: - from_date = date(2024, 3, 15) - events = get_birthdays(from_date, filepath) - - # Should have 4 events (2 people × 2 years each) - assert len(events) == 4 - - # Check John's birthdays - john_events = [e for e in events if "John Doe" in e.title] - assert len(john_events) == 2 - assert john_events[0].date == date(2024, 6, 20) - assert "aged 34" in john_events[0].title - assert john_events[1].date == date(2025, 6, 20) - assert "aged 35" in john_events[1].title - - # Check Jane's birthdays - jane_events = [e for e in events if "Jane Smith" in e.title] - assert len(jane_events) == 2 - assert jane_events[0].date == date(2024, 12, 15) - assert "aged 39" in jane_events[0].title - - # All events should be birthday events - for event in events: - assert event.name == "birthday" - assert isinstance(event, Event) - - finally: - import os - os.unlink(filepath) - - def test_get_birthdays_without_year(self) -> None: - """Test getting birthdays with unknown birth years.""" - birthday_data = [ - { - "label": "Anonymous Person", - "birthday": {"month": 6, "day": 20} - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(birthday_data, f) - filepath = f.name - - try: - from_date = date(2024, 3, 15) - events = get_birthdays(from_date, filepath) - - # Should have 2 events (1 person × 2 years) - assert len(events) == 2 - - # Check that age is unknown - for event in events: - assert "age unknown" in event.title - assert "Anonymous Person" in event.title - assert event.name == "birthday" - - finally: - import os - os.unlink(filepath) - - def test_get_birthdays_mixed_data(self) -> None: - """Test getting birthdays with mixed known/unknown years.""" - birthday_data = [ - { - "label": "Known Person", - "birthday": {"year": 1990, "month": 6, "day": 20} - }, - { - "label": "Unknown Person", - "birthday": {"month": 8, "day": 15} - } - ] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(birthday_data, f) - filepath = f.name - - try: - from_date = date(2024, 3, 15) - events = get_birthdays(from_date, filepath) - - # Should have 4 events total - assert len(events) == 4 - - # Check known person events - known_events = [e for e in events if "Known Person" in e.title] - assert len(known_events) == 2 - assert all("aged" in e.title and "age unknown" not in e.title for e in known_events) - - # Check unknown person events - unknown_events = [e for e in events if "Unknown Person" in e.title] - assert len(unknown_events) == 2 - assert all("age unknown" in e.title for e in unknown_events) - - finally: - import os - os.unlink(filepath) - - def test_get_birthdays_empty_file(self) -> None: - """Test getting birthdays from empty file.""" - birthday_data: list[dict[str, Any]] = [] - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(birthday_data, f) - filepath = f.name - - try: - from_date = date(2024, 3, 15) - events = get_birthdays(from_date, filepath) - - assert events == [] - - finally: - import os - os.unlink(filepath) - - def test_get_birthdays_file_not_found(self) -> None: - """Test error handling when file doesn't exist.""" - from_date = date(2024, 3, 15) - - with pytest.raises(FileNotFoundError): - get_birthdays(from_date, "/nonexistent/file.yaml") - - -class TestConstants: - """Test module constants.""" - - def test_year_not_known_constant(self) -> None: - """Test that YEAR_NOT_KNOWN has expected value.""" - assert YEAR_NOT_KNOWN == 1900 \ No newline at end of file diff --git a/tests/test_bristol_waste.py b/tests/test_bristol_waste.py deleted file mode 100644 index c0a35d1..0000000 --- a/tests/test_bristol_waste.py +++ /dev/null @@ -1,370 +0,0 @@ -"""Test Bristol waste collection module.""" - -import json -import os -import tempfile -from datetime import date, datetime, timedelta -from unittest.mock import AsyncMock, Mock, patch - -import httpx -import pytest -from agenda import bristol_waste -from agenda.event import Event - - -@pytest.fixture -def temp_data_dir(): - """Create a temporary directory for test data.""" - with tempfile.TemporaryDirectory() as tmpdir: - yield tmpdir - - -@pytest.fixture -def sample_bristol_data(): - """Sample Bristol waste collection data.""" - return [ - { - "containerName": "Recycling Container", - "collection": [ - { - "nextCollectionDate": "2024-07-15T00:00:00Z", - "lastCollectionDate": "2024-07-01T00:00:00Z", - } - ], - }, - { - "containerName": "General Waste Container", - "collection": [ - { - "nextCollectionDate": "2024-07-22T00:00:00Z", - "lastCollectionDate": "2024-07-08T00:00:00Z", - } - ], - }, - { - "containerName": "Food Waste Container", - "collection": [ - { - "nextCollectionDate": "2024-07-16T00:00:00Z", - "lastCollectionDate": "2024-07-02T00:00:00Z", - } - ], - }, - ] - - -@pytest.fixture -def mock_response(sample_bristol_data): - """Mock HTTP response for Bristol waste API.""" - response = Mock(spec=httpx.Response) - response.content = json.dumps({"data": sample_bristol_data}).encode() - response.json.return_value = {"data": sample_bristol_data} - return response - - -class TestGetService: - """Test get_service function.""" - - def test_recycling_container(self): - """Test extracting recycling service name.""" - item = {"containerName": "Recycling Container"} - result = bristol_waste.get_service(item) - assert result == "Recycling" - - def test_general_waste_container(self): - """Test extracting general waste service name.""" - item = {"containerName": "General Waste Container"} - result = bristol_waste.get_service(item) - assert result == "Waste Container" - - def test_food_waste_container(self): - """Test extracting food waste service name.""" - item = {"containerName": "Food Waste Container"} - result = bristol_waste.get_service(item) - assert result == "Waste Container" - - def test_garden_waste_container(self): - """Test extracting garden waste service name.""" - item = {"containerName": "Garden Waste Container"} - result = bristol_waste.get_service(item) - assert result == "Waste Container" - - -class TestCollections: - """Test collections function.""" - - def test_single_collection_dates(self): - """Test extracting dates from a single collection.""" - item = { - "collection": [ - { - "nextCollectionDate": "2024-07-15T00:00:00Z", - "lastCollectionDate": "2024-07-01T00:00:00Z", - } - ] - } - dates = list(bristol_waste.collections(item)) - expected = [date(2024, 7, 15), date(2024, 7, 1)] - assert dates == expected - - def test_multiple_collection_dates(self): - """Test extracting dates from multiple collections.""" - item = { - "collection": [ - { - "nextCollectionDate": "2024-07-15T00:00:00Z", - "lastCollectionDate": "2024-07-01T00:00:00Z", - }, - { - "nextCollectionDate": "2024-07-22T00:00:00Z", - "lastCollectionDate": "2024-07-08T00:00:00Z", - }, - ] - } - dates = list(bristol_waste.collections(item)) - expected = [ - date(2024, 7, 15), - date(2024, 7, 1), - date(2024, 7, 22), - date(2024, 7, 8), - ] - assert dates == expected - - def test_empty_collection(self): - """Test extracting dates from empty collection.""" - item = {"collection": []} - dates = list(bristol_waste.collections(item)) - assert dates == [] - - -class TestGetWebData: - """Test get_web_data function.""" - - @pytest.mark.asyncio - async def test_get_web_data_success(self, mock_response): - """Test successful web data retrieval.""" - uprn = "123456789012" - - with patch("httpx.AsyncClient") as mock_client: - mock_async_client = AsyncMock() - mock_client.return_value.__aenter__.return_value = mock_async_client - mock_async_client.get.return_value = mock_response - mock_async_client.post.return_value = mock_response - - result = await bristol_waste.get_web_data(uprn) - - assert result == mock_response - assert mock_async_client.get.call_count == 1 - assert mock_async_client.post.call_count == 2 - - @pytest.mark.asyncio - async def test_get_web_data_zero_padded_uprn(self, mock_response): - """Test UPRN is zero-padded to 12 digits.""" - uprn = "123456" - - with patch("httpx.AsyncClient") as mock_client: - mock_async_client = AsyncMock() - mock_client.return_value.__aenter__.return_value = mock_async_client - mock_async_client.get.return_value = mock_response - mock_async_client.post.return_value = mock_response - - await bristol_waste.get_web_data(uprn) - - # Check that the UPRN was zero-padded in the requests - calls = mock_async_client.post.call_args_list - assert calls[0][1]["json"] == {"Uprn": "UPRN000000123456"} - assert calls[1][1]["json"] == {"uprn": "000000123456"} - - -class TestGetData: - """Test get_data function.""" - - @pytest.mark.asyncio - async def test_get_data_force_cache(self, temp_data_dir, sample_bristol_data): - """Test using forced cache.""" - uprn = "123456789012" - os.makedirs(os.path.join(temp_data_dir, "waste")) - - # Create a cached file - cache_file = os.path.join( - temp_data_dir, "waste", f"2024-07-01_12:00_{uprn}.json" - ) - with open(cache_file, "w") as f: - json.dump({"data": sample_bristol_data}, f) - - result = await bristol_waste.get_data(temp_data_dir, uprn, "force") - assert result == sample_bristol_data - - @pytest.mark.asyncio - async def test_get_data_refresh_cache( - self, temp_data_dir, sample_bristol_data, mock_response - ): - """Test refreshing cache.""" - uprn = "123456789012" - - with patch("agenda.bristol_waste.get_web_data", return_value=mock_response): - with patch("agenda.bristol_waste.datetime") as mock_datetime: - mock_now = datetime(2024, 7, 15, 14, 30) - mock_datetime.now.return_value = mock_now - mock_datetime.strptime = datetime.strptime - - result = await bristol_waste.get_data(temp_data_dir, uprn, "refresh") - - assert result == sample_bristol_data - # Check that cache file was created - waste_dir = os.path.join(temp_data_dir, "waste") - cache_files = [ - f for f in os.listdir(waste_dir) if f.endswith(f"_{uprn}.json") - ] - assert len(cache_files) == 1 - assert cache_files[0] == f"2024-07-15_14:30_{uprn}.json" - - @pytest.mark.asyncio - async def test_get_data_recent_cache(self, temp_data_dir, sample_bristol_data): - """Test using recent cache within TTL.""" - uprn = "123456789012" - os.makedirs(os.path.join(temp_data_dir, "waste")) - - # Create a recent cached file (within TTL) - recent_time = datetime.now() - timedelta(hours=6) - cache_file = os.path.join( - temp_data_dir, - "waste", - f"{recent_time.strftime('%Y-%m-%d_%H:%M')}_{uprn}.json", - ) - with open(cache_file, "w") as f: - json.dump({"data": sample_bristol_data}, f) - - result = await bristol_waste.get_data(temp_data_dir, uprn, "auto") - assert result == sample_bristol_data - - @pytest.mark.asyncio - async def test_get_data_expired_cache( - self, temp_data_dir, sample_bristol_data, mock_response - ): - """Test with expired cache, should fetch new data.""" - uprn = "123456789012" - os.makedirs(os.path.join(temp_data_dir, "waste")) - - # Create an old cached file (beyond TTL) - old_time = datetime.now() - timedelta(hours=25) - cache_file = os.path.join( - temp_data_dir, "waste", f"{old_time.strftime('%Y-%m-%d_%H:%M')}_{uprn}.json" - ) - with open(cache_file, "w") as f: - json.dump({"data": [{"old": "data"}]}, f) - - with patch("agenda.bristol_waste.get_web_data", return_value=mock_response): - result = await bristol_waste.get_data(temp_data_dir, uprn, "auto") - - assert result == sample_bristol_data - - @pytest.mark.asyncio - async def test_get_data_timeout_fallback(self, temp_data_dir, sample_bristol_data): - """Test fallback to cache when web request times out.""" - uprn = "123456789012" - os.makedirs(os.path.join(temp_data_dir, "waste")) - - # Create a cached file - cache_file = os.path.join( - temp_data_dir, "waste", f"2024-07-01_12:00_{uprn}.json" - ) - with open(cache_file, "w") as f: - json.dump({"data": sample_bristol_data}, f) - - with patch( - "agenda.bristol_waste.get_web_data", - side_effect=httpx.ReadTimeout("Timeout"), - ): - result = await bristol_waste.get_data(temp_data_dir, uprn, "refresh") - - assert result == sample_bristol_data - - -class TestGet: - """Test main get function.""" - - @pytest.mark.asyncio - async def test_get_events(self, temp_data_dir, sample_bristol_data): - """Test generating events from Bristol waste data.""" - start_date = date(2024, 7, 10) - uprn = "123456789012" - - with patch("agenda.bristol_waste.get_data", return_value=sample_bristol_data): - events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto") - - assert len(events) == 3 # Only dates before start_date - - # Check event properties - for event in events: - assert isinstance(event, Event) - assert event.name == "waste_schedule" - assert event.title.startswith("Bristol: ") - assert event.date < start_date - - @pytest.mark.asyncio - async def test_get_events_filtered_by_start_date( - self, temp_data_dir, sample_bristol_data - ): - """Test events are filtered by start date.""" - start_date = date(2024, 7, 20) # Later start date - uprn = "123456789012" - - with patch("agenda.bristol_waste.get_data", return_value=sample_bristol_data): - events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto") - - # Should get all events before start_date (all dates in sample data) - assert len(events) == 5 # All collection dates are before 2024-07-20 - for event in events: - assert event.date < start_date - - @pytest.mark.asyncio - async def test_get_events_combined_services(self, temp_data_dir): - """Test services are combined for same date.""" - # Create data with multiple services on same date - data = [ - { - "containerName": "Recycling Container", - "collection": [ - { - "nextCollectionDate": "2024-07-15T00:00:00Z", - "lastCollectionDate": "2024-07-01T00:00:00Z", - } - ], - }, - { - "containerName": "Food Waste Container", - "collection": [ - { - "nextCollectionDate": "2024-07-15T00:00:00Z", - "lastCollectionDate": "2024-07-01T00:00:00Z", - } - ], - }, - ] - - start_date = date(2024, 7, 10) - uprn = "123456789012" - - with patch("agenda.bristol_waste.get_data", return_value=data): - events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto") - - # Should have 1 event (only the 2024-07-01 date is before start_date) - assert len(events) == 1 - - # Check that the event for 2024-07-01 combines both services - july_1_event = events[0] - assert july_1_event.date == date(2024, 7, 1) - assert "Recycling" in july_1_event.title - assert "Waste Container" in july_1_event.title - - @pytest.mark.asyncio - async def test_get_empty_data(self, temp_data_dir): - """Test with empty data.""" - start_date = date(2024, 7, 10) - uprn = "123456789012" - - with patch("agenda.bristol_waste.get_data", return_value=[]): - events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto") - - assert events == [] diff --git a/tests/test_build_place_yaml.py b/tests/test_build_place_yaml.py deleted file mode 100644 index 9e181dd..0000000 --- a/tests/test_build_place_yaml.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Tests for agenda.build_place_yaml.""" - -from pathlib import Path - -import yaml - -from agenda import build_place_yaml - - -def test_upsert_station_adds_new_station(tmp_path: Path) -> None: - """Station upsert should add a new station to stations.yaml.""" - path = tmp_path / "stations.yaml" - path.write_text("""- name: London St Pancras - latitude: 51.531921 - longitude: -0.126361 - country: gb - wikidata: Q720102 - routes: {} -""") - - replaced = build_place_yaml.upsert_station( - tmp_path, - { - "name": "Paris Gare du Nord", - "latitude": 48.8809, - "longitude": 2.3553, - "country": "fr", - "wikidata": "Q624511", - "routes": {}, - }, - ) - - stations = yaml.safe_load(path.read_text()) - assert replaced is False - assert [station["name"] for station in stations] == [ - "London St Pancras", - "Paris Gare du Nord", - ] - assert "\n\n- name: Paris Gare du Nord\n" in path.read_text() - - -def test_upsert_station_replaces_existing_station(tmp_path: Path) -> None: - """Station upsert should replace an existing station with the same name.""" - path = tmp_path / "stations.yaml" - path.write_text("""- name: Paris Gare du Nord - latitude: 0 - longitude: 0 - country: fr - wikidata: Q624511 - routes: {} -""") - - replaced = build_place_yaml.upsert_station( - tmp_path, - { - "name": "Paris Gare du Nord", - "latitude": 48.8809, - "longitude": 2.3553, - "country": "fr", - "wikidata": "Q624511", - "routes": {}, - }, - ) - - stations = yaml.safe_load(path.read_text()) - assert replaced is True - assert len(stations) == 1 - assert stations[0]["latitude"] == 48.8809 - assert "\n\n- name:" not in path.read_text() - - -def test_upsert_airport_adds_mapping_entry(tmp_path: Path) -> None: - """Airport upsert should add a new IATA-keyed airport entry.""" - path = tmp_path / "airports.yaml" - path.write_text("""LHR: - iata: LHR - name: Heathrow Airport - city: London - country: gb - latitude: 51.47 - longitude: -0.4543 - qid: Q8691 -""") - - replaced = build_place_yaml.upsert_airport( - tmp_path, - { - "iata": "ORY", - "name": "Paris Orly Airport", - "city": "Paris Orly Airport", - "country": "fr", - "latitude": 48.723333, - "longitude": 2.379444, - "qid": "Q193353", - }, - ) - - airports = yaml.safe_load(path.read_text()) - assert replaced is False - assert list(airports) == ["LHR", "ORY"] - assert airports["ORY"]["country"] == "fr" diff --git a/tests/test_busy.py b/tests/test_busy.py deleted file mode 100644 index d3b80fc..0000000 --- a/tests/test_busy.py +++ /dev/null @@ -1,215 +0,0 @@ -from datetime import date, datetime, timezone - -import agenda.busy -import agenda.travel as travel -import agenda.trip -import pytest -from agenda.busy import _parse_datetime_field -from agenda.event import Event -from web_view import app - - -@pytest.fixture(scope="session") -def app_context(): - """Set up Flask app context for tests.""" - app.config["SERVER_NAME"] = "test" - with app.app_context(): - yield - - -@pytest.fixture(scope="session") -def trips(app_context): - """Load trip list once for all tests.""" - return agenda.trip.build_trip_list() - - -@pytest.fixture(scope="session") -def travel_data(app_context): - """Load travel data (bookings, accommodations, airports) once for all tests.""" - data_dir = app.config["PERSONAL_DATA"] - return { - "bookings": travel.parse_yaml("flights", data_dir), - "accommodations": travel.parse_yaml("accommodation", data_dir), - "airports": travel.parse_yaml("airports", data_dir), - "data_dir": data_dir, - } - - -def test_weekend_location_consistency(app_context, trips, travel_data): - """Test that weekend locations are consistent with events (free=home, events=away).""" - for year in range(2023, 2025): - start = date(2023, 1, 1) - busy_events = agenda.busy.get_busy_events(start, app.config, trips) - weekends = agenda.busy.weekends( - start, busy_events, trips, travel_data["data_dir"] - ) - - for weekend in weekends: - for day in "saturday", "sunday": - # When free (no events), should be home (None) - # When traveling (events), should be away (City name) - location_exists = bool(weekend[day + "_location"][0]) - has_events = bool(weekend[day]) - assert location_exists == has_events, ( - f"Weekend {weekend['date']} {day}: " - f"location_exists={location_exists}, has_events={has_events}" - ) - - -def test_specific_home_dates(travel_data): - """Test specific dates that should return home (None).""" - trips = agenda.trip.build_trip_list() - - home_dates = [ - date(2023, 4, 29), - date(2025, 7, 1), - date(2023, 12, 2), - date(2023, 10, 7), - date(2023, 2, 18), - date(2025, 8, 2), - ] - - for test_date in home_dates: - location = agenda.busy.get_location_for_date( - test_date, - trips, - ) - assert not location[ - 0 - ], f"Expected home (None) for {test_date}, got {location[0]}" - - -def test_specific_away_dates(travel_data): - """Test specific dates that should return away locations.""" - trips = agenda.trip.build_trip_list() - - away_cases = [ - (date(2025, 2, 15), "Hackettstown"), - ] - - for test_date, expected_city in away_cases: - location = agenda.busy.get_location_for_date( - test_date, - trips, - ) - assert ( - location[0] == expected_city - ), f"Expected {expected_city} for {test_date}, got {location[0]}" - - -def test_get_location_for_date_basic(travel_data): - """Test basic functionality of get_location_for_date function.""" - trips = agenda.trip.build_trip_list() - test_date = date(2023, 1, 1) - - location = agenda.busy.get_location_for_date( - test_date, - trips, - ) - - # Should return a tuple with (city|None, country) - assert isinstance(location, tuple) - assert len(location) == 2 - assert location[1] is not None # Should always have a country - - -def test_busy_event_classification(): - """Test the busy_event function for different event types.""" - - # Busy event types - busy_events = [ - Event(name="event", title="Test Event", date=date(2023, 1, 1)), - Event( - name="conference", - title="Test Conference", - date=date(2023, 1, 1), - going=True, - ), - Event(name="accommodation", title="Hotel", date=date(2023, 1, 1)), - Event(name="transport", title="Flight", date=date(2023, 1, 1)), - ] - - for event in busy_events: - assert agenda.busy.busy_event(event), f"Event {event.name} should be busy" - - # Non-busy events - non_busy_events = [ - Event( - name="conference", - title="Test Conference", - date=date(2023, 1, 1), - going=False, - ), - Event(name="other", title="Other Event", date=date(2023, 1, 1)), - Event(name="event", title="LHG Run Club", date=date(2023, 1, 1)), - Event(name="event", title="IA UK board meeting", date=date(2023, 1, 1)), - ] - - for event in non_busy_events: - assert not agenda.busy.busy_event( - event - ), f"Event {event.name}/{event.title} should not be busy" - - -def test_parse_datetime_field(): - """Test the _parse_datetime_field helper function.""" - - # Test with datetime object - dt = datetime(2023, 1, 1, 12, 0, 0) - parsed_dt, parsed_date = _parse_datetime_field(dt) - assert parsed_dt == dt.replace(tzinfo=timezone.utc) - assert parsed_dt.tzinfo == timezone.utc - assert parsed_date == date(2023, 1, 1) - - # Test with ISO string - iso_string = "2023-01-01T12:00:00Z" - parsed_dt, parsed_date = _parse_datetime_field(iso_string) - assert parsed_date == date(2023, 1, 1) - assert parsed_dt.year == 2023 - assert parsed_dt.month == 1 - assert parsed_dt.day == 1 - - -def test_get_busy_events(app_context, trips): - """Test get_busy_events function.""" - start_date = date(2023, 1, 1) - busy_events = agenda.busy.get_busy_events(start_date, app.config, trips) - - # Should return a list - assert isinstance(busy_events, list) - - # All events should be Event objects - for event in busy_events: - assert hasattr(event, "name") - assert hasattr(event, "as_date") - - # Events should be sorted by date - dates = [event.as_date for event in busy_events] - assert dates == sorted(dates), "Events should be sorted by date" - - -def test_weekends_function(app_context, trips, travel_data): - """Test the weekends function.""" - start_date = date(2023, 1, 1) - busy_events = agenda.busy.get_busy_events(start_date, app.config, trips) - weekends = agenda.busy.weekends( - start_date, busy_events, trips, travel_data["data_dir"] - ) - - # Should return a list of weekend info - assert isinstance(weekends, list) - assert len(weekends) == 52 # Should return 52 weekends - - # Each weekend should have the required keys - for weekend in weekends[:5]: # Check first 5 weekends - assert "date" in weekend - assert "saturday" in weekend - assert "sunday" in weekend - assert "saturday_location" in weekend - assert "sunday_location" in weekend - - # Locations should be tuples - assert isinstance(weekend["saturday_location"], tuple) - assert isinstance(weekend["sunday_location"], tuple) - assert len(weekend["saturday_location"]) == 2 - assert len(weekend["sunday_location"]) == 2 diff --git a/tests/test_busy_timezone.py b/tests/test_busy_timezone.py deleted file mode 100644 index 73d35dd..0000000 --- a/tests/test_busy_timezone.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Regression tests for timezone handling in busy location logic.""" - -from datetime import date, datetime, timezone - -import agenda.busy -from agenda.types import Trip - - -def test_mixed_naive_and_aware_arrivals_do_not_crash() -> None: - """Most recent travel should compare mixed timezone styles safely.""" - trips = [ - Trip( - start=date(2099, 12, 30), - travel=[ - { - "type": "flight", - "arrive": datetime(2100, 1, 1, 10, 0, 0), - "to": "CDG", - "to_airport": {"country": "fr", "city": "Paris"}, - }, - { - "type": "flight", - "arrive": datetime(2100, 1, 1, 12, 0, 0, tzinfo=timezone.utc), - "to": "AMS", - "to_airport": {"country": "nl", "city": "Amsterdam"}, - }, - ], - ) - ] - - location = agenda.busy._find_most_recent_travel_before_date(date(2100, 1, 1), trips) - assert location is not None - assert location[0] == "Amsterdam" - assert location[1] is not None - assert location[1].alpha_2 == "NL" diff --git a/tests/test_car_journey_timeline.py b/tests/test_car_journey_timeline.py deleted file mode 100644 index ad8b86d..0000000 --- a/tests/test_car_journey_timeline.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Tests for importing car journeys from a Google Timeline export.""" - -from __future__ import annotations - -import json -from datetime import date, datetime, timezone -from pathlib import Path -import typing - -from agenda import car_journey_timeline, car_journey_yaml - - -def test_journey_key_orders_split_same_day_rows() -> None: - """Datetime rows should sort chronologically within a single day.""" - earlier = { - "trip": date(2025, 7, 4), - "depart": datetime(2025, 7, 4, 8, 0, tzinfo=timezone.utc), - "arrive": datetime(2025, 7, 4, 8, 30, tzinfo=timezone.utc), - "route": "a", - } - later = { - "trip": date(2025, 7, 4), - "depart": datetime(2025, 7, 4, 9, 0, tzinfo=timezone.utc), - "arrive": datetime(2025, 7, 4, 9, 30, tzinfo=timezone.utc), - "route": "b", - } - - assert car_journey_yaml.journey_key(earlier) < car_journey_yaml.journey_key(later) - - -def write_json(path: Path, data: dict[str, typing.Any]) -> None: - """Write JSON test data.""" - path.write_text(json.dumps(data)) - - -def test_import_from_timeline_splits_stopovers(tmp_path: Path) -> None: - """Past car journeys should be split into multiple timeline segments.""" - data_dir = tmp_path - route_dir = data_dir / "car_routes" - route_dir.mkdir() - (data_dir / "car_journeys.yaml").write_text("""--- - -- trip: 2025-07-04 - depart: 2025-07-04 - arrive: 2025-07-04 - route: PCH_to_Far - -- trip: 2026-07-16 - depart: 2026-07-16 - arrive: 2026-07-16 - route: Future_to_Elsewhere -""") - write_json( - route_dir / "PCH_to_Far.geojson", - { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "LineString", - "coordinates": [[-2.6, 51.4], [-4.1, 50.3]], - }, - }, - ) - write_json( - route_dir / "Future_to_Elsewhere.geojson", - { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "LineString", - "coordinates": [[0.0, 0.0], [1.0, 1.0]], - }, - }, - ) - - timeline_path = tmp_path / "Timeline.json" - write_json( - timeline_path, - { - "semanticSegments": [ - { - "startTime": "2025-07-04T08:00:00+00:00", - "endTime": "2025-07-04T10:00:00+00:00", - "timelinePath": [ - { - "point": "51.4000°, -2.6000°", - "time": "2025-07-04T08:00:00+00:00", - }, - { - "point": "51.0000°, -3.1000°", - "time": "2025-07-04T08:30:00+00:00", - }, - ], - }, - { - "startTime": "2025-07-04T08:00:00+00:00", - "endTime": "2025-07-04T08:30:00+00:00", - "activity": { - "start": {"latLng": "51.4000°, -2.6000°"}, - "end": {"latLng": "51.0000°, -3.1000°"}, - "topCandidate": {"type": "IN_PASSENGER_VEHICLE"}, - }, - }, - { - "startTime": "2025-07-04T10:00:00+00:00", - "endTime": "2025-07-04T12:00:00+00:00", - "timelinePath": [ - { - "point": "51.0000°, -3.1000°", - "time": "2025-07-04T10:00:00+00:00", - }, - { - "point": "50.3000°, -4.1000°", - "time": "2025-07-04T11:00:00+00:00", - }, - ], - }, - { - "startTime": "2025-07-04T10:00:00+00:00", - "endTime": "2025-07-04T11:00:00+00:00", - "activity": { - "start": {"latLng": "51.0000°, -3.1000°"}, - "end": {"latLng": "50.3000°, -4.1000°"}, - "topCandidate": {"type": "IN_PASSENGER_VEHICLE"}, - }, - }, - ] - }, - ) - - routes_written, rows_replaced, rows_written = ( - car_journey_timeline.import_from_timeline( - timeline_path, data_dir=data_dir, today=date(2026, 7, 9) - ) - ) - - assert routes_written == 2 - assert rows_replaced == 1 - assert rows_written == 2 - - updated = car_journey_yaml.load_yaml_list(data_dir / "car_journeys.yaml") - assert len(updated) == 3 - assert updated[0]["from"] == "PCH" - assert updated[0]["to"] == "Drive stop 1" - assert updated[1]["from"] == "Drive stop 1" - assert updated[1]["to"] == "Far" - assert updated[2]["route"] == "Future_to_Elsewhere" - - generated_paths = sorted(route_dir.glob("timeline_*.geojson")) - assert len(generated_paths) == 2 - generated_geojson = json.loads(generated_paths[0].read_text()) - assert generated_geojson["geometry"]["type"] == "LineString" diff --git a/tests/test_car_journey_yaml.py b/tests/test_car_journey_yaml.py deleted file mode 100644 index e746cab..0000000 --- a/tests/test_car_journey_yaml.py +++ /dev/null @@ -1,344 +0,0 @@ -"""Tests for car journey YAML generation.""" - -import json -from datetime import date, datetime -from pathlib import Path - -import agenda.car_journey_yaml -from agenda.types import Trip - - -def write_home_route(data_dir: Path) -> None: - """Write a route that identifies PCH as the home coordinate.""" - route_dir = data_dir / "car_routes" - route_dir.mkdir() - (route_dir / "PCH_to_EMF.geojson").write_text( - json.dumps( - { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "LineString", - "coordinates": [[-2.60283, 51.44083], [-2.37789, 52.038]], - }, - } - ) - ) - - -def test_car_journeys_for_trip_uses_accommodation_dates_and_locations( - tmp_path: Path, -) -> None: - """Build outbound and return journeys for the trip accommodation.""" - write_home_route(tmp_path) - trip = Trip( - start=date(2025, 4, 22), - accommodation=[ - { - "location": "Callestick", - "from": datetime(2025, 4, 22, 15, 0), - "to": datetime(2025, 4, 25, 10, 0), - "latitude": 50.307093, - "longitude": -5.13466, - } - ], - ) - - journeys = agenda.car_journey_yaml.car_journeys_for_trip( - trip, tmp_path, destination="accommodation" - ) - - assert [journey.route for journey in journeys] == [ - "PCH_to_Callestick", - "Callestick_to_PCH", - ] - assert [journey.depart for journey in journeys] == [ - date(2025, 4, 22), - date(2025, 4, 25), - ] - assert journeys[0].start == (-2.60283, 51.44083) - assert journeys[0].end == (-5.13466, 50.307093) - assert journeys[1].start == (-5.13466, 50.307093) - assert journeys[1].end == (-2.60283, 51.44083) - - -def test_car_journeys_for_trip_can_route_to_airport(tmp_path: Path) -> None: - """Build outbound and return journeys for airport access.""" - write_home_route(tmp_path) - trip = Trip( - start=date(2025, 11, 17), - travel=[ - { - "type": "flight", - "depart": datetime(2025, 11, 17, 17, 55), - "arrive": datetime(2025, 11, 18, 14, 50), - "from_airport": { - "iata": "LHR", - "latitude": 51.4775, - "longitude": -0.461389, - }, - "to_airport": { - "iata": "HKG", - "latitude": 22.308889, - "longitude": 113.914444, - }, - }, - { - "type": "flight", - "depart": datetime(2025, 11, 30, 22, 45), - "arrive": datetime(2025, 12, 1, 5, 35), - "from_airport": { - "iata": "HKG", - "latitude": 22.308889, - "longitude": 113.914444, - }, - "to_airport": { - "iata": "LHR", - "latitude": 51.4775, - "longitude": -0.461389, - }, - }, - ], - ) - - journeys = agenda.car_journey_yaml.car_journeys_for_trip( - trip, tmp_path, destination="airport" - ) - - assert [journey.route for journey in journeys] == ["PCH_to_LHR", "LHR_to_PCH"] - assert [journey.depart for journey in journeys] == [ - date(2025, 11, 17), - date(2025, 12, 1), - ] - assert journeys[0].start == (-2.60283, 51.44083) - assert journeys[0].end == (-0.461389, 51.4775) - assert journeys[1].start == (-0.461389, 51.4775) - assert journeys[1].end == (-2.60283, 51.44083) - - -def test_car_journeys_for_trip_can_route_around_ferry(tmp_path: Path) -> None: - """Build home, ferry terminal and accommodation driving legs.""" - write_home_route(tmp_path) - trip = Trip( - start=date(2025, 7, 4), - travel=[ - { - "type": "ferry", - "depart": datetime(2025, 7, 4, 22, 0), - "arrive": datetime(2025, 7, 5, 8, 0), - "from": "Plymouth", - "to": "Roscoff", - "from_terminal": { - "name": "Plymouth", - "latitude": 50.3651254, - "longitude": -4.1578164, - }, - "to_terminal": { - "name": "Roscoff", - "latitude": 48.721672, - "longitude": -3.966925, - }, - }, - { - "type": "ferry", - "depart": datetime(2025, 7, 21, 15, 0), - "arrive": datetime(2025, 7, 21, 20, 10), - "from": "Roscoff", - "to": "Plymouth", - "from_terminal": { - "name": "Roscoff", - "latitude": 48.721672, - "longitude": -3.966925, - }, - "to_terminal": { - "name": "Plymouth", - "latitude": 50.3651254, - "longitude": -4.1578164, - }, - }, - ], - accommodation=[ - { - "location": "Brest", - "from": datetime(2025, 7, 5, 16, 0), - "to": datetime(2025, 7, 21, 10, 0), - "latitude": 48.3645307, - "longitude": -4.5534685, - } - ], - ) - - journeys = agenda.car_journey_yaml.car_journeys_for_trip( - trip, tmp_path, destination="ferry" - ) - - assert [journey.route for journey in journeys] == [ - "PCH_to_Plymouth", - "Roscoff_to_Brest", - "Brest_to_Roscoff", - "Plymouth_to_PCH", - ] - assert [journey.depart for journey in journeys] == [ - date(2025, 7, 4), - date(2025, 7, 5), - date(2025, 7, 21), - date(2025, 7, 21), - ] - assert journeys[0].start == (-2.60283, 51.44083) - assert journeys[0].end == (-4.1578164, 50.3651254) - assert journeys[1].start == (-3.966925, 48.721672) - assert journeys[1].end == (-4.5534685, 48.3645307) - assert journeys[2].start == (-4.5534685, 48.3645307) - assert journeys[2].end == (-3.966925, 48.721672) - assert journeys[3].start == (-4.1578164, 50.3651254) - assert journeys[3].end == (-2.60283, 51.44083) - - -def test_car_journeys_for_trip_can_route_fly_drive_chain(tmp_path: Path) -> None: - """Build home, destination rental and return airport driving legs.""" - write_home_route(tmp_path) - trip = Trip( - start=date(2025, 9, 13), - travel=[ - { - "type": "flight", - "depart": datetime(2025, 9, 14, 6, 50), - "arrive": datetime(2025, 9, 14, 8, 55), - "from_airport": { - "iata": "LTN", - "latitude": 51.8747, - "longitude": -0.3683, - "country": "gb", - }, - "to_airport": { - "iata": "KEF", - "latitude": 63.985, - "longitude": -22.605556, - "country": "is", - }, - }, - { - "type": "flight", - "depart": datetime(2025, 10, 1, 9, 0), - "arrive": datetime(2025, 10, 1, 13, 0), - "from_airport": { - "iata": "KEF", - "latitude": 63.985, - "longitude": -22.605556, - "country": "is", - }, - "to_airport": { - "iata": "LTN", - "latitude": 51.8747, - "longitude": -0.3683, - "country": "gb", - }, - }, - ], - accommodation=[ - { - "location": "Luton Airport", - "country": "gb", - "from": datetime(2025, 9, 13, 15, 0), - "to": datetime(2025, 9, 14, 12, 0), - "latitude": 51.8745678, - "longitude": -0.3844256, - }, - { - "location": "Akranes", - "country": "is", - "from": datetime(2025, 9, 14, 16, 0), - "to": datetime(2025, 9, 16, 11, 0), - "latitude": 64.4344723, - "longitude": -21.5517961, - }, - { - "location": "Ísafjörður", - "country": "is", - "from": datetime(2025, 9, 16, 15, 0), - "to": datetime(2025, 10, 1, 11, 0), - "latitude": 66.072445, - "longitude": -23.120111, - }, - ], - ) - - journeys = agenda.car_journey_yaml.car_journeys_for_trip( - trip, tmp_path, destination="fly-drive" - ) - - assert [journey.route for journey in journeys] == [ - "PCH_to_Luton_Airport", - "KEF_to_Akranes", - "Akranes_to_Isafjordur", - "Isafjordur_to_KEF", - "LTN_to_PCH", - ] - assert [journey.depart for journey in journeys] == [ - date(2025, 9, 13), - date(2025, 9, 14), - date(2025, 9, 16), - date(2025, 10, 1), - date(2025, 10, 1), - ] - - -def test_write_route_files_skips_existing_routes(tmp_path: Path) -> None: - """Existing route files should not call the route API.""" - write_home_route(tmp_path) - journey = agenda.car_journey_yaml.CarJourney( - trip=date(2025, 4, 22), - depart=date(2025, 4, 22), - arrive=date(2025, 4, 22), - route="PCH_to_EMF", - start=(-2.60283, 51.44083), - end=(-2.37789, 52.038), - ) - - def fail_fetch( - _start: agenda.car_journey_yaml.LonLat, - _end: agenda.car_journey_yaml.LonLat, - ) -> agenda.car_journey_yaml.GeoJSON: - raise AssertionError("route API should not be called") - - written = agenda.car_journey_yaml.write_route_files(tmp_path, [journey], fail_fetch) - - assert written == 0 - - -def test_import_car_journeys_inserts_chronologically_and_is_idempotent( - tmp_path: Path, -) -> None: - """Generated journeys should be inserted without duplicates.""" - (tmp_path / "car_journeys.yaml").write_text("""--- -- trip: 2025-05-01 - depart: 2025-05-01 - arrive: 2025-05-01 - route: PCH_to_Later -""") - journeys = [ - agenda.car_journey_yaml.CarJourney( - trip=date(2025, 4, 22), - depart=date(2025, 4, 22), - arrive=date(2025, 4, 22), - route="PCH_to_Callestick", - start=(-2.60283, 51.44083), - end=(-5.13466, 50.307093), - ), - agenda.car_journey_yaml.CarJourney( - trip=date(2025, 4, 22), - depart=date(2025, 4, 25), - arrive=date(2025, 4, 25), - route="Callestick_to_PCH", - start=(-5.13466, 50.307093), - end=(-2.60283, 51.44083), - ), - ] - - added = agenda.car_journey_yaml.import_car_journeys(tmp_path, journeys) - added_again = agenda.car_journey_yaml.import_car_journeys(tmp_path, journeys) - - assert added == 2 - assert added_again == 0 - text = (tmp_path / "car_journeys.yaml").read_text() - assert text.index("PCH_to_Callestick") < text.index("PCH_to_Later") diff --git a/tests/test_carnival.py b/tests/test_carnival.py deleted file mode 100644 index 5985262..0000000 --- a/tests/test_carnival.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Tests for carnival functionality.""" - -from datetime import date - -from agenda.carnival import rio_carnival_events -from agenda.event import Event - - -class TestRioCarnivalEvents: - """Test the rio_carnival_events function.""" - - def test_carnival_events_single_year(self) -> None: - """Test getting carnival events for a single year.""" - # 2024 Easter is March 31, so carnival should be around Feb 9-14 - start_date = date(2024, 1, 1) - end_date = date(2024, 12, 31) - - events = rio_carnival_events(start_date, end_date) - - assert len(events) == 1 - event = events[0] - assert event.name == "carnival" - assert event.title == "Rio Carnival" - assert event.url == "https://en.wikipedia.org/wiki/Rio_Carnival" - assert event.date.year == 2024 - assert event.end_date is not None - assert event.end_date.year == 2024 - # Should be about 51 days before Easter (around early-mid February) - assert event.date.month == 2 - assert event.end_date.month == 2 - - def test_carnival_events_multiple_years(self) -> None: - """Test getting carnival events for multiple years.""" - start_date = date(2023, 1, 1) - end_date = date(2025, 12, 31) - - events = rio_carnival_events(start_date, end_date) - - # Should have carnival for 2023, 2024, and 2025 - assert len(events) == 3 - - years = [event.date.year for event in events] - assert sorted(years) == [2023, 2024, 2025] - - # All events should be carnival events - for event in events: - assert event.name == "carnival" - assert event.title == "Rio Carnival" - assert event.url == "https://en.wikipedia.org/wiki/Rio_Carnival" - - def test_carnival_events_no_overlap(self) -> None: - """Test when date range doesn't overlap with carnival.""" - # Choose a range that's unlikely to include carnival (summer) - start_date = date(2024, 6, 1) - end_date = date(2024, 8, 31) - - events = rio_carnival_events(start_date, end_date) - - assert events == [] - - def test_carnival_events_partial_overlap_start(self) -> None: - """Test when carnival start overlaps with date range.""" - # 2024 carnival should be around Feb 9-14 - start_date = date(2024, 2, 10) # Might overlap with carnival start - end_date = date(2024, 2, 15) - - events = rio_carnival_events(start_date, end_date) - - # Should include carnival if there's any overlap - if events: - assert len(events) == 1 - assert events[0].name == "carnival" - - def test_carnival_events_partial_overlap_end(self) -> None: - """Test when carnival end overlaps with date range.""" - # 2024 carnival should be around Feb 9-14 - start_date = date(2024, 2, 12) - end_date = date(2024, 2, 20) # Might overlap with carnival end - - events = rio_carnival_events(start_date, end_date) - - # Should include carnival if there's any overlap - if events: - assert len(events) == 1 - assert events[0].name == "carnival" - - def test_carnival_dates_relative_to_easter(self) -> None: - """Test that carnival dates are correctly calculated relative to Easter.""" - start_date = date(2024, 1, 1) - end_date = date(2024, 12, 31) - - events = rio_carnival_events(start_date, end_date) - - assert len(events) == 1 - event = events[0] - - # Carnival should be 5 days long - duration = (event.end_date - event.date).days + 1 - assert duration == 6 # 51 days before to 46 days before Easter (6 days total) - - # Both dates should be in February for 2024 - assert event.date.month == 2 - assert event.end_date.month == 2 - - # End date should be after start date - assert event.end_date > event.date - - def test_carnival_events_empty_date_range(self) -> None: - """Test with empty date range.""" - start_date = date(2024, 6, 15) - end_date = date(2024, 6, 10) # End before start - - events = rio_carnival_events(start_date, end_date) - - # Should return empty list for invalid range - assert events == [] - - def test_carnival_events_same_start_end_date(self) -> None: - """Test with same start and end date.""" - # Pick a date that's definitely not carnival - test_date = date(2024, 7, 15) - - events = rio_carnival_events(test_date, test_date) - - assert events == [] \ No newline at end of file diff --git a/tests/test_conference.py b/tests/test_conference.py deleted file mode 100644 index d056527..0000000 --- a/tests/test_conference.py +++ /dev/null @@ -1,468 +0,0 @@ -"""Tests for agenda.conference module.""" - -import decimal -import tempfile -from datetime import date, datetime -from typing import Any - -import pytest -import yaml - -from agenda.conference import Conference, conference_date_fields, get_list -from agenda.event import Event - - -class TestConference: - """Tests for Conference dataclass.""" - - def test_conference_creation_minimal(self) -> None: - """Test creating conference with minimal required fields.""" - conf = Conference( - name="PyCon", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.name == "PyCon" - assert conf.topic == "Python" - assert conf.location == "Portland" - assert conf.start == date(2024, 5, 15) - assert conf.end == date(2024, 5, 17) - assert conf.trip is None - assert conf.going is False - assert conf.online is False - - def test_conference_creation_full(self) -> None: - """Test creating conference with all fields.""" - conf = Conference( - name="PyCon US", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - trip=date(2024, 5, 14), - country="USA", - venue="Convention Center", - address="123 Main St", - url="https://pycon.org", - accommodation_booked=True, - transport_booked=True, - going=True, - registered=True, - speaking=True, - online=False, - price=decimal.Decimal("500.00"), - currency="USD", - latitude=45.5152, - longitude=-122.6784, - cfp_end=date(2024, 2, 1), - cfp_url="https://pycon.org/cfp", - free=False, - hackathon=True, - ticket_type="early_bird", - attendees=3000, - hashtag="#pycon2024", - ) - assert conf.name == "PyCon US" - assert conf.going is True - assert conf.price == decimal.Decimal("500.00") - assert conf.currency == "USD" - assert conf.latitude == 45.5152 - assert conf.longitude == -122.6784 - assert conf.cfp_end == date(2024, 2, 1) - assert conf.hashtag == "#pycon2024" - - def test_display_name_location_in_name(self) -> None: - """Test display_name when location is already in conference name.""" - conf = Conference( - name="PyCon Portland", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.display_name == "PyCon Portland" - - def test_display_name_location_not_in_name(self) -> None: - """Test display_name when location is not in conference name.""" - conf = Conference( - name="PyCon", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.display_name == "PyCon (Portland)" - - def test_display_name_partial_location_match(self) -> None: - """Test display_name when location is partially in name.""" - conf = Conference( - name="PyConf", - topic="Python", - location="Conference Center", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.display_name == "PyConf (Conference Center)" - - def test_conference_with_datetime(self) -> None: - """Test conference with datetime objects.""" - start_dt = datetime(2024, 5, 15, 9, 0) - end_dt = datetime(2024, 5, 17, 17, 0) - conf = Conference( - name="PyCon", - topic="Python", - location="Portland", - start=start_dt, - end=end_dt, - ) - assert conf.start == start_dt - assert conf.end == end_dt - - -class TestGetList: - """Tests for get_list function.""" - - def test_get_list_single_conference(self) -> None: - """Test reading single conference from YAML.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - "url": "https://pycon.org", - "going": True, - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - event = events[0] - assert isinstance(event, Event) - assert event.name == "conference" - assert event.date == date(2024, 5, 15) - assert event.end_date == date(2024, 5, 17) - assert event.title == "PyCon (Portland)" - assert event.url == "https://pycon.org" - assert event.going is True - - def test_get_list_conference_with_cfp(self) -> None: - """Test reading conference with CFP end date.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - "url": "https://pycon.org", - "cfp_end": date(2024, 2, 1), - "cfp_url": "https://pycon.org/cfp", - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 2 - - # Conference event - conf_event = events[0] - assert conf_event.name == "conference" - assert conf_event.title == "PyCon (Portland)" - assert conf_event.url == "https://pycon.org" - - # CFP end event - cfp_event = events[1] - assert cfp_event.name == "cfp_end" - assert cfp_event.date == date(2024, 2, 1) - assert cfp_event.title == "CFP end: PyCon (Portland)" - assert cfp_event.url == "https://pycon.org/cfp" - - def test_get_list_conference_cfp_no_url(self) -> None: - """Test reading conference with CFP end date but no CFP URL.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - "url": "https://pycon.org", - "cfp_end": date(2024, 2, 1), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 2 - cfp_event = events[1] - assert cfp_event.url == "https://pycon.org" # Falls back to conference URL - - def test_get_list_multiple_conferences(self) -> None: - """Test reading multiple conferences from YAML.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - }, - { - "name": "EuroPython", - "topic": "Python", - "location": "Prague", - "start": date(2024, 7, 8), - "end": date(2024, 7, 14), - "cfp_end": date(2024, 3, 15), - }, - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 3 # 2 conferences + 1 CFP end - - # First conference - assert events[0].title == "PyCon (Portland)" - assert events[0].date == date(2024, 5, 15) - - # Second conference - assert events[1].title == "EuroPython (Prague)" - assert events[1].date == date(2024, 7, 8) - - # CFP end event for second conference - assert events[2].name == "cfp_end" - assert events[2].title == "CFP end: EuroPython (Prague)" - - def test_get_list_location_in_name(self) -> None: - """Test conference where location is already in name.""" - yaml_data = [ - { - "name": "PyCon Portland", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - assert events[0].title == "PyCon Portland" # No location appended - - def test_get_list_datetime_objects(self) -> None: - """Test reading conferences with datetime objects.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": datetime(2024, 5, 15, 9, 0), - "end": datetime(2024, 5, 17, 17, 0), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - event = events[0] - assert event.date == datetime(2024, 5, 15, 9, 0) - assert event.end_date == datetime(2024, 5, 17, 17, 0) - - def test_get_list_nested_exact_dates(self) -> None: - """Test reading conference with nested exact dates.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "dates": { - "status": "exact", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - }, - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - assert events[0].date == date(2024, 5, 15) - assert events[0].end_date == date(2024, 5, 17) - - def test_get_list_tentative_dates_do_not_create_conference_event(self) -> None: - """Test tentative conference dates are not emitted as calendar events.""" - yaml_data = [ - { - "name": "FOSDEM", - "topic": "FOSDEM", - "location": "Brussels", - "dates": { - "status": "tentative", - "start": date(2027, 1, 30), - "end": date(2027, 1, 31), - "label": "likely first weekend of February 2027", - }, - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert events == [] - - def test_get_list_approximate_dates_keep_cfp_event(self) -> None: - """Test approximate dates do not block CFP reminders.""" - yaml_data = [ - { - "name": "PyCascades", - "topic": "Python", - "location": "TBC", - "dates": { - "status": "approximate", - "label": "March 2027", - "earliest": date(2027, 3, 1), - "latest": date(2027, 3, 31), - }, - "cfp_end": date(2026, 11, 1), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - assert events[0].name == "cfp_end" - assert events[0].date == date(2026, 11, 1) - - def test_conference_date_fields_approximate(self) -> None: - """Test derived fields for approximate conference dates.""" - fields = conference_date_fields( - { - "name": "PyCascades", - "topic": "Python", - "location": "TBC", - "dates": { - "status": "approximate", - "label": "March 2027", - "earliest": date(2027, 3, 1), - "latest": date(2027, 3, 31), - }, - } - ) - - assert fields["date_status"] == "approximate" - assert fields["sort_date"] == date(2027, 3, 1) - assert fields["latest_date"] == date(2027, 3, 31) - assert fields["display_date"] == "March 2027" - assert fields["has_exact_dates"] is False - - def test_get_list_invalid_date_order(self) -> None: - """Test that conferences with end before start raise assertion error.""" - yaml_data = [ - { - "name": "Invalid Conference", - "topic": "Testing", - "location": "Nowhere", - "start": date(2024, 5, 17), - "end": date(2024, 5, 15), # End before start - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - with pytest.raises(AssertionError): - get_list(f.name) - - def test_get_list_too_long_conference(self) -> None: - """Test that conferences longer than MAX_CONF_DAYS raise assertion error.""" - yaml_data = [ - { - "name": "Too Long Conference", - "topic": "Testing", - "location": "Nowhere", - "start": date(2024, 5, 1), - "end": date(2024, 6, 1), # More than 20 days - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - with pytest.raises(AssertionError): - get_list(f.name) - - def test_get_list_empty_file(self) -> None: - """Test reading empty YAML file.""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump([], f) - f.flush() - - events = get_list(f.name) - - assert events == [] - - def test_get_list_same_day_conference(self) -> None: - """Test conference that starts and ends on same day.""" - yaml_data = [ - { - "name": "One Day Conference", - "topic": "Testing", - "location": "Test City", - "start": date(2024, 5, 15), - "end": date(2024, 5, 15), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - event = events[0] - assert event.date == date(2024, 5, 15) - assert event.end_date == date(2024, 5, 15) diff --git a/tests/test_conference_detail.py b/tests/test_conference_detail.py deleted file mode 100644 index 74f97b1..0000000 --- a/tests/test_conference_detail.py +++ /dev/null @@ -1,587 +0,0 @@ -"""Conference detail pages, airport resolution and flight caching.""" - -import typing -from datetime import date, datetime, timedelta, timezone -from pathlib import Path - -import pytest -import yaml - -import agenda.conference_detail as detail -import agenda.conference_flights as flights -import agenda.conference_list -import agenda.conference_page as page -import agenda.flight_search_cache as cache -import agenda.trip -import web_view -from agenda.types import StrDict - - -def test_airport_suggestions(tmp_path: Path) -> None: - """Mappings can be overridden; proximity stays within the conference country.""" - conf: StrDict = {"location": "Brussels", "country": "be"} - assert detail.suggest_airport(conf, str(tmp_path))[0] == "BRU" - (tmp_path / "conference_airports.yaml").write_text("be:brussels: CRL\n") - assert detail.suggest_airport(conf, str(tmp_path))[0] == "CRL" - (tmp_path / "airports.yaml").write_text( - yaml.safe_dump( - { - "AAA": { - "city": "Elsewhere", - "country": "fr", - "latitude": 50, - "longitude": 4, - }, - "BBB": { - "city": "Another", - "country": "be", - "latitude": 50.1, - "longitude": 4.1, - }, - } - ) - ) - assert ( - detail.suggest_airport( - {"location": "Town", "country": "be", "latitude": 50, "longitude": 4}, - str(tmp_path), - )[0] - == "BBB" - ) - assert detail.suggest_airport({"location": "TBC"}, str(tmp_path))[0] is None - - -def test_short_haul() -> None: - """European geography determines Bristol preference, with a distance limit.""" - assert detail.is_short_haul({"country": "BE"}) - assert not detail.is_short_haul({"country": "us"}) - assert not detail.is_short_haul({"country": "fr", "latitude": -21, "longitude": 55}) - - -def test_page_is_cache_only(tmp_path: Path, monkeypatch: typing.Any) -> None: - """GET displays venue/details without initiating a search; POST requires a token.""" - conf: StrDict = { - "name": "Mapping 2099", - "location": "Brussels", - "country": "be", - "topic": "Maps", - "start": date(2099, 5, 5), - "end": date(2099, 5, 6), - "venue": "Test Venue", - "address": "123 Test Street", - "latitude": 50.8, - "longitude": 4.3, - "url": "https://example.com/conference", - "description": "A great conference", - } - (tmp_path / "conferences.yaml").write_text(yaml.safe_dump([conf])) - monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path)) - monkeypatch.setitem(web_view.app.config, "DATA_DIR", str(tmp_path)) - monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: []) - monkeypatch.setattr( - flights, "lookup", lambda *args: pytest.fail("GET made a lookup") - ) - monkeypatch.setattr(flights, "valid_airport", lambda code: code == "BRU") - url = f"/conference/2099-05-05/{detail.conference_slug(conf)}" - with web_view.app.test_client() as client: - response = client.get(url) - assert response.status_code == 200 - for value in ( - b"Test Venue", - b"123 Test Street", - b"View venue on map", - b"A great conference", - b"Look up flights", - b"Conference website", - ): - assert value in response.data - assert client.post(url, data={"airport": "BRU"}).status_code == 400 - assert client.get("/conference/2099-05-05/missing").status_code == 404 - cached: StrDict = { - "updated_at": datetime.now(timezone.utc).isoformat(), - "destination": "BRU", - "searches": [], - } - path = flights.cache_path( - str(tmp_path), conf["start"], conf["end"], "BRU", True - ) - path.parent.mkdir() - flights.write_cache(path, cached) - response = client.get(url) - assert b"Last updated" in response.data and b"Update flights" in response.data - with client.session_transaction() as session: - token = session["conference_flight_token"] - calls: list[str] = [] - - def lookup(*args: typing.Any) -> StrDict: - calls.append(args[3]) - return cached - - monkeypatch.setattr(flights, "lookup", lookup) - assert ( - client.post(url, data={"airport": "BRU", "csrf_token": token}).status_code - == 302 - ) - assert calls == ["BRU"] - monkeypatch.setattr( - flights, - "resolve_airport", - lambda query: ("CPH", "Copenhagen Kastrup Airport"), - ) - response = client.post( - url + "?airport=BRU", - data={"airport": "Copenhagen, Denmark", "csrf_token": token}, - ) - assert response.status_code == 302 - assert response.headers["Location"].endswith("airport=CPH") - assert calls == ["BRU", "CPH"] - - -def test_slug_collision(tmp_path: Path) -> None: - """Events of one series on one day receive distinct reproducible URLs.""" - conferences = [ - { - "name": name, - "series": "maps", - "location": "Berlin", - "start": date(2099, 5, 1), - } - for name in ("Maps morning", "Maps evening") - ] - (tmp_path / "conferences.yaml").write_text(yaml.safe_dump(conferences)) - rows = agenda.conference_list.build_conference_list(str(tmp_path), []) - assert rows[0]["slug"] != rows[1]["slug"] - assert ( - detail.conference_slug({"name": "FOSDEM", "location": "Brussels"}) - == "fosdem-brussels" - ) - - -@pytest.mark.parametrize( - "short_haul,bristol_complete,expected", - [(True, True, ["BRS"]), (True, False, ["BRS", "LON"]), (False, True, ["LON"])], -) -def test_cache_and_fallback( - tmp_path: Path, - monkeypatch: typing.Any, - short_haul: bool, - bristol_complete: bool, - expected: list[str], -) -> None: - """Bristol preference, London fallback, and refresh throttling.""" - calls: list[str] = [] - - def search( - origin: str, destination: str, start: date, end: date, flexible: bool - ) -> StrDict: - calls.append(origin) - return { - "origin": origin, - "outbound": [{"price": 50}], - "inbound": [{"price": 60}] if bristol_complete else [], - } - - monkeypatch.setattr(flights, "search_origin", search) - start, end = date(2099, 1, 5), date(2099, 1, 6) - path = flights.cache_path(str(tmp_path), start, end, "BRU", short_haul) - result = flights.lookup(path, start, end, "BRU", short_haul) - assert calls == expected - assert flights.lookup(path, start, end, "BRU", short_haul) == result - assert calls == expected - - -def test_failed_refresh_keeps_results(tmp_path: Path, monkeypatch: typing.Any) -> None: - """Failures retain old results and get a short retry cooldown.""" - path = flights.cache_path( - str(tmp_path), date(2099, 1, 5), date(2099, 1, 6), "BRU", True - ) - path.parent.mkdir() - old: StrDict = { - "updated_at": (datetime.now(timezone.utc) - timedelta(days=1)).isoformat(), - "destination": "BRU", - "searches": [{"origin": "BRS", "outbound": [1], "inbound": [2]}], - } - flights.write_cache(path, old) - calls: list[bool] = [] - - def fail(*args: typing.Any, **kwargs: typing.Any) -> StrDict: - calls.append(True) - raise RuntimeError("network unavailable") - - monkeypatch.setattr(flights, "search_origin", fail) - result = flights.lookup(path, date(2099, 1, 5), date(2099, 1, 6), "BRU", True) - assert result["updated_at"] == old["updated_at"] - assert result["searches"] == old["searches"] - assert result["error"] - assert "RuntimeError: network unavailable" in result["error_details"] - flights.lookup(path, date(2099, 1, 5), date(2099, 1, 6), "BRU", True) - assert len(calls) == 1 - - -@pytest.mark.parametrize( - "outbound_offset,inbound_offset,expected_calls", - [(1, 1, 2), (3, 1, 4), (1, 3, 4), (None, None, 8)], -) -def test_flexible_direct_dates( - monkeypatch: typing.Any, - outbound_offset: int | None, - inbound_offset: int | None, - expected_calls: int, -) -> None: - """Each direction expands only as needed, excluding late outbound arrivals.""" - calls: list[tuple[str, str, date, bool]] = [] - start, end = date(2099, 1, 5), date(2099, 1, 6) - - def search(origin: str, destination: str, day: date, direct: bool) -> list[StrDict]: - calls.append((origin, destination, day, direct)) - if origin == "BRS": - offset = (start - day).days - if offset == 1 and outbound_offset != 1: - return [{"arrival": start.isoformat() + "T18:00:00", "price": 30}] - available = offset == outbound_offset - else: - available = (day - end).days == inbound_offset - return ( - [{"arrival": day.isoformat() + "T18:00:00", "price": 30}] - if available - else [] - ) - - monkeypatch.setattr(flights, "search_day", search) - result = flights.search_origin("BRS", "BRU", start, end, True) - assert len(calls) == expected_calls - assert all(call[3] for call in calls) - assert bool(result["outbound"]) == (outbound_offset is not None) - assert bool(result["inbound"]) == (inbound_offset is not None) - assert sum(call[0] == "BRS" for call in calls) == (outbound_offset or 4) - assert sum(call[0] == "BRU" for call in calls) == (inbound_offset or 4) - - -def test_bad_cache(tmp_path: Path) -> None: - """Invalid JSON and naive timestamps are ignored.""" - path = tmp_path / "cache.json" - path.write_text('{"updated_at": "2026-01-01T12:00:00", "searches": []}') - assert flights.read_cache(path) is None - path.write_text("bad json") - assert flights.read_cache(path) is None - - -def test_airport_autocomplete(monkeypatch: typing.Any) -> None: - """Autocomplete uses the local index and returns airport names and codes.""" - queries: list[str] = [] - - def matches(query: str) -> list[StrDict]: - queries.append(query) - return [{"code": "CPH", "name": "Copenhagen Kastrup Airport"}] - - monkeypatch.setattr(flights, "airport_matches", matches) - with web_view.app.test_client() as client: - assert client.get("/conference/airports?q=C").json == [] - response = client.get("/conference/airports?q=Copenhagen%2C%20Denmark") - assert response.json == [{"code": "CPH", "name": "Copenhagen Kastrup Airport"}] - assert queries == ["Copenhagen, Denmark"] - - -def test_multi_airport_location_mapping(tmp_path: Path) -> None: - """Mappings support airport lists, accents, and personal overrides.""" - for location in ("Malmö", "Malmo"): - assert ( - detail.suggest_airport( - {"country": "se", "location": location}, str(tmp_path) - )[0] - == "MMX,CPH" - ) - (tmp_path / "conference_airports.yaml").write_text('"se:malmo": [CPH, MMX]\n') - assert ( - detail.suggest_airport({"country": "se", "location": "Malmö"}, str(tmp_path))[0] - == "CPH,MMX" - ) - - -@pytest.mark.parametrize("available_offset", [2, 3, None]) -def test_london_earlier_departure( - monkeypatch: typing.Any, available_offset: int | None -) -> None: - """London expands the outbound by one day, without repeating the return search.""" - start, end = date(2099, 1, 5), date(2099, 1, 6) - calls: list[tuple[str, date]] = [] - - def search(origin: str, destination: str, day: date, direct: bool) -> list[StrDict]: - calls.append((origin, day)) - if origin == "LON" and (start - day).days != available_offset: - return [] - return [{"arrival": day.isoformat() + "T18:00:00"}] - - monkeypatch.setattr(flights, "search_day", search) - result = flights.search_origin("LON", "BKW", start, end, False) - expected = ["2099-01-03"] if available_offset == 2 else ["2099-01-03", "2099-01-02"] - assert result["outbound_dates"] == expected - assert result["inbound_dates"] == ["2099-01-07"] - assert sum(origin == "BKW" for origin, _ in calls) == 1 - assert bool(result["outbound"]) == (available_offset is not None) - - -def test_legacy_empty_search_can_expand( - tmp_path: Path, monkeypatch: typing.Any -) -> None: - """An old fresh empty London cache can immediately try the new earlier date.""" - start, end = date(2099, 1, 5), date(2099, 1, 6) - path = flights.cache_path(str(tmp_path), start, end, "BKW", False) - path.parent.mkdir() - flights.write_cache( - path, - { - "updated_at": datetime.now(timezone.utc).isoformat(), - "searches": [{"origin": "LON", "outbound": [], "inbound": []}], - "transport": cache.TRANSPORT, - }, - ) - calls: list[date] = [] - - def search(origin: str, destination: str, day: date, direct: bool) -> list[StrDict]: - calls.append(day) - return [] - - monkeypatch.setattr(flights, "search_day", search) - result = flights.lookup(path, start, end, "BKW", False) - assert result["searches"][0]["outbound_dates"] == ["2099-01-03", "2099-01-02"] - assert len(calls) == 3 - assert flights.lookup(path, start, end, "BKW", False) == result - assert len(calls) == 3 - - -def test_empty_flights_show_airport_and_dates( - tmp_path: Path, monkeypatch: typing.Any -) -> None: - """Cached empty results show full airport names and checked departure dates.""" - conf: StrDict = { - "name": "Mapping 2099", - "location": "Raleigh", - "country": "us", - "start": date(2099, 5, 5), - "end": date(2099, 5, 6), - } - (tmp_path / "conferences.yaml").write_text(yaml.safe_dump([conf])) - monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path)) - monkeypatch.setitem(web_view.app.config, "DATA_DIR", str(tmp_path)) - monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: []) - path = flights.cache_path(str(tmp_path), conf["start"], conf["end"], "BKW", False) - path.parent.mkdir() - cached: StrDict = { - "updated_at": datetime.now(timezone.utc).isoformat(), - "destination": "BKW", - "searches": [ - { - "origin": "LON", - "outbound": [], - "inbound": [], - "outbound_dates": ["2099-05-03", "2099-05-02"], - "inbound_dates": ["2099-05-07"], - } - ], - } - flights.write_cache(path, cached) - url = "/conference/2099-05-05/" + detail.conference_slug(conf) + "?airport=BKW" - with web_view.app.test_client() as client: - response = client.get(url) - assert response.status_code == 200 - assert b"Raleigh County Memorial Airport (BKW)" in response.data - for day in ("2099-05-03", "2099-05-02", "2099-05-07"): - assert page.format_date(day).encode() in response.data - # Legacy cache data reconstructs the single London departure date. - del cached["searches"][0]["outbound_dates"] - del cached["searches"][0]["inbound_dates"] - flights.write_cache(path, cached) - response = client.get(url) - assert page.format_date("2099-05-03").encode() in response.data - - -@pytest.mark.parametrize( - "location,online,eligible", - [("Belfast", False, True), ("Belfast", True, False), ("London", False, False)], -) -def test_belfast_domestic_exception( - tmp_path: Path, monkeypatch: typing.Any, location: str, online: bool, eligible: bool -) -> None: - """Belfast alone permits UK flight lookups; online events remain excluded.""" - conf: StrDict = { - "name": "Test conference", - "location": location, - "country": "gb", - "start": date(2099, 5, 5), - "end": date(2099, 5, 6), - "online": online, - } - (tmp_path / "conferences.yaml").write_text(yaml.safe_dump([conf])) - monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path)) - monkeypatch.setitem(web_view.app.config, "DATA_DIR", str(tmp_path)) - monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: []) - calls: list[tuple[str, bool]] = [] - - def lookup( - path: Path, start: date, end: date, destination: str, short_haul: bool - ) -> StrDict: - calls.append((destination, short_haul)) - return {} - - monkeypatch.setattr(flights, "lookup", lookup) - url = "/conference/2099-05-05/" + detail.conference_slug(conf) - with web_view.app.test_client() as client: - response = client.get(url) - assert response.status_code == 200 - assert (b"Look up flights" in response.data) == eligible - assert calls == [] - if eligible: - assert b"BFS,BHD" in response.data - with client.session_transaction() as session: - token = session["conference_flight_token"] - response = client.post(url, data={"airport": location, "csrf_token": token}) - assert response.status_code == (302 if eligible else 400) - assert calls == ([("BFS,BHD", True)] if eligible else []) - - -def test_personal_airport_names(tmp_path: Path, monkeypatch: typing.Any) -> None: - """Personal names apply to codes, groups and autocomplete, with live updates.""" - path = tmp_path / "airports.yaml" - path.write_text( - yaml.safe_dump({"BSL": {"name": "My Basel Airport"}, "BRN": {"name": ""}}) - ) - monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path)) - with web_view.app.app_context(): - assert flights.resolve_airport("BSL") == ("BSL", "My Basel Airport") - code, name = flights.resolve_airport("Bern") - assert code == "BRN,BSL" - assert name == "Bern Airport / My Basel Airport" - assert flights.resolve_airport("BRN,BSL")[1] == name - assert flights.airport_matches("My Basel Airport")[0] == { - "code": "BSL", - "name": "My Basel Airport", - } - assert any( - match["code"] == "BSL" for match in flights.airport_matches("EuroAirport") - ) - with web_view.app.test_client() as client: - assert client.get("/conference/airports?q=BSL").json == [ - {"code": "BSL", "name": "My Basel Airport"} - ] - path.write_text(yaml.safe_dump({"BSL": {"name": "Renamed Basel Airport"}})) - assert client.get("/conference/airports?q=BSL").json == [ - {"code": "BSL", "name": "Renamed Basel Airport"} - ] - path.unlink() - fallback = client.get("/conference/airports?q=BSL").json - assert isinstance(fallback, list) - assert "EuroAirport" in fallback[0]["name"] - - -def test_date_limit_banner(tmp_path: Path, monkeypatch: typing.Any) -> None: - """A wrapped date rejection asks to retry nearer departure, keeping cached fares.""" - from agenda.google_flights import FlightDateUnavailableError - - start, end = date(2099, 1, 5), date(2099, 1, 6) - path = flights.cache_path(str(tmp_path), start, end, "AKJ", False) - path.parent.mkdir() - old: StrDict = { - "updated_at": (datetime.now(timezone.utc) - timedelta(days=1)).isoformat(), - "destination": "AKJ", - "searches": [{"origin": "LON", "outbound": [{"price": 100}], "inbound": []}], - } - flights.write_cache(path, old) - - def fail(*args: typing.Any, **kwargs: typing.Any) -> StrDict: - try: - raise FlightDateUnavailableError( - "Requested flight date is too far in the future. Please try again closer to departure." - ) - except FlightDateUnavailableError as exc: - raise RuntimeError("AKJ → LON on 2099-01-07") from exc - - monkeypatch.setattr(flights, "search_origin", fail) - result = flights.lookup(path, start, end, "AKJ", False) - assert "closer to departure" in result["error"] - assert "15 minutes" not in result["error"] - assert result["searches"] == old["searches"] - assert result["updated_at"] == old["updated_at"] - - -def test_previous_parser_error_can_retry_once( - tmp_path: Path, monkeypatch: typing.Any -) -> None: - """The parser fix unlocks a recent old-format error, but fresh errors still throttle.""" - start, end = date(2099, 6, 2), date(2099, 6, 4) - path = flights.cache_path(str(tmp_path), start, end, "BLQ", True) - path.parent.mkdir() - flights.write_cache( - path, - { - "updated_at": datetime.now(timezone.utc).isoformat(), - "searches": [], - "error": "Flight lookup failed", - "error_details": "ValueError: Unrecognized Google Flights result format", - "transport": cache.TRANSPORT, - }, - ) - calls: list[bool] = [] - - def fail(*args: typing.Any, **kwargs: typing.Any) -> StrDict: - calls.append(True) - raise ValueError("Unrecognized Google Flights result format") - - monkeypatch.setattr(flights, "search_origin", fail) - refreshed = flights.lookup(path, start, end, "BLQ", True) - assert len(calls) == 1 - assert flights.lookup(path, start, end, "BLQ", True) == refreshed - assert len(calls) == 1 - - -@pytest.mark.parametrize( - "status,eligible", [("exact", True), ("tentative", True), ("approximate", False)] -) -def test_flight_lookup_with_tentative_dates( - tmp_path: Path, monkeypatch: typing.Any, status: str, eligible: bool -) -> None: - """Tentative start/end dates support lookup and links; approximate ranges don't.""" - start, end = date(2099, 6, 2), date(2099, 6, 4) - dates: StrDict = {"status": status} - dates.update( - {"earliest": start, "latest": end} - if status == "approximate" - else {"start": start, "end": end} - ) - conf: StrDict = { - "name": "PyCon test", - "location": "Bologna", - "country": "it", - "dates": dates, - } - (tmp_path / "conferences.yaml").write_text(yaml.safe_dump([conf])) - monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path)) - monkeypatch.setitem(web_view.app.config, "DATA_DIR", str(tmp_path)) - monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: []) - calls: list[tuple[date, date, str]] = [] - - def lookup( - path: Path, departure: date, returning: date, destination: str, short_haul: bool - ) -> StrDict: - calls.append((departure, returning, destination)) - return {} - - monkeypatch.setattr(flights, "lookup", lookup) - url = "/conference/2099-06-02/" + detail.conference_slug(conf) - with web_view.app.test_client() as client: - response = client.get(url) - assert response.status_code == 200 - assert (b"Look up flights" in response.data) == eligible - assert (b"Open in Google Flights" in response.data) == eligible - assert (b"Using tentative conference dates" in response.data) == ( - status == "tentative" - ) - if status == "tentative": - assert page.format_date(start).encode() in response.data - assert page.format_date(end).encode() in response.data - assert calls == [] - with client.session_transaction() as session: - token = session["conference_flight_token"] - response = client.post(url, data={"airport": "BLQ", "csrf_token": token}) - assert response.status_code == (302 if eligible else 400) - assert calls == ([(start, end, "BLQ")] if eligible else []) diff --git a/tests/test_conference_list.py b/tests/test_conference_list.py deleted file mode 100644 index 6de3ee6..0000000 --- a/tests/test_conference_list.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Tests for conference list date handling.""" - -from datetime import date -import typing -from types import SimpleNamespace - -import yaml - -import agenda.conference_list -import agenda.fx -import agenda.trip -import web_view - - -def test_build_conference_list_supports_inexact_dates(tmp_path: typing.Any) -> None: - """Conference list should include tentative and approximate dates.""" - conferences = [ - { - "name": "PyCascades 2027", - "series": "pycascades", - "topic": "Python", - "location": "TBC", - "dates": { - "status": "approximate", - "label": "March 2027", - "earliest": date(2027, 3, 1), - "latest": date(2027, 3, 31), - }, - }, - { - "name": "FOSDEM 2027", - "topic": "FOSDEM", - "location": "Brussels", - "dates": { - "status": "tentative", - "start": date(2027, 1, 30), - "end": date(2027, 1, 31), - "label": "likely first weekend of February 2027", - }, - }, - ] - (tmp_path / "conferences.yaml").write_text( - yaml.safe_dump(conferences), encoding="utf-8" - ) - (tmp_path / "conference_series.yaml").write_text( - yaml.safe_dump( - { - "pycascades": { - "name": "PyCascades", - "topic": "Python", - "cadence": "annual", - "url": "https://pycascades.com/", - } - } - ), - encoding="utf-8", - ) - - items = agenda.conference_list.build_conference_list(str(tmp_path), []) - - assert [item["name"] for item in items] == ["FOSDEM 2027", "PyCascades 2027"] - assert items[0]["date_status"] == "tentative" - assert items[0]["display_date"] == "likely first weekend of February 2027" - assert items[0]["sort_date"] == date(2027, 1, 30) - assert items[1]["date_status"] == "approximate" - assert items[1]["display_date"] == "March 2027" - assert items[1]["latest_date"] == date(2027, 3, 31) - assert items[1]["series_detail"]["name"] == "PyCascades" - - -def test_conference_series_pages(tmp_path: typing.Any, monkeypatch: typing.Any) -> None: - """Series index and detail pages should render linked conferences.""" - conferences = [ - { - "name": "PyCascades 2027", - "series": "pycascades", - "topic": "Python", - "location": "TBC", - "trip": date(2027, 3, 1), - "dates": { - "status": "exact", - "start": date(2027, 3, 5), - "end": date(2027, 3, 6), - }, - "going": True, - } - ] - series = { - "pycascades": { - "name": "PyCascades", - "topic": "Python", - "cadence": "annual", - "url": "https://pycascades.com/", - } - } - (tmp_path / "conferences.yaml").write_text( - yaml.safe_dump(conferences), encoding="utf-8" - ) - (tmp_path / "conference_series.yaml").write_text( - yaml.safe_dump(series), encoding="utf-8" - ) - - monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path)) - fake_trip = SimpleNamespace( - start=date(2027, 3, 1), - title="Seattle Python trip", - conferences=[{"start": date(2027, 3, 5), "name": "PyCascades 2027"}], - ) - monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: [fake_trip]) - - web_view.app.config["TESTING"] = True - with web_view.app.test_client() as client: - index_response = client.get("/conference/series") - detail_response = client.get("/conference/series/pycascades") - - assert index_response.status_code == 200 - assert b"PyCascades" in index_response.data - assert b"attended" in index_response.data - assert detail_response.status_code == 200 - assert b"trip: Seattle Python trip" in detail_response.data - - -def test_conference_page_filters_by_country( - tmp_path: typing.Any, monkeypatch: typing.Any -) -> None: - """Conference page should filter upcoming conferences by country code.""" - conferences = [ - { - "name": "UK Mapping Conf 2099", - "topic": "Maps", - "location": "London", - "country": "GB", - "start": date(2099, 5, 1), - "end": date(2099, 5, 2), - }, - { - "name": "US Python Conf 2099", - "topic": "Python", - "location": "Pittsburgh", - "country": "US", - "start": date(2099, 6, 1), - "end": date(2099, 6, 2), - }, - ] - (tmp_path / "conferences.yaml").write_text( - yaml.safe_dump(conferences), encoding="utf-8" - ) - - monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path)) - monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: []) - monkeypatch.setattr(agenda.fx, "get_rates", lambda config: {}) - - web_view.app.config["TESTING"] = True - with web_view.app.test_client() as client: - response = client.get("/conference?country=gb") - - assert response.status_code == 200 - assert b"UK Mapping Conf 2099" in response.data - assert b"US Python Conf 2099" not in response.data - assert b'