From 2a5e6b71e318d8aa533e533e3a58ec98d6953bf9 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Sat, 11 May 2024 13:46:21 -0700 Subject: [PATCH] Clean up --- aerodata/__init__.py | 26 ++++++++----- aerodata/fetch.py | 81 +++++++++++++++++++++------------------ aerodata/gunicorn.conf.py | 2 - aerodata/query.py | 8 ++-- 4 files changed, 64 insertions(+), 53 deletions(-) diff --git a/aerodata/__init__.py b/aerodata/__init__.py index a9249f4..84c0a2f 100644 --- a/aerodata/__init__.py +++ b/aerodata/__init__.py @@ -1,6 +1,6 @@ -import multiprocessing - from gevent import monkey +from requests import HTTPError + monkey.patch_all() import flask @@ -11,22 +11,28 @@ webapp = flask.Flask(__name__) -lock = multiprocessing.RLock() - - @webapp.route("/aerodromes") def get_aerodromes(): try: - query = AerodromeQueryParams.from_dict(flask.request.args) + query_params = AerodromeQueryParams.from_dict(flask.request.args) except ValueError as e: - return f"Error parsing query parameters: {str(e)}" + return f"Error parsing query parameters: {str(e)}", 400 - features = get_features() - feature_collection = select_features(features, query) + try: + features = get_features() + except HTTPError as e: + return f"Error fetching features from source: {str(e)}", 500 + except ValueError as e: + return f"Error processing source data: {str(e)}", 500 + + try: + feature_collection = select_features(features, query_params) + except ValueError as e: + return f"Error selecting features: {str(e)}", 400 return flask.jsonify(feature_collection) @webapp.route("/status") def status(): - return "Ok" + return "Ok\n" diff --git a/aerodata/fetch.py b/aerodata/fetch.py index 113acd2..5f95b50 100644 --- a/aerodata/fetch.py +++ b/aerodata/fetch.py @@ -10,12 +10,13 @@ raw_data_lock = multiprocessing.RLock() -full_data_lock = multiprocessing.RLock() CACHE_PATH = ".cache" MAX_AGE = timedelta(hours=6000) +RUNWAYS_URL = "https://opendata.arcgis.com/api/v3/datasets/4d8fa46181aa470d809776c57a8ab1f6_0/downloads/data?format=geojson&spatialRefId=4326&where=1%3D1" RUNWAYS_FILENAME = "Runways.geojson" -FEATURES_FILENAME = "Features.geojson" +AIRPORTS_URL = "https://opendata.arcgis.com/api/v3/datasets/e747ab91a11045e8b3f8a3efd093d3b5_0/downloads/data?format=geojson&spatialRefId=4326&where=1%3D1" AIRPORTS_FILENAME = "Airports.geosjon" +FEATURES_FILENAME = "Features.geojson" EARTH_CIRCUMFERENCE_FT = 131482560 RUNWAY_WIDTH_TOLERANCE = 0.2 # fraction RUNWAY_HEADING_TOLERANCE = 30 # degrees @@ -61,44 +62,44 @@ } -def _heading_of(name: str) -> float: - if name in RUNWAY_HEADINGS: - return RUNWAY_HEADINGS[name] - if name[-1] not in "0123456789": - name = name[0:-1] - if name in RUNWAY_HEADINGS: - return RUNWAY_HEADINGS[name] - if len(name) == 2: - return float(name) * 10 - elif len(name) == 3: - return float(name) +def _heading_of(runway_name: str) -> float: + if runway_name in RUNWAY_HEADINGS: + return RUNWAY_HEADINGS[runway_name] + if runway_name[-1] not in "0123456789": + runway_name = runway_name[0:-1] + if runway_name in RUNWAY_HEADINGS: + return RUNWAY_HEADINGS[runway_name] + if len(runway_name) == 2: + return float(runway_name) * 10 + elif len(runway_name) == 3: + return float(runway_name) else: - raise ValueError(f"Could not determine heading of runway `{name}`") + raise ValueError(f"Could not determine heading of runway `{runway_name}`") -def _reciprocal_runway(name: str) -> str: - if name in RECIPROCAL_SUFFIXES: - return RECIPROCAL_SUFFIXES[name] - if name[-1] not in "0123456789": - suffix = name[-1] +def _reciprocal_runway(runway_name: str) -> str: + if runway_name in RECIPROCAL_SUFFIXES: + return RECIPROCAL_SUFFIXES[runway_name] + if runway_name[-1] not in "0123456789": + suffix = runway_name[-1] if suffix not in RECIPROCAL_SUFFIXES: - raise ValueError(f"Cannot determine reciprocal suffix for runway `{name}`") + raise ValueError(f"Cannot determine reciprocal suffix for runway `{runway_name}`") suffix = RECIPROCAL_SUFFIXES[suffix] - name = name[0:-1] + runway_name = runway_name[0:-1] else: suffix = "" - if name in RECIPROCAL_SUFFIXES: - return RECIPROCAL_SUFFIXES[name] + suffix - if len(name) == 2: - heading = int(name) * 10 - elif len(name) == 3: - heading = int(name) + if runway_name in RECIPROCAL_SUFFIXES: + return RECIPROCAL_SUFFIXES[runway_name] + suffix + if len(runway_name) == 2: + heading = int(runway_name) * 10 + elif len(runway_name) == 3: + heading = int(runway_name) else: - raise ValueError(f"Could not determine reciprocal runway for `{name}`") + raise ValueError(f"Could not determine reciprocal runway for `{runway_name}`") heading = (heading + 180) % 360 - if len(name) == 2: + if len(runway_name) == 2: return f"{round(heading / 10):02d}{suffix}" - elif len(name) == 3: + elif len(runway_name) == 3: return f"{heading:03d}{suffix}" else: raise RuntimeError("Impossible logic reached") @@ -128,15 +129,19 @@ def _unflatten(xy: list[tuple[float, float]], lat0: float, lng0: float) -> list[ return unflattened -def _angular_distance(a1: float, a2: float) -> float: - candidates = [a2 - a1, a2 + 360 - a1, a2 - 360 - a1] +def _angular_distance(a1_deg: float, a2_deg: float) -> float: + candidates = [a2_deg - a1_deg, a2_deg + 360 - a1_deg, a2_deg - 360 - a1_deg] return min(abs(c) for c in candidates) -def get_features(): - """ +def get_features() -> list[dict]: + """Get Features in API format. + + When necessary, download the source data for runways and airports to cache files. + + When necessary, regenerate features cache file. Otherwise, read featuers from cache file. - Returns: + Returns: List of GeoJSON Features compliant with the API. Raises: * HTTPError for non-successful retrieval status @@ -151,7 +156,7 @@ def get_features(): runways_path = os.path.join(CACHE_PATH, RUNWAYS_FILENAME) if not os.path.exists(runways_path) or (datetime.utcnow() - datetime.fromtimestamp(os.path.getmtime(runways_path))) > MAX_AGE: logger.debug(f"Downloading {RUNWAYS_FILENAME}") - resp = requests.get("https://opendata.arcgis.com/api/v3/datasets/4d8fa46181aa470d809776c57a8ab1f6_0/downloads/data?format=geojson&spatialRefId=4326&where=1%3D1") + resp = requests.get(RUNWAYS_URL) resp.raise_for_status() runways = resp.json() with open(runways_path, "w") as f: @@ -165,7 +170,7 @@ def get_features(): airports_path = os.path.join(CACHE_PATH, AIRPORTS_FILENAME) if not os.path.exists(airports_path) or (datetime.utcnow() - datetime.fromtimestamp(os.path.getmtime(airports_path))) > MAX_AGE: logger.debug(f"Downloading {AIRPORTS_FILENAME}") - resp = requests.get("https://opendata.arcgis.com/api/v3/datasets/e747ab91a11045e8b3f8a3efd093d3b5_0/downloads/data?format=geojson&spatialRefId=4326&where=1%3D1") + resp = requests.get(AIRPORTS_URL) resp.raise_for_status() airports = resp.json() with open(airports_path, "w") as f: @@ -177,9 +182,11 @@ def get_features(): airports = json.load(f) if os.path.exists(features_path): + # Use cached Features.geojson with open(features_path, "r") as f: features = json.load(f) else: + # Features.geojson isn't present; generate it from airports + runways features = [] # Process airports diff --git a/aerodata/gunicorn.conf.py b/aerodata/gunicorn.conf.py index 2c7a591..38fa608 100644 --- a/aerodata/gunicorn.conf.py +++ b/aerodata/gunicorn.conf.py @@ -6,8 +6,6 @@ from gunicorn.workers.base import Worker from loguru import logger -from aerodata import webapp - def on_starting(server: Arbiter): """gunicorn server hook called just before master process is initialized.""" diff --git a/aerodata/query.py b/aerodata/query.py index 0dbb2ba..596c806 100644 --- a/aerodata/query.py +++ b/aerodata/query.py @@ -72,13 +72,13 @@ def from_dict(params: dict) -> AerodromeQueryParams: def select_features(all_features: dict, query: AerodromeQueryParams) -> dict: - """ + """Filter the provided features and return a FeatureCollection with selected features. Args: - all_features: - query: + all_features: Entire set of features that could be returned. + query: Query parameters indicating which features to select. - Returns: + Returns: GeoJSON FeatureCollection compliant with the API. Raises: * ValueError when page_token is invalid