From 8f0e98e8813eb4aaed284ae708fd28291c93aca7 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 21 Oct 2025 13:57:16 +0200 Subject: [PATCH 01/17] Add make_wsgi_app function --- pygeoapi/config.py | 9 +- pygeoapi/flask_app.py | 902 +++++++++++++++++++++--------------------- pygeoapi/openapi.py | 13 +- 3 files changed, 473 insertions(+), 451 deletions(-) diff --git a/pygeoapi/config.py b/pygeoapi/config.py index 88f50e74b..0a6a1b129 100644 --- a/pygeoapi/config.py +++ b/pygeoapi/config.py @@ -41,7 +41,7 @@ LOGGER = logging.getLogger(__name__) -def get_config(raw: bool = False) -> dict: +def get_config(config_location: str = None, raw: bool = False) -> dict: """ Get pygeoapi configurations @@ -50,10 +50,13 @@ def get_config(raw: bool = False) -> dict: :returns: `dict` of pygeoapi configuration """ - if not os.environ.get('PYGEOAPI_CONFIG'): + if config_location is None: + config_location = os.environ.get('PYGEOAPI_CONFIG') + + if config_location is None: raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') - with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh: + with open(config_location, encoding='utf8') as fh: if raw: CONFIG = yaml.safe_load(fh) else: diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index a0cc2ea16..d17accc25 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -51,608 +51,628 @@ from pygeoapi.util import get_mimetype, get_api_rules -CONFIG = get_config() -OPENAPI = load_openapi_document() +# NOTE: made a function to return a WSGI application, passing the locations for the config and +# openapi files as variables, instead as environment variables +def make_wsgi_app(config_location: str, openapi_location: str) -> Flask: + """ + Create a WSGI application + Args: + config_location (str): location of the pygeoapi config file + openapi_location (str): location of the OpenAPI document file + + Returns: + Flask WSGI application + """ + CONFIG = get_config(config_location=config_location) + OPENAPI = load_openapi_document(openapi_location=openapi_location) + + API_RULES = get_api_rules(CONFIG) + + if CONFIG['server'].get('admin'): + import pygeoapi.api.admin as admin_api + from pygeoapi.api.admin import Admin + + STATIC_FOLDER = 'static' + if 'templates' in CONFIG['server']: + STATIC_FOLDER = CONFIG['server']['templates'].get('static', 'static') + + APP = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path='/static') + APP.url_map.strict_slashes = API_RULES.strict_slashes + + BLUEPRINT = Blueprint( + 'pygeoapi', + __name__, + static_folder=STATIC_FOLDER, + url_prefix=API_RULES.get_url_prefix('flask') + ) + ADMIN_BLUEPRINT = Blueprint( + 'admin', + __name__, + static_folder=STATIC_FOLDER, + url_prefix=API_RULES.get_url_prefix('flask') + ) -API_RULES = get_api_rules(CONFIG) + # CORS: optionally enable from config. + if CONFIG['server'].get('cors', False): + try: + from flask_cors import CORS + CORS(APP, CORS_EXPOSE_HEADERS=['*']) + except ModuleNotFoundError: + print('Python package flask-cors required for CORS support') -if CONFIG['server'].get('admin'): - import pygeoapi.api.admin as admin_api - from pygeoapi.api.admin import Admin + APP.config['JSONIFY_PRETTYPRINT_REGULAR'] = CONFIG['server'].get( + 'pretty_print', True) -STATIC_FOLDER = 'static' -if 'templates' in CONFIG['server']: - STATIC_FOLDER = CONFIG['server']['templates'].get('static', 'static') + api_ = API(CONFIG, OPENAPI) -APP = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path='/static') -APP.url_map.strict_slashes = API_RULES.strict_slashes + OGC_SCHEMAS_LOCATION = CONFIG['server'].get('ogc_schemas_location') -BLUEPRINT = Blueprint( - 'pygeoapi', - __name__, - static_folder=STATIC_FOLDER, - url_prefix=API_RULES.get_url_prefix('flask') -) -ADMIN_BLUEPRINT = Blueprint( - 'admin', - __name__, - static_folder=STATIC_FOLDER, - url_prefix=API_RULES.get_url_prefix('flask') -) + if (OGC_SCHEMAS_LOCATION is not None and + not OGC_SCHEMAS_LOCATION.startswith('http')): + # serve the OGC schemas locally -# CORS: optionally enable from config. -if CONFIG['server'].get('cors', False): - try: - from flask_cors import CORS - CORS(APP, CORS_EXPOSE_HEADERS=['*']) - except ModuleNotFoundError: - print('Python package flask-cors required for CORS support') + if not os.path.exists(OGC_SCHEMAS_LOCATION): + raise RuntimeError('OGC schemas misconfigured') -APP.config['JSONIFY_PRETTYPRINT_REGULAR'] = CONFIG['server'].get( - 'pretty_print', True) + @BLUEPRINT.route('/schemas/', methods=['GET']) + def schemas(path): + """ + Serve OGC schemas locally -api_ = API(CONFIG, OPENAPI) + :param path: path of the OGC schema document -OGC_SCHEMAS_LOCATION = CONFIG['server'].get('ogc_schemas_location') + :returns: HTTP response + """ -if (OGC_SCHEMAS_LOCATION is not None and - not OGC_SCHEMAS_LOCATION.startswith('http')): - # serve the OGC schemas locally + full_filepath = os.path.join(OGC_SCHEMAS_LOCATION, path) + dirname_ = os.path.dirname(full_filepath) + basename_ = os.path.basename(full_filepath) - if not os.path.exists(OGC_SCHEMAS_LOCATION): - raise RuntimeError('OGC schemas misconfigured') + path_ = dirname_.replace('..', '').replace('//', '').replace('./', '') - @BLUEPRINT.route('/schemas/', methods=['GET']) - def schemas(path): - """ - Serve OGC schemas locally + if '..' in path_: + return 'Invalid path', 400 - :param path: path of the OGC schema document - - :returns: HTTP response - """ + return send_from_directory(path_, basename_, + mimetype=get_mimetype(basename_)) - full_filepath = os.path.join(OGC_SCHEMAS_LOCATION, path) - dirname_ = os.path.dirname(full_filepath) - basename_ = os.path.basename(full_filepath) - path_ = dirname_.replace('..', '').replace('//', '').replace('./', '') + def execute_from_flask(api_function, request: Request, *args, + skip_valid_check=False, + alternative_api=None + ) -> Response: + """ + Executes API function from Flask - if '..' in path_: - return 'Invalid path', 400 + :param api_function: API function + :param request: request object + :param *args: variable length additional arguments + :param skip_validity_check: bool + :param alternative_api: specify custom api instance such as Admin - return send_from_directory(path_, basename_, - mimetype=get_mimetype(basename_)) + :returns: A Response instance + """ + actual_api = api_ if alternative_api is None else alternative_api -def execute_from_flask(api_function, request: Request, *args, - skip_valid_check=False, - alternative_api=None - ) -> Response: - """ - Executes API function from Flask + api_request = APIRequest.from_flask(request, actual_api.locales) - :param api_function: API function - :param request: request object - :param *args: variable length additional arguments - :param skip_validity_check: bool - :param alternative_api: specify custom api instance such as Admin + content: Union[str, bytes] - :returns: A Response instance - """ + if not skip_valid_check and not api_request.is_valid(): + headers, status, content = actual_api.get_format_exception(api_request) + else: + headers, status, content = api_function(actual_api, api_request, *args) + content = apply_gzip(headers, content) - actual_api = api_ if alternative_api is None else alternative_api + response = make_response(content, status) - api_request = APIRequest.from_flask(request, actual_api.locales) + if headers: + response.headers = headers + return response - content: Union[str, bytes] - if not skip_valid_check and not api_request.is_valid(): - headers, status, content = actual_api.get_format_exception(api_request) - else: - headers, status, content = api_function(actual_api, api_request, *args) - content = apply_gzip(headers, content) + @BLUEPRINT.route('/') + def landing_page(): + """ + OGC API landing page endpoint - response = make_response(content, status) + :returns: HTTP response + """ + return execute_from_flask(core_api.landing_page, request) - if headers: - response.headers = headers - return response + @BLUEPRINT.route('/openapi') + def openapi(): + """ + OpenAPI endpoint -@BLUEPRINT.route('/') -def landing_page(): - """ - OGC API landing page endpoint + :returns: HTTP response + """ - :returns: HTTP response - """ - return execute_from_flask(core_api.landing_page, request) + return execute_from_flask(core_api.openapi_, request) -@BLUEPRINT.route('/openapi') -def openapi(): - """ - OpenAPI endpoint + @BLUEPRINT.route('/conformance') + def conformance(): + """ + OGC API conformance endpoint - :returns: HTTP response - """ + :returns: HTTP response + """ - return execute_from_flask(core_api.openapi_, request) + return execute_from_flask(core_api.conformance, request) -@BLUEPRINT.route('/conformance') -def conformance(): - """ - OGC API conformance endpoint + @BLUEPRINT.route('/TileMatrixSets/') + def get_tilematrix_set(tileMatrixSetId=None): + """ + OGC API TileMatrixSet endpoint - :returns: HTTP response - """ + :param tileMatrixSetId: identifier of tile matrix set - return execute_from_flask(core_api.conformance, request) + :returns: HTTP response + """ + return execute_from_flask(tiles_api.tilematrixset, request, + tileMatrixSetId) -@BLUEPRINT.route('/TileMatrixSets/') -def get_tilematrix_set(tileMatrixSetId=None): - """ - OGC API TileMatrixSet endpoint - :param tileMatrixSetId: identifier of tile matrix set + @BLUEPRINT.route('/TileMatrixSets') + def get_tilematrix_sets(): + """ + OGC API TileMatrixSets endpoint - :returns: HTTP response - """ + :returns: HTTP response + """ - return execute_from_flask(tiles_api.tilematrixset, request, - tileMatrixSetId) + return execute_from_flask(tiles_api.tilematrixsets, request) -@BLUEPRINT.route('/TileMatrixSets') -def get_tilematrix_sets(): - """ - OGC API TileMatrixSets endpoint + @BLUEPRINT.route('/collections') + @BLUEPRINT.route('/collections/') + def collections(collection_id=None): + """ + OGC API collections endpoint - :returns: HTTP response - """ + :param collection_id: collection identifier - return execute_from_flask(tiles_api.tilematrixsets, request) + :returns: HTTP response + """ + return execute_from_flask(core_api.describe_collections, request, + collection_id) -@BLUEPRINT.route('/collections') -@BLUEPRINT.route('/collections/') -def collections(collection_id=None): - """ - OGC API collections endpoint - :param collection_id: collection identifier + @BLUEPRINT.route('/collections//schema') + def collection_schema(collection_id): + """ + OGC API - collections schema endpoint - :returns: HTTP response - """ + :param collection_id: collection identifier - return execute_from_flask(core_api.describe_collections, request, - collection_id) + :returns: HTTP response + """ + return execute_from_flask(core_api.get_collection_schema, request, + collection_id) -@BLUEPRINT.route('/collections//schema') -def collection_schema(collection_id): - """ - OGC API - collections schema endpoint - :param collection_id: collection identifier + @BLUEPRINT.route('/collections//queryables') + def collection_queryables(collection_id=None): + """ + OGC API collections queryables endpoint - :returns: HTTP response - """ + :param collection_id: collection identifier - return execute_from_flask(core_api.get_collection_schema, request, - collection_id) + :returns: HTTP response + """ + return execute_from_flask(itemtypes_api.get_collection_queryables, request, + collection_id) -@BLUEPRINT.route('/collections//queryables') -def collection_queryables(collection_id=None): - """ - OGC API collections queryables endpoint - :param collection_id: collection identifier + @BLUEPRINT.route('/collections//items', + methods=['GET', 'POST', 'OPTIONS'], + provide_automatic_options=False) + @BLUEPRINT.route('/collections//items/', + methods=['GET', 'PUT', 'DELETE', 'OPTIONS'], + provide_automatic_options=False) + def collection_items(collection_id, item_id=None): + """ + OGC API collections items endpoint - :returns: HTTP response - """ + :param collection_id: collection identifier + :param item_id: item identifier - return execute_from_flask(itemtypes_api.get_collection_queryables, request, - collection_id) + :returns: HTTP response + """ + if item_id is None: + if request.method == 'POST': # filter or manage items + if request.content_type is not None: + if request.content_type == 'application/geo+json': + return execute_from_flask( + itemtypes_api.manage_collection_item, + request, 'create', collection_id, + skip_valid_check=True) + else: + return execute_from_flask( + itemtypes_api.get_collection_items, request, + collection_id, skip_valid_check=True) + elif request.method == 'OPTIONS': + return execute_from_flask( + itemtypes_api.manage_collection_item, request, 'options', + collection_id, skip_valid_check=True) + else: # GET: list items + return execute_from_flask(itemtypes_api.get_collection_items, + request, collection_id, + skip_valid_check=True) + + elif request.method == 'DELETE': + return execute_from_flask(itemtypes_api.manage_collection_item, + request, 'delete', collection_id, item_id, + skip_valid_check=True) + elif request.method == 'PUT': + return execute_from_flask(itemtypes_api.manage_collection_item, + request, 'update', collection_id, item_id, + skip_valid_check=True) + elif request.method == 'OPTIONS': + return execute_from_flask(itemtypes_api.manage_collection_item, + request, 'options', collection_id, item_id, + skip_valid_check=True) + else: + return execute_from_flask(itemtypes_api.get_collection_item, request, + collection_id, item_id) -@BLUEPRINT.route('/collections//items', - methods=['GET', 'POST', 'OPTIONS'], - provide_automatic_options=False) -@BLUEPRINT.route('/collections//items/', - methods=['GET', 'PUT', 'DELETE', 'OPTIONS'], - provide_automatic_options=False) -def collection_items(collection_id, item_id=None): - """ - OGC API collections items endpoint - :param collection_id: collection identifier - :param item_id: item identifier + @BLUEPRINT.route('/collections//coverage') + def collection_coverage(collection_id): + """ + OGC API - Coverages coverage endpoint - :returns: HTTP response - """ + :param collection_id: collection identifier - if item_id is None: - if request.method == 'POST': # filter or manage items - if request.content_type is not None: - if request.content_type == 'application/geo+json': - return execute_from_flask( - itemtypes_api.manage_collection_item, - request, 'create', collection_id, - skip_valid_check=True) - else: - return execute_from_flask( - itemtypes_api.get_collection_items, request, - collection_id, skip_valid_check=True) - elif request.method == 'OPTIONS': - return execute_from_flask( - itemtypes_api.manage_collection_item, request, 'options', - collection_id, skip_valid_check=True) - else: # GET: list items - return execute_from_flask(itemtypes_api.get_collection_items, - request, collection_id, - skip_valid_check=True) - - elif request.method == 'DELETE': - return execute_from_flask(itemtypes_api.manage_collection_item, - request, 'delete', collection_id, item_id, - skip_valid_check=True) - elif request.method == 'PUT': - return execute_from_flask(itemtypes_api.manage_collection_item, - request, 'update', collection_id, item_id, - skip_valid_check=True) - elif request.method == 'OPTIONS': - return execute_from_flask(itemtypes_api.manage_collection_item, - request, 'options', collection_id, item_id, - skip_valid_check=True) - else: - return execute_from_flask(itemtypes_api.get_collection_item, request, - collection_id, item_id) - - -@BLUEPRINT.route('/collections//coverage') -def collection_coverage(collection_id): - """ - OGC API - Coverages coverage endpoint + :returns: HTTP response + """ - :param collection_id: collection identifier + return execute_from_flask(coverages_api.get_collection_coverage, request, + collection_id, skip_valid_check=True) - :returns: HTTP response - """ - return execute_from_flask(coverages_api.get_collection_coverage, request, - collection_id, skip_valid_check=True) + @BLUEPRINT.route('/collections//tiles') + def get_collection_tiles(collection_id=None): + """ + OGC open api collections tiles access point + :param collection_id: collection identifier -@BLUEPRINT.route('/collections//tiles') -def get_collection_tiles(collection_id=None): - """ - OGC open api collections tiles access point + :returns: HTTP response + """ - :param collection_id: collection identifier + return execute_from_flask(tiles_api.get_collection_tiles, request, + collection_id) - :returns: HTTP response - """ - return execute_from_flask(tiles_api.get_collection_tiles, request, - collection_id) + @BLUEPRINT.route('/collections//tiles/') + @BLUEPRINT.route('/collections//tiles//metadata') # noqa + def get_collection_tiles_metadata(collection_id=None, tileMatrixSetId=None): + """ + OGC open api collection tiles service metadata + :param collection_id: collection identifier + :param tileMatrixSetId: identifier of tile matrix set -@BLUEPRINT.route('/collections//tiles/') -@BLUEPRINT.route('/collections//tiles//metadata') # noqa -def get_collection_tiles_metadata(collection_id=None, tileMatrixSetId=None): - """ - OGC open api collection tiles service metadata + :returns: HTTP response + """ - :param collection_id: collection identifier - :param tileMatrixSetId: identifier of tile matrix set + return execute_from_flask(tiles_api.get_collection_tiles_metadata, + request, collection_id, tileMatrixSetId, + skip_valid_check=True) - :returns: HTTP response - """ - return execute_from_flask(tiles_api.get_collection_tiles_metadata, - request, collection_id, tileMatrixSetId, - skip_valid_check=True) + @BLUEPRINT.route('/collections//tiles/\ + ///') + def get_collection_tiles_data(collection_id=None, tileMatrixSetId=None, + tileMatrix=None, tileRow=None, tileCol=None): + """ + OGC open api collection tiles service data + :param collection_id: collection identifier + :param tileMatrixSetId: identifier of tile matrix set + :param tileMatrix: identifier of {z} matrix index + :param tileRow: identifier of {y} matrix index + :param tileCol: identifier of {x} matrix index -@BLUEPRINT.route('/collections//tiles/\ -///') -def get_collection_tiles_data(collection_id=None, tileMatrixSetId=None, - tileMatrix=None, tileRow=None, tileCol=None): - """ - OGC open api collection tiles service data + :returns: HTTP response + """ - :param collection_id: collection identifier - :param tileMatrixSetId: identifier of tile matrix set - :param tileMatrix: identifier of {z} matrix index - :param tileRow: identifier of {y} matrix index - :param tileCol: identifier of {x} matrix index + return execute_from_flask( + tiles_api.get_collection_tiles_data, + request, collection_id, tileMatrixSetId, tileMatrix, tileRow, tileCol, + skip_valid_check=True, + ) - :returns: HTTP response - """ - return execute_from_flask( - tiles_api.get_collection_tiles_data, - request, collection_id, tileMatrixSetId, tileMatrix, tileRow, tileCol, - skip_valid_check=True, - ) + @BLUEPRINT.route('/collections//map') + @BLUEPRINT.route('/collections//styles//map') + def collection_map(collection_id, style_id=None): + """ + OGC API - Maps map render endpoint + :param collection_id: collection identifier + :param style_id: style identifier -@BLUEPRINT.route('/collections//map') -@BLUEPRINT.route('/collections//styles//map') -def collection_map(collection_id, style_id=None): - """ - OGC API - Maps map render endpoint + :returns: HTTP response + """ - :param collection_id: collection identifier - :param style_id: style identifier + return execute_from_flask( + maps_api.get_collection_map, request, collection_id, style_id + ) - :returns: HTTP response - """ - return execute_from_flask( - maps_api.get_collection_map, request, collection_id, style_id - ) + @BLUEPRINT.route('/processes') + @BLUEPRINT.route('/processes/') + def get_processes(process_id=None): + """ + OGC API - Processes description endpoint + :param process_id: process identifier -@BLUEPRINT.route('/processes') -@BLUEPRINT.route('/processes/') -def get_processes(process_id=None): - """ - OGC API - Processes description endpoint + :returns: HTTP response + """ - :param process_id: process identifier + return execute_from_flask(processes_api.describe_processes, request, + process_id) - :returns: HTTP response - """ - return execute_from_flask(processes_api.describe_processes, request, - process_id) + @BLUEPRINT.route('/jobs') + @BLUEPRINT.route('/jobs/', + methods=['GET', 'DELETE']) + def get_jobs(job_id=None): + """ + OGC API - Processes jobs endpoint + :param job_id: job identifier -@BLUEPRINT.route('/jobs') -@BLUEPRINT.route('/jobs/', - methods=['GET', 'DELETE']) -def get_jobs(job_id=None): - """ - OGC API - Processes jobs endpoint + :returns: HTTP response + """ - :param job_id: job identifier + if job_id is None: + return execute_from_flask(processes_api.get_jobs, request) + else: + if request.method == 'DELETE': # dismiss job + return execute_from_flask(processes_api.delete_job, request, + job_id) + else: # Return status of a specific job + return execute_from_flask(processes_api.get_jobs, request, job_id) - :returns: HTTP response - """ - if job_id is None: - return execute_from_flask(processes_api.get_jobs, request) - else: - if request.method == 'DELETE': # dismiss job - return execute_from_flask(processes_api.delete_job, request, - job_id) - else: # Return status of a specific job - return execute_from_flask(processes_api.get_jobs, request, job_id) + @BLUEPRINT.route('/processes//execution', methods=['POST']) + def execute_process_jobs(process_id): + """ + OGC API - Processes execution endpoint + :param process_id: process identifier -@BLUEPRINT.route('/processes//execution', methods=['POST']) -def execute_process_jobs(process_id): - """ - OGC API - Processes execution endpoint + :returns: HTTP response + """ - :param process_id: process identifier + return execute_from_flask(processes_api.execute_process, request, + process_id) - :returns: HTTP response - """ - return execute_from_flask(processes_api.execute_process, request, - process_id) + @BLUEPRINT.route('/jobs//results', + methods=['GET']) + def get_job_result(job_id=None): + """ + OGC API - Processes job result endpoint + :param job_id: job identifier -@BLUEPRINT.route('/jobs//results', - methods=['GET']) -def get_job_result(job_id=None): - """ - OGC API - Processes job result endpoint + :returns: HTTP response + """ - :param job_id: job identifier + return execute_from_flask(processes_api.get_job_result, request, job_id) + + + @BLUEPRINT.route('/collections//position') + @BLUEPRINT.route('/collections//area') + @BLUEPRINT.route('/collections//cube') + @BLUEPRINT.route('/collections//radius') + @BLUEPRINT.route('/collections//trajectory') + @BLUEPRINT.route('/collections//corridor') + @BLUEPRINT.route('/collections//locations/') + @BLUEPRINT.route('/collections//locations') + @BLUEPRINT.route('/collections//instances//position') # noqa + @BLUEPRINT.route('/collections//instances//area') # noqa + @BLUEPRINT.route('/collections//instances//cube') # noqa + @BLUEPRINT.route('/collections//instances//radius') # noqa + @BLUEPRINT.route('/collections//instances//trajectory') # noqa + @BLUEPRINT.route('/collections//instances//corridor') # noqa + @BLUEPRINT.route('/collections//instances//locations/') # noqa + @BLUEPRINT.route('/collections//instances//locations') # noqa + @BLUEPRINT.route('/collections//instances/') + @BLUEPRINT.route('/collections//instances') + def get_collection_edr_query(collection_id, instance_id=None, + location_id=None): + """ + OGC EDR API endpoints - :returns: HTTP response - """ + :param collection_id: collection identifier + :param instance_id: instance identifier + :param location_id: location id of a /locations/ query - return execute_from_flask(processes_api.get_job_result, request, job_id) - - -@BLUEPRINT.route('/collections//position') -@BLUEPRINT.route('/collections//area') -@BLUEPRINT.route('/collections//cube') -@BLUEPRINT.route('/collections//radius') -@BLUEPRINT.route('/collections//trajectory') -@BLUEPRINT.route('/collections//corridor') -@BLUEPRINT.route('/collections//locations/') -@BLUEPRINT.route('/collections//locations') -@BLUEPRINT.route('/collections//instances//position') # noqa -@BLUEPRINT.route('/collections//instances//area') # noqa -@BLUEPRINT.route('/collections//instances//cube') # noqa -@BLUEPRINT.route('/collections//instances//radius') # noqa -@BLUEPRINT.route('/collections//instances//trajectory') # noqa -@BLUEPRINT.route('/collections//instances//corridor') # noqa -@BLUEPRINT.route('/collections//instances//locations/') # noqa -@BLUEPRINT.route('/collections//instances//locations') # noqa -@BLUEPRINT.route('/collections//instances/') -@BLUEPRINT.route('/collections//instances') -def get_collection_edr_query(collection_id, instance_id=None, - location_id=None): - """ - OGC EDR API endpoints + :returns: HTTP response + """ - :param collection_id: collection identifier - :param instance_id: instance identifier - :param location_id: location id of a /locations/ query + if (request.path.endswith('instances') or + (instance_id is not None and + request.path.endswith(instance_id))): + return execute_from_flask( + edr_api.get_collection_edr_instances, request, collection_id, + instance_id + ) - :returns: HTTP response - """ + if location_id: + query_type = 'locations' + else: + query_type = request.path.split('/')[-1] - if (request.path.endswith('instances') or - (instance_id is not None and - request.path.endswith(instance_id))): return execute_from_flask( - edr_api.get_collection_edr_instances, request, collection_id, - instance_id + edr_api.get_collection_edr_query, request, collection_id, instance_id, + query_type, location_id, skip_valid_check=True ) - if location_id: - query_type = 'locations' - else: - query_type = request.path.split('/')[-1] - return execute_from_flask( - edr_api.get_collection_edr_query, request, collection_id, instance_id, - query_type, location_id, skip_valid_check=True - ) + @BLUEPRINT.route('/stac-api') + def stac_landing_page(): + """ + STAC API landing page endpoint + :returns: HTTP response + """ -@BLUEPRINT.route('/stac-api') -def stac_landing_page(): - """ - STAC API landing page endpoint + return execute_from_flask(stac_api.landing_page, request) - :returns: HTTP response - """ - return execute_from_flask(stac_api.landing_page, request) + @BLUEPRINT.route('/stac-api/search', methods=['GET', 'POST']) + def stac_search(): + """ + STAC API search endpoint + :returns: HTTP response + """ -@BLUEPRINT.route('/stac-api/search', methods=['GET', 'POST']) -def stac_search(): - """ - STAC API search endpoint + return execute_from_flask(stac_api.search, request) - :returns: HTTP response - """ - return execute_from_flask(stac_api.search, request) + @BLUEPRINT.route('/stac') + def stac_catalog_root(): + """ + STAC root endpoint + :returns: HTTP response + """ -@BLUEPRINT.route('/stac') -def stac_catalog_root(): - """ - STAC root endpoint + return execute_from_flask(stac_api.get_stac_root, request) - :returns: HTTP response - """ - return execute_from_flask(stac_api.get_stac_root, request) + @BLUEPRINT.route('/stac/') + def stac_catalog_path(path): + """ + STAC path endpoint + :param path: path -@BLUEPRINT.route('/stac/') -def stac_catalog_path(path): - """ - STAC path endpoint + :returns: HTTP response + """ - :param path: path + return execute_from_flask(stac_api.get_stac_path, request, path) - :returns: HTTP response - """ - return execute_from_flask(stac_api.get_stac_path, request, path) + @ADMIN_BLUEPRINT.route('/admin/config', methods=['GET', 'PUT', 'PATCH']) + def admin_config(): + """ + Admin endpoint + :returns: HTTP response + """ -@ADMIN_BLUEPRINT.route('/admin/config', methods=['GET', 'PUT', 'PATCH']) -def admin_config(): - """ - Admin endpoint + if request.method == 'GET': + return execute_from_flask(admin_api.get_config_, request, + alternative_api=admin_) - :returns: HTTP response - """ + elif request.method == 'PUT': + return execute_from_flask(admin_api.put_config, request, + alternative_api=admin_) - if request.method == 'GET': - return execute_from_flask(admin_api.get_config_, request, - alternative_api=admin_) + elif request.method == 'PATCH': + return execute_from_flask(admin_api.patch_config, request, + alternative_api=admin_) - elif request.method == 'PUT': - return execute_from_flask(admin_api.put_config, request, - alternative_api=admin_) - elif request.method == 'PATCH': - return execute_from_flask(admin_api.patch_config, request, - alternative_api=admin_) + @ADMIN_BLUEPRINT.route('/admin/config/resources', methods=['GET', 'POST']) + def admin_config_resources(): + """ + Resources endpoint + :returns: HTTP response + """ -@ADMIN_BLUEPRINT.route('/admin/config/resources', methods=['GET', 'POST']) -def admin_config_resources(): - """ - Resources endpoint + if request.method == 'GET': + return execute_from_flask(admin_api.get_resources, request, + alternative_api=admin_) - :returns: HTTP response - """ + elif request.method == 'POST': + return execute_from_flask(admin_api.post_resource, request, + alternative_api=admin_) - if request.method == 'GET': - return execute_from_flask(admin_api.get_resources, request, - alternative_api=admin_) - elif request.method == 'POST': - return execute_from_flask(admin_api.post_resource, request, - alternative_api=admin_) + @ADMIN_BLUEPRINT.route( + '/admin/config/resources/', + methods=['GET', 'PUT', 'PATCH', 'DELETE']) + def admin_config_resource(resource_id): + """ + Resource endpoint + :returns: HTTP response + """ -@ADMIN_BLUEPRINT.route( - '/admin/config/resources/', - methods=['GET', 'PUT', 'PATCH', 'DELETE']) -def admin_config_resource(resource_id): - """ - Resource endpoint + if request.method == 'GET': + return execute_from_flask(admin_api.get_resource, request, + resource_id, + alternative_api=admin_) - :returns: HTTP response - """ + elif request.method == 'DELETE': + return execute_from_flask(admin_api.delete_resource, request, + resource_id, + alternative_api=admin_) - if request.method == 'GET': - return execute_from_flask(admin_api.get_resource, request, - resource_id, - alternative_api=admin_) + elif request.method == 'PUT': + return execute_from_flask(admin_api.put_resource, request, + resource_id, + alternative_api=admin_) - elif request.method == 'DELETE': - return execute_from_flask(admin_api.delete_resource, request, - resource_id, - alternative_api=admin_) + elif request.method == 'PATCH': + return execute_from_flask(admin_api.patch_resource, request, + resource_id, + alternative_api=admin_) - elif request.method == 'PUT': - return execute_from_flask(admin_api.put_resource, request, - resource_id, - alternative_api=admin_) - elif request.method == 'PATCH': - return execute_from_flask(admin_api.patch_resource, request, - resource_id, - alternative_api=admin_) + APP.register_blueprint(BLUEPRINT) + if CONFIG['server'].get('admin'): + admin_ = Admin(CONFIG, OPENAPI) + APP.register_blueprint(ADMIN_BLUEPRINT) -APP.register_blueprint(BLUEPRINT) + return APP -if CONFIG['server'].get('admin'): - admin_ = Admin(CONFIG, OPENAPI) - APP.register_blueprint(ADMIN_BLUEPRINT) +if os.environ.get('PYGEOAPI_CONFIG_VIA_ENV', 'true') == 'true': + APP = make_wsgi_app( + config_location=None, + openapi_location=None + ) -@click.command() -@click.pass_context -@click.option('--debug', '-d', default=False, is_flag=True, help='debug') -def serve(ctx, server=None, debug=False): - """ - Serve pygeoapi via Flask. Runs pygeoapi - as a flask server. Not recommend for production. - :param server: `string` of server type - :param debug: `bool` of whether to run in debug mode + @click.command() + @click.pass_context + @click.option('--debug', '-d', default=False, is_flag=True, help='debug') + def serve(ctx, server=None, debug=False): + """ + Serve pygeoapi via Flask. Runs pygeoapi + as a flask server. Not recommend for production. - :returns: void - """ + :param server: `string` of server type + :param debug: `bool` of whether to run in debug mode - # setup_logger(CONFIG['logging']) - APP.run(debug=True, host=api_.config['server']['bind']['host'], - port=api_.config['server']['bind']['port']) + :returns: void + """ + # setup_logger(CONFIG['logging']) + APP.run(debug=True, host=api_.config['server']['bind']['host'], + port=api_.config['server']['bind']['port']) if __name__ == '__main__': # run locally, for testing serve() diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index c3016c828..09b64db32 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -998,19 +998,18 @@ def generate_openapi_document(cfg_file: Union[Path, io.TextIOWrapper], return content -def load_openapi_document() -> dict: +def load_openapi_document(pygeoapi_openapi: str = None) -> dict: """ Open OpenAPI document from `PYGEOAPI_OPENAPI` environment variable :returns: `dict` of OpenAPI document """ - - pygeoapi_openapi = os.environ.get('PYGEOAPI_OPENAPI') - if pygeoapi_openapi is None: - msg = 'PYGEOAPI_OPENAPI environment not set' - LOGGER.error(msg) - raise RuntimeError(msg) + pygeoapi_openapi = os.getenv('PYGEOAPI_OPENAPI') + if pygeoapi_openapi is None: + msg = 'PYGEOAPI_OPENAPI environment not set' + LOGGER.error(msg) + raise RuntimeError(msg) if not os.path.exists(pygeoapi_openapi): msg = (f'OpenAPI document {pygeoapi_openapi} does not exist. ' From 6a6f70517cb9b9e597d6db6cf8fc293c1be367c0 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 21 Oct 2025 14:03:04 +0200 Subject: [PATCH 02/17] Refactor WSGI app configuration to use local variables instead of environment variables --- pygeoapi/flask_app.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index d17accc25..d4a8a7949 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -653,8 +653,7 @@ def admin_config_resource(resource_id): config_location=None, openapi_location=None ) - - + CONFIG = get_config() @click.command() @click.pass_context @@ -671,8 +670,8 @@ def serve(ctx, server=None, debug=False): """ # setup_logger(CONFIG['logging']) - APP.run(debug=True, host=api_.config['server']['bind']['host'], - port=api_.config['server']['bind']['port']) + APP.run(debug=True, host=CONFIG['server']['bind']['host'], + port=CONFIG['server']['bind']['port']) if __name__ == '__main__': # run locally, for testing serve() From f33b7c1d7b6d689adb40d5fb65284bda428fc9c1 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 21 Oct 2025 15:26:51 +0200 Subject: [PATCH 03/17] Enhance docstrings in config and openapi modules to clarify argument descriptions --- pygeoapi/config.py | 6 +++++- pygeoapi/openapi.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pygeoapi/config.py b/pygeoapi/config.py index 0a6a1b129..68971cb6f 100644 --- a/pygeoapi/config.py +++ b/pygeoapi/config.py @@ -45,7 +45,11 @@ def get_config(config_location: str = None, raw: bool = False) -> dict: """ Get pygeoapi configurations - :param raw: `bool` over interpolation during config loading + Args: + config_location: `str` of configuration file location, if `None` + will attempt to read from `PYGEOAPI_CONFIG` + environment variable + raw: `bool` over interpolation during config loading :returns: `dict` of pygeoapi configuration """ diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index 09b64db32..97ae5ae82 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -1001,6 +1001,10 @@ def generate_openapi_document(cfg_file: Union[Path, io.TextIOWrapper], def load_openapi_document(pygeoapi_openapi: str = None) -> dict: """ Open OpenAPI document from `PYGEOAPI_OPENAPI` environment variable + + Args: + pygeoapi_openapi: `str` of OpenAPI document filepath, if `None` will + attempt to read from `PYGEOAPI_OPENAPI` environment :returns: `dict` of OpenAPI document """ From 1f95b3c36b0a31321cd17b951de79464213893fb Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 21 Oct 2025 16:06:49 +0200 Subject: [PATCH 04/17] flake codestyle fixes --- pygeoapi/flask_app.py | 303 ++++++++++++++++++++------------------ pygeoapi/openapi.py | 2 +- tests/data/mysql_data.sql | 2 +- 3 files changed, 165 insertions(+), 142 deletions(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index d4a8a7949..2aaee2cde 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -51,7 +51,8 @@ from pygeoapi.util import get_mimetype, get_api_rules -# NOTE: made a function to return a WSGI application, passing the locations for the config and +# Function to return a WSGI application, +# passing the locations for the config and # openapi files as variables, instead as environment variables def make_wsgi_app(config_location: str, openapi_location: str) -> Flask: """ @@ -59,7 +60,7 @@ def make_wsgi_app(config_location: str, openapi_location: str) -> Flask: Args: config_location (str): location of the pygeoapi config file openapi_location (str): location of the OpenAPI document file - + Returns: Flask WSGI application """ @@ -76,7 +77,12 @@ def make_wsgi_app(config_location: str, openapi_location: str) -> Flask: if 'templates' in CONFIG['server']: STATIC_FOLDER = CONFIG['server']['templates'].get('static', 'static') - APP = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path='/static') + APP = Flask( + __name__, + static_folder=STATIC_FOLDER, + static_url_path='/static' + ) + APP.url_map.strict_slashes = API_RULES.strict_slashes BLUEPRINT = Blueprint( @@ -128,19 +134,24 @@ def schemas(path): dirname_ = os.path.dirname(full_filepath) basename_ = os.path.basename(full_filepath) - path_ = dirname_.replace('..', '').replace('//', '').replace('./', '') + path_ = dirname_.replace('..', '').replace('//', '').replace('./', '') # noqa: E501 if '..' in path_: return 'Invalid path', 400 - return send_from_directory(path_, basename_, - mimetype=get_mimetype(basename_)) - + return send_from_directory( + path_, + basename_, + mimetype=get_mimetype(basename_) + ) - def execute_from_flask(api_function, request: Request, *args, - skip_valid_check=False, - alternative_api=None - ) -> Response: + def execute_from_flask( + api_function, + request: Request, + *args, + skip_valid_check=False, + alternative_api=None, + ) -> Response: """ Executes API function from Flask @@ -160,9 +171,9 @@ def execute_from_flask(api_function, request: Request, *args, content: Union[str, bytes] if not skip_valid_check and not api_request.is_valid(): - headers, status, content = actual_api.get_format_exception(api_request) + headers, status, content = actual_api.get_format_exception(api_request) # noqa: E501 else: - headers, status, content = api_function(actual_api, api_request, *args) + headers, status, content = api_function(actual_api, api_request, *args) # noqa: E501 content = apply_gzip(headers, content) response = make_response(content, status) @@ -171,7 +182,6 @@ def execute_from_flask(api_function, request: Request, *args, response.headers = headers return response - @BLUEPRINT.route('/') def landing_page(): """ @@ -181,7 +191,6 @@ def landing_page(): """ return execute_from_flask(core_api.landing_page, request) - @BLUEPRINT.route('/openapi') def openapi(): """ @@ -192,7 +201,6 @@ def openapi(): return execute_from_flask(core_api.openapi_, request) - @BLUEPRINT.route('/conformance') def conformance(): """ @@ -203,7 +211,6 @@ def conformance(): return execute_from_flask(core_api.conformance, request) - @BLUEPRINT.route('/TileMatrixSets/') def get_tilematrix_set(tileMatrixSetId=None): """ @@ -214,9 +221,9 @@ def get_tilematrix_set(tileMatrixSetId=None): :returns: HTTP response """ - return execute_from_flask(tiles_api.tilematrixset, request, - tileMatrixSetId) - + return execute_from_flask( + tiles_api.tilematrixset, request, tileMatrixSetId + ) @BLUEPRINT.route('/TileMatrixSets') def get_tilematrix_sets(): @@ -228,7 +235,6 @@ def get_tilematrix_sets(): return execute_from_flask(tiles_api.tilematrixsets, request) - @BLUEPRINT.route('/collections') @BLUEPRINT.route('/collections/') def collections(collection_id=None): @@ -240,9 +246,9 @@ def collections(collection_id=None): :returns: HTTP response """ - return execute_from_flask(core_api.describe_collections, request, - collection_id) - + return execute_from_flask( + core_api.describe_collections, request, collection_id + ) @BLUEPRINT.route('/collections//schema') def collection_schema(collection_id): @@ -254,9 +260,9 @@ def collection_schema(collection_id): :returns: HTTP response """ - return execute_from_flask(core_api.get_collection_schema, request, - collection_id) - + return execute_from_flask( + core_api.get_collection_schema, request, collection_id + ) @BLUEPRINT.route('/collections//queryables') def collection_queryables(collection_id=None): @@ -268,16 +274,20 @@ def collection_queryables(collection_id=None): :returns: HTTP response """ - return execute_from_flask(itemtypes_api.get_collection_queryables, request, - collection_id) - + return execute_from_flask( + itemtypes_api.get_collection_queryables, request, collection_id + ) - @BLUEPRINT.route('/collections//items', - methods=['GET', 'POST', 'OPTIONS'], - provide_automatic_options=False) - @BLUEPRINT.route('/collections//items/', - methods=['GET', 'PUT', 'DELETE', 'OPTIONS'], - provide_automatic_options=False) + @BLUEPRINT.route( + '/collections//items', + methods=['GET', 'POST', 'OPTIONS'], + provide_automatic_options=False + ) + @BLUEPRINT.route( + '/collections//items/', + methods=['GET', 'PUT', 'DELETE', 'OPTIONS'], + provide_automatic_options=False + ) def collection_items(collection_id, item_id=None): """ OGC API collections items endpoint @@ -293,38 +303,42 @@ def collection_items(collection_id, item_id=None): if request.content_type is not None: if request.content_type == 'application/geo+json': return execute_from_flask( - itemtypes_api.manage_collection_item, - request, 'create', collection_id, - skip_valid_check=True) + itemtypes_api.manage_collection_item, + request, 'create', collection_id, + skip_valid_check=True) else: return execute_from_flask( - itemtypes_api.get_collection_items, request, - collection_id, skip_valid_check=True) + itemtypes_api.get_collection_items, request, + collection_id, skip_valid_check=True) elif request.method == 'OPTIONS': return execute_from_flask( - itemtypes_api.manage_collection_item, request, 'options', - collection_id, skip_valid_check=True) + itemtypes_api.manage_collection_item, request, 'options', + collection_id, skip_valid_check=True) else: # GET: list items - return execute_from_flask(itemtypes_api.get_collection_items, - request, collection_id, - skip_valid_check=True) + return execute_from_flask( + itemtypes_api.get_collection_items, + request, collection_id, + skip_valid_check=True) elif request.method == 'DELETE': - return execute_from_flask(itemtypes_api.manage_collection_item, - request, 'delete', collection_id, item_id, - skip_valid_check=True) + return execute_from_flask( + itemtypes_api.manage_collection_item, + request, 'delete', collection_id, item_id, + skip_valid_check=True) elif request.method == 'PUT': - return execute_from_flask(itemtypes_api.manage_collection_item, - request, 'update', collection_id, item_id, - skip_valid_check=True) + return execute_from_flask( + itemtypes_api.manage_collection_item, + request, 'update', collection_id, item_id, + skip_valid_check=True) elif request.method == 'OPTIONS': - return execute_from_flask(itemtypes_api.manage_collection_item, - request, 'options', collection_id, item_id, - skip_valid_check=True) + return execute_from_flask( + itemtypes_api.manage_collection_item, + request, 'options', collection_id, item_id, + skip_valid_check=True) else: - return execute_from_flask(itemtypes_api.get_collection_item, request, - collection_id, item_id) - + return execute_from_flask( + itemtypes_api.get_collection_item, request, + collection_id, item_id) @BLUEPRINT.route('/collections//coverage') def collection_coverage(collection_id): @@ -336,9 +350,10 @@ def collection_coverage(collection_id): :returns: HTTP response """ - return execute_from_flask(coverages_api.get_collection_coverage, request, - collection_id, skip_valid_check=True) - + return execute_from_flask( + coverages_api.get_collection_coverage, request, + collection_id, skip_valid_check=True + ) @BLUEPRINT.route('/collections//tiles') def get_collection_tiles(collection_id=None): @@ -350,13 +365,13 @@ def get_collection_tiles(collection_id=None): :returns: HTTP response """ - return execute_from_flask(tiles_api.get_collection_tiles, request, - collection_id) - + return execute_from_flask( + tiles_api.get_collection_tiles, request, collection_id) - @BLUEPRINT.route('/collections//tiles/') - @BLUEPRINT.route('/collections//tiles//metadata') # noqa - def get_collection_tiles_metadata(collection_id=None, tileMatrixSetId=None): + @BLUEPRINT.route('/collections//tiles/') # noqa E501 + @BLUEPRINT.route('/collections//tiles//metadata') # noqa E501 + def get_collection_tiles_metadata( + collection_id=None, tileMatrixSetId=None): """ OGC open api collection tiles service metadata @@ -366,15 +381,16 @@ def get_collection_tiles_metadata(collection_id=None, tileMatrixSetId=None): :returns: HTTP response """ - return execute_from_flask(tiles_api.get_collection_tiles_metadata, - request, collection_id, tileMatrixSetId, - skip_valid_check=True) - + return execute_from_flask( + tiles_api.get_collection_tiles_metadata, + request, collection_id, tileMatrixSetId, + skip_valid_check=True) @BLUEPRINT.route('/collections//tiles/\ ///') - def get_collection_tiles_data(collection_id=None, tileMatrixSetId=None, - tileMatrix=None, tileRow=None, tileCol=None): + def get_collection_tiles_data( + collection_id=None, tileMatrixSetId=None, + tileMatrix=None, tileRow=None, tileCol=None): """ OGC open api collection tiles service data @@ -389,11 +405,10 @@ def get_collection_tiles_data(collection_id=None, tileMatrixSetId=None, return execute_from_flask( tiles_api.get_collection_tiles_data, - request, collection_id, tileMatrixSetId, tileMatrix, tileRow, tileCol, - skip_valid_check=True, + request, collection_id, tileMatrixSetId, tileMatrix, + tileRow, tileCol, skip_valid_check=True ) - @BLUEPRINT.route('/collections//map') @BLUEPRINT.route('/collections//styles//map') def collection_map(collection_id, style_id=None): @@ -410,7 +425,6 @@ def collection_map(collection_id, style_id=None): maps_api.get_collection_map, request, collection_id, style_id ) - @BLUEPRINT.route('/processes') @BLUEPRINT.route('/processes/') def get_processes(process_id=None): @@ -422,13 +436,13 @@ def get_processes(process_id=None): :returns: HTTP response """ - return execute_from_flask(processes_api.describe_processes, request, - process_id) - + return execute_from_flask( + processes_api.describe_processes, request, process_id) @BLUEPRINT.route('/jobs') - @BLUEPRINT.route('/jobs/', - methods=['GET', 'DELETE']) + @BLUEPRINT.route( + '/jobs/', methods=['GET', 'DELETE'] + ) def get_jobs(job_id=None): """ OGC API - Processes jobs endpoint @@ -442,11 +456,13 @@ def get_jobs(job_id=None): return execute_from_flask(processes_api.get_jobs, request) else: if request.method == 'DELETE': # dismiss job - return execute_from_flask(processes_api.delete_job, request, - job_id) + return execute_from_flask( + processes_api.delete_job, request, job_id + ) else: # Return status of a specific job - return execute_from_flask(processes_api.get_jobs, request, job_id) - + return execute_from_flask( + processes_api.get_jobs, request, job_id + ) @BLUEPRINT.route('/processes//execution', methods=['POST']) def execute_process_jobs(process_id): @@ -458,12 +474,14 @@ def execute_process_jobs(process_id): :returns: HTTP response """ - return execute_from_flask(processes_api.execute_process, request, - process_id) - + return execute_from_flask( + processes_api.execute_process, request, process_id + ) - @BLUEPRINT.route('/jobs//results', - methods=['GET']) + @BLUEPRINT.route( + '/jobs//results', + methods=['GET'] + ) def get_job_result(job_id=None): """ OGC API - Processes job result endpoint @@ -473,8 +491,9 @@ def get_job_result(job_id=None): :returns: HTTP response """ - return execute_from_flask(processes_api.get_job_result, request, job_id) - + return execute_from_flask( + processes_api.get_job_result, request, job_id + ) @BLUEPRINT.route('/collections//position') @BLUEPRINT.route('/collections//area') @@ -482,20 +501,21 @@ def get_job_result(job_id=None): @BLUEPRINT.route('/collections//radius') @BLUEPRINT.route('/collections//trajectory') @BLUEPRINT.route('/collections//corridor') - @BLUEPRINT.route('/collections//locations/') + @BLUEPRINT.route('/collections//locations/') # noqa E501 @BLUEPRINT.route('/collections//locations') - @BLUEPRINT.route('/collections//instances//position') # noqa - @BLUEPRINT.route('/collections//instances//area') # noqa - @BLUEPRINT.route('/collections//instances//cube') # noqa - @BLUEPRINT.route('/collections//instances//radius') # noqa - @BLUEPRINT.route('/collections//instances//trajectory') # noqa - @BLUEPRINT.route('/collections//instances//corridor') # noqa - @BLUEPRINT.route('/collections//instances//locations/') # noqa - @BLUEPRINT.route('/collections//instances//locations') # noqa - @BLUEPRINT.route('/collections//instances/') + @BLUEPRINT.route('/collections//instances//position') # noqa E501 + @BLUEPRINT.route('/collections//instances//area') # noqa E501 + @BLUEPRINT.route('/collections//instances//cube') # noqa E501 + @BLUEPRINT.route('/collections//instances//radius') # noqa E501 + @BLUEPRINT.route('/collections//instances//trajectory') # noqa E501 + @BLUEPRINT.route('/collections//instances//corridor') # noqa E501 + @BLUEPRINT.route('/collections//instances//locations/') # noqa E501 + @BLUEPRINT.route('/collections//instances//locations') # noqa E501 + @BLUEPRINT.route('/collections//instances/') # noqa E501 @BLUEPRINT.route('/collections//instances') - def get_collection_edr_query(collection_id, instance_id=None, - location_id=None): + def get_collection_edr_query( + collection_id, instance_id=None, location_id=None + ): """ OGC EDR API endpoints @@ -506,9 +526,10 @@ def get_collection_edr_query(collection_id, instance_id=None, :returns: HTTP response """ - if (request.path.endswith('instances') or - (instance_id is not None and - request.path.endswith(instance_id))): + if ( + request.path.endswith('instances') or + (instance_id is not None and request.path.endswith(instance_id)) + ): return execute_from_flask( edr_api.get_collection_edr_instances, request, collection_id, instance_id @@ -520,11 +541,10 @@ def get_collection_edr_query(collection_id, instance_id=None, query_type = request.path.split('/')[-1] return execute_from_flask( - edr_api.get_collection_edr_query, request, collection_id, instance_id, - query_type, location_id, skip_valid_check=True + edr_api.get_collection_edr_query, request, collection_id, + instance_id, query_type, location_id, skip_valid_check=True ) - @BLUEPRINT.route('/stac-api') def stac_landing_page(): """ @@ -535,7 +555,6 @@ def stac_landing_page(): return execute_from_flask(stac_api.landing_page, request) - @BLUEPRINT.route('/stac-api/search', methods=['GET', 'POST']) def stac_search(): """ @@ -546,7 +565,6 @@ def stac_search(): return execute_from_flask(stac_api.search, request) - @BLUEPRINT.route('/stac') def stac_catalog_root(): """ @@ -557,7 +575,6 @@ def stac_catalog_root(): return execute_from_flask(stac_api.get_stac_root, request) - @BLUEPRINT.route('/stac/') def stac_catalog_path(path): """ @@ -570,7 +587,6 @@ def stac_catalog_path(path): return execute_from_flask(stac_api.get_stac_path, request, path) - @ADMIN_BLUEPRINT.route('/admin/config', methods=['GET', 'PUT', 'PATCH']) def admin_config(): """ @@ -580,17 +596,19 @@ def admin_config(): """ if request.method == 'GET': - return execute_from_flask(admin_api.get_config_, request, - alternative_api=admin_) + return execute_from_flask( + admin_api.get_config_, request, alternative_api=admin_ + ) elif request.method == 'PUT': - return execute_from_flask(admin_api.put_config, request, - alternative_api=admin_) + return execute_from_flask( + admin_api.put_config, request, alternative_api=admin_ + ) elif request.method == 'PATCH': - return execute_from_flask(admin_api.patch_config, request, - alternative_api=admin_) - + return execute_from_flask( + admin_api.patch_config, request, alternative_api=admin_ + ) @ADMIN_BLUEPRINT.route('/admin/config/resources', methods=['GET', 'POST']) def admin_config_resources(): @@ -601,13 +619,14 @@ def admin_config_resources(): """ if request.method == 'GET': - return execute_from_flask(admin_api.get_resources, request, - alternative_api=admin_) + return execute_from_flask( + admin_api.get_resources, request, alternative_api=admin_ + ) elif request.method == 'POST': - return execute_from_flask(admin_api.post_resource, request, - alternative_api=admin_) - + return execute_from_flask( + admin_api.post_resource, request, alternative_api=admin_ + ) @ADMIN_BLUEPRINT.route( '/admin/config/resources/', @@ -620,25 +639,28 @@ def admin_config_resource(resource_id): """ if request.method == 'GET': - return execute_from_flask(admin_api.get_resource, request, - resource_id, - alternative_api=admin_) + return execute_from_flask( + admin_api.get_resource, request, + resource_id, alternative_api=admin_ + ) elif request.method == 'DELETE': - return execute_from_flask(admin_api.delete_resource, request, - resource_id, - alternative_api=admin_) + return execute_from_flask( + admin_api.delete_resource, request, + resource_id, alternative_api=admin_ + ) elif request.method == 'PUT': - return execute_from_flask(admin_api.put_resource, request, - resource_id, - alternative_api=admin_) + return execute_from_flask( + admin_api.put_resource, request, + resource_id, alternative_api=admin_ + ) elif request.method == 'PATCH': - return execute_from_flask(admin_api.patch_resource, request, - resource_id, - alternative_api=admin_) - + return execute_from_flask( + admin_api.patch_resource, request, + resource_id, alternative_api=admin_ + ) APP.register_blueprint(BLUEPRINT) @@ -648,6 +670,7 @@ def admin_config_resource(resource_id): return APP + if os.environ.get('PYGEOAPI_CONFIG_VIA_ENV', 'true') == 'true': APP = make_wsgi_app( config_location=None, diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index 97ae5ae82..fdc3abd07 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -1001,7 +1001,7 @@ def generate_openapi_document(cfg_file: Union[Path, io.TextIOWrapper], def load_openapi_document(pygeoapi_openapi: str = None) -> dict: """ Open OpenAPI document from `PYGEOAPI_OPENAPI` environment variable - + Args: pygeoapi_openapi: `str` of OpenAPI document filepath, if `None` will attempt to read from `PYGEOAPI_OPENAPI` environment diff --git a/tests/data/mysql_data.sql b/tests/data/mysql_data.sql index f2174ae55..9bb7c34f8 100644 --- a/tests/data/mysql_data.sql +++ b/tests/data/mysql_data.sql @@ -1,4 +1,4 @@ --- A test database for the mysql provider; a simple geospatial app +-- A test database for the mysql provider; a simple geospatial app -- Create the database DROP DATABASE IF EXISTS test_geo_app; From 8168dc5537cc38c96c972a72d273dd8423a83f2c Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 21 Oct 2025 16:34:00 +0200 Subject: [PATCH 05/17] Fix argument name in load_openapi_document call in make_wsgi_app function --- pygeoapi/flask_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 2aaee2cde..67815267d 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -65,7 +65,7 @@ def make_wsgi_app(config_location: str, openapi_location: str) -> Flask: Flask WSGI application """ CONFIG = get_config(config_location=config_location) - OPENAPI = load_openapi_document(openapi_location=openapi_location) + OPENAPI = load_openapi_document(pygeoapi_openapi=openapi_location) API_RULES = get_api_rules(CONFIG) From f68df95d6647197a2386cb6684a7ef91d5bd5eeb Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Thu, 23 Oct 2025 08:44:13 +0200 Subject: [PATCH 06/17] Refactor docstrings in config and openapi modules to use :param for argument descriptions --- Dockerfile | 7 +------ pygeoapi/config.py | 8 +++----- pygeoapi/openapi.py | 5 ++--- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4a87e2096..23f9a48c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -128,12 +128,7 @@ RUN apt-get update -y \ ADD . /pygeoapi -# Install remaining pygeoapi deps and pygeoapi itself -RUN python3 -m venv --system-site-packages /venv \ - && /venv/bin/python3 -m pip install --no-cache-dir -r requirements-docker.txt \ - && /venv/bin/python3 -m pip install --no-cache-dir -r requirements-admin.txt \ - && /venv/bin/python3 -m pip install --no-cache-dir gunicorn \ - && /venv/bin/python3 -m pip install --no-cache-dir -e . + # Set default config and entrypoint for Docker Image # and compile language files diff --git a/pygeoapi/config.py b/pygeoapi/config.py index 68971cb6f..cd3e7ff46 100644 --- a/pygeoapi/config.py +++ b/pygeoapi/config.py @@ -45,11 +45,9 @@ def get_config(config_location: str = None, raw: bool = False) -> dict: """ Get pygeoapi configurations - Args: - config_location: `str` of configuration file location, if `None` - will attempt to read from `PYGEOAPI_CONFIG` - environment variable - raw: `bool` over interpolation during config loading + :param config_location: `str` of configuration file location, if `None` + will attempt to read from `PYGEOAPI_CONFIG` + :param raw: `bool` over interpolation during config loading :returns: `dict` of pygeoapi configuration """ diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index fdc3abd07..5ce99f420 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -1002,9 +1002,8 @@ def load_openapi_document(pygeoapi_openapi: str = None) -> dict: """ Open OpenAPI document from `PYGEOAPI_OPENAPI` environment variable - Args: - pygeoapi_openapi: `str` of OpenAPI document filepath, if `None` will - attempt to read from `PYGEOAPI_OPENAPI` environment + :param pygeoapi_openapi: `str` of OpenAPI document filepath, if `None` will + attempt to read from `PYGEOAPI_OPENAPI` environment :returns: `dict` of OpenAPI document """ From 9399cf75c827042360960ef34c23d70f6aaecb7b Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Thu, 23 Oct 2025 11:24:51 +0200 Subject: [PATCH 07/17] Revert unintended change --- Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 23f9a48c1..4a87e2096 100644 --- a/Dockerfile +++ b/Dockerfile @@ -128,7 +128,12 @@ RUN apt-get update -y \ ADD . /pygeoapi - +# Install remaining pygeoapi deps and pygeoapi itself +RUN python3 -m venv --system-site-packages /venv \ + && /venv/bin/python3 -m pip install --no-cache-dir -r requirements-docker.txt \ + && /venv/bin/python3 -m pip install --no-cache-dir -r requirements-admin.txt \ + && /venv/bin/python3 -m pip install --no-cache-dir gunicorn \ + && /venv/bin/python3 -m pip install --no-cache-dir -e . # Set default config and entrypoint for Docker Image # and compile language files From 14ffc469b229fdfb96b43fc50792865f302c37df Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Thu, 23 Oct 2025 12:00:58 +0200 Subject: [PATCH 08/17] Update environment variable check for WSGI app configuration --- pygeoapi/flask_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 67815267d..4268ea642 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -671,7 +671,7 @@ def admin_config_resource(resource_id): return APP -if os.environ.get('PYGEOAPI_CONFIG_VIA_ENV', 'true') == 'true': +if os.environ.get('PYGEOAPI_DISABLE_ENV_CONFIGS', 'false') == 'false': APP = make_wsgi_app( config_location=None, openapi_location=None From 9455ca558c389ff04ba539a3f5e182a960ac5ae7 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Mon, 27 Oct 2025 09:19:16 +0100 Subject: [PATCH 09/17] Type hint, upper cases and var naming --- pygeoapi/config.py | 20 ++-- pygeoapi/flask_app.py | 240 +++++++++++++++++++++--------------------- tests/util.py | 2 +- 3 files changed, 131 insertions(+), 131 deletions(-) diff --git a/pygeoapi/config.py b/pygeoapi/config.py index cd3e7ff46..3f5ade823 100644 --- a/pygeoapi/config.py +++ b/pygeoapi/config.py @@ -41,30 +41,30 @@ LOGGER = logging.getLogger(__name__) -def get_config(config_location: str = None, raw: bool = False) -> dict: +def get_config(config_path: str = None, raw: bool = False) -> dict: """ Get pygeoapi configurations - :param config_location: `str` of configuration file location, if `None` + :param config_path: `str` of configuration file location, if `None` will attempt to read from `PYGEOAPI_CONFIG` :param raw: `bool` over interpolation during config loading :returns: `dict` of pygeoapi configuration """ - if config_location is None: - config_location = os.environ.get('PYGEOAPI_CONFIG') + if config_path is None: + config_path = os.environ.get('PYGEOAPI_CONFIG') - if config_location is None: - raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') + if config_path is None: + raise KeyError('PYGEOAPI_CONFIG environment variable not set') - with open(config_location, encoding='utf8') as fh: + with open(config_path, encoding='utf8') as fh: if raw: - CONFIG = yaml.safe_load(fh) + config = yaml.safe_load(fh) else: - CONFIG = yaml_load(fh) + config = yaml_load(fh) - return CONFIG + return config def load_schema() -> dict: diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 4268ea642..d2f05acea 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -64,64 +64,64 @@ def make_wsgi_app(config_location: str, openapi_location: str) -> Flask: Returns: Flask WSGI application """ - CONFIG = get_config(config_location=config_location) - OPENAPI = load_openapi_document(pygeoapi_openapi=openapi_location) + config = get_config(config_path=config_location) + openapi = load_openapi_document(pygeoapi_openapi=openapi_location) - API_RULES = get_api_rules(CONFIG) + api_rules = get_api_rules(config) - if CONFIG['server'].get('admin'): + if config['server'].get('admin'): import pygeoapi.api.admin as admin_api from pygeoapi.api.admin import Admin - STATIC_FOLDER = 'static' - if 'templates' in CONFIG['server']: - STATIC_FOLDER = CONFIG['server']['templates'].get('static', 'static') + static_folder = 'static' + if 'templates' in config['server']: + static_folder = config['server']['templates'].get('static', 'static') - APP = Flask( + app = Flask( __name__, - static_folder=STATIC_FOLDER, + static_folder=static_folder, static_url_path='/static' ) - APP.url_map.strict_slashes = API_RULES.strict_slashes + app.url_map.strict_slashes = api_rules.strict_slashes - BLUEPRINT = Blueprint( + blueprint = Blueprint( 'pygeoapi', __name__, - static_folder=STATIC_FOLDER, - url_prefix=API_RULES.get_url_prefix('flask') + static_folder=static_folder, + url_prefix=api_rules.get_url_prefix('flask') ) - ADMIN_BLUEPRINT = Blueprint( + admin_blueprint = Blueprint( 'admin', __name__, - static_folder=STATIC_FOLDER, - url_prefix=API_RULES.get_url_prefix('flask') + static_folder=static_folder, + url_prefix=api_rules.get_url_prefix('flask') ) # CORS: optionally enable from config. - if CONFIG['server'].get('cors', False): + if config['server'].get('cors', False): try: from flask_cors import CORS - CORS(APP, CORS_EXPOSE_HEADERS=['*']) + CORS(app, CORS_EXPOSE_HEADERS=['*']) except ModuleNotFoundError: print('Python package flask-cors required for CORS support') - APP.config['JSONIFY_PRETTYPRINT_REGULAR'] = CONFIG['server'].get( + app.config['JSONIFY_PRETTYPRINT_REGULAR'] = config['server'].get( 'pretty_print', True) - api_ = API(CONFIG, OPENAPI) + api_ = API(config, openapi) - OGC_SCHEMAS_LOCATION = CONFIG['server'].get('ogc_schemas_location') + ogc_schemas_location = config['server'].get('ogc_schemas_location') - if (OGC_SCHEMAS_LOCATION is not None and - not OGC_SCHEMAS_LOCATION.startswith('http')): + if (ogc_schemas_location is not None and + not ogc_schemas_location.startswith('http')): # serve the OGC schemas locally - if not os.path.exists(OGC_SCHEMAS_LOCATION): + if not os.path.exists(ogc_schemas_location): raise RuntimeError('OGC schemas misconfigured') - @BLUEPRINT.route('/schemas/', methods=['GET']) - def schemas(path): + @blueprint.route('/schemas/', methods=['GET']) + def schemas(path: str) -> Response: """ Serve OGC schemas locally @@ -130,7 +130,7 @@ def schemas(path): :returns: HTTP response """ - full_filepath = os.path.join(OGC_SCHEMAS_LOCATION, path) + full_filepath = os.path.join(ogc_schemas_location, path) dirname_ = os.path.dirname(full_filepath) basename_ = os.path.basename(full_filepath) @@ -146,11 +146,11 @@ def schemas(path): ) def execute_from_flask( - api_function, + api_function: callable, request: Request, *args, - skip_valid_check=False, - alternative_api=None, + skip_valid_check: bool = False, + alternative_api: Response = None, ) -> Response: """ Executes API function from Flask @@ -182,8 +182,8 @@ def execute_from_flask( response.headers = headers return response - @BLUEPRINT.route('/') - def landing_page(): + @blueprint.route('/') + def landing_page() -> Response: """ OGC API landing page endpoint @@ -191,8 +191,8 @@ def landing_page(): """ return execute_from_flask(core_api.landing_page, request) - @BLUEPRINT.route('/openapi') - def openapi(): + @blueprint.route('/openapi') + def openapi() -> Response: """ OpenAPI endpoint @@ -201,8 +201,8 @@ def openapi(): return execute_from_flask(core_api.openapi_, request) - @BLUEPRINT.route('/conformance') - def conformance(): + @blueprint.route('/conformance') + def conformance() -> Response: """ OGC API conformance endpoint @@ -211,8 +211,8 @@ def conformance(): return execute_from_flask(core_api.conformance, request) - @BLUEPRINT.route('/TileMatrixSets/') - def get_tilematrix_set(tileMatrixSetId=None): + @blueprint.route('/TileMatrixSets/') + def get_tilematrix_set(tileMatrixSetId: str) -> Response: """ OGC API TileMatrixSet endpoint @@ -220,24 +220,22 @@ def get_tilematrix_set(tileMatrixSetId=None): :returns: HTTP response """ - return execute_from_flask( tiles_api.tilematrixset, request, tileMatrixSetId - ) + ) - @BLUEPRINT.route('/TileMatrixSets') - def get_tilematrix_sets(): + @blueprint.route('/TileMatrixSets') + def get_tilematrix_sets() -> Response: """ OGC API TileMatrixSets endpoint :returns: HTTP response """ - return execute_from_flask(tiles_api.tilematrixsets, request) - @BLUEPRINT.route('/collections') - @BLUEPRINT.route('/collections/') - def collections(collection_id=None): + @blueprint.route('/collections') + @blueprint.route('/collections/') + def collections(collection_id: str = None) -> Response: """ OGC API collections endpoint @@ -245,13 +243,12 @@ def collections(collection_id=None): :returns: HTTP response """ - return execute_from_flask( core_api.describe_collections, request, collection_id ) - @BLUEPRINT.route('/collections//schema') - def collection_schema(collection_id): + @blueprint.route('/collections//schema') + def collection_schema(collection_id: str) -> Response: """ OGC API - collections schema endpoint @@ -264,8 +261,8 @@ def collection_schema(collection_id): core_api.get_collection_schema, request, collection_id ) - @BLUEPRINT.route('/collections//queryables') - def collection_queryables(collection_id=None): + @blueprint.route('/collections//queryables') + def collection_queryables(collection_id: str) -> Response: """ OGC API collections queryables endpoint @@ -278,17 +275,17 @@ def collection_queryables(collection_id=None): itemtypes_api.get_collection_queryables, request, collection_id ) - @BLUEPRINT.route( + @blueprint.route( '/collections//items', methods=['GET', 'POST', 'OPTIONS'], provide_automatic_options=False ) - @BLUEPRINT.route( + @blueprint.route( '/collections//items/', methods=['GET', 'PUT', 'DELETE', 'OPTIONS'], provide_automatic_options=False ) - def collection_items(collection_id, item_id=None): + def collection_items(collection_id: str, item_id: str = None) -> Response: """ OGC API collections items endpoint @@ -340,8 +337,8 @@ def collection_items(collection_id, item_id=None): itemtypes_api.get_collection_item, request, collection_id, item_id) - @BLUEPRINT.route('/collections//coverage') - def collection_coverage(collection_id): + @blueprint.route('/collections//coverage') + def collection_coverage(collection_id: str) -> Response: """ OGC API - Coverages coverage endpoint @@ -355,8 +352,8 @@ def collection_coverage(collection_id): collection_id, skip_valid_check=True ) - @BLUEPRINT.route('/collections//tiles') - def get_collection_tiles(collection_id=None): + @blueprint.route('/collections//tiles') + def get_collection_tiles(collection_id: str) -> Response: """ OGC open api collections tiles access point @@ -368,10 +365,10 @@ def get_collection_tiles(collection_id=None): return execute_from_flask( tiles_api.get_collection_tiles, request, collection_id) - @BLUEPRINT.route('/collections//tiles/') # noqa E501 - @BLUEPRINT.route('/collections//tiles//metadata') # noqa E501 + @blueprint.route('/collections//tiles/') # noqa E501 + @blueprint.route('/collections//tiles//metadata') # noqa E501 def get_collection_tiles_metadata( - collection_id=None, tileMatrixSetId=None): + collection_id: str, tileMatrixSetId: str) -> Response: """ OGC open api collection tiles service metadata @@ -386,11 +383,11 @@ def get_collection_tiles_metadata( request, collection_id, tileMatrixSetId, skip_valid_check=True) - @BLUEPRINT.route('/collections//tiles/\ + @blueprint.route('/collections//tiles/\ ///') def get_collection_tiles_data( - collection_id=None, tileMatrixSetId=None, - tileMatrix=None, tileRow=None, tileCol=None): + collection_id: str, tileMatrixSetId: str, + tileMatrix: str, tileRow: str, tileCol: str) -> Response: """ OGC open api collection tiles service data @@ -409,9 +406,9 @@ def get_collection_tiles_data( tileRow, tileCol, skip_valid_check=True ) - @BLUEPRINT.route('/collections//map') - @BLUEPRINT.route('/collections//styles//map') - def collection_map(collection_id, style_id=None): + @blueprint.route('/collections//map') + @blueprint.route('/collections//styles//map') + def collection_map(collection_id: str, style_id: str = None) -> Response: """ OGC API - Maps map render endpoint @@ -425,9 +422,9 @@ def collection_map(collection_id, style_id=None): maps_api.get_collection_map, request, collection_id, style_id ) - @BLUEPRINT.route('/processes') - @BLUEPRINT.route('/processes/') - def get_processes(process_id=None): + @blueprint.route('/processes') + @blueprint.route('/processes/') + def get_processes(process_id: str = None) -> Response: """ OGC API - Processes description endpoint @@ -439,11 +436,11 @@ def get_processes(process_id=None): return execute_from_flask( processes_api.describe_processes, request, process_id) - @BLUEPRINT.route('/jobs') - @BLUEPRINT.route( + @blueprint.route('/jobs') + @blueprint.route( '/jobs/', methods=['GET', 'DELETE'] ) - def get_jobs(job_id=None): + def get_jobs(job_id: str = None) -> Response: """ OGC API - Processes jobs endpoint @@ -464,8 +461,8 @@ def get_jobs(job_id=None): processes_api.get_jobs, request, job_id ) - @BLUEPRINT.route('/processes//execution', methods=['POST']) - def execute_process_jobs(process_id): + @blueprint.route('/processes//execution', methods=['POST']) + def execute_process_jobs(process_id: str) -> Response: """ OGC API - Processes execution endpoint @@ -478,11 +475,11 @@ def execute_process_jobs(process_id): processes_api.execute_process, request, process_id ) - @BLUEPRINT.route( + @blueprint.route( '/jobs//results', methods=['GET'] ) - def get_job_result(job_id=None): + def get_job_result(job_id: str = None) -> Response: """ OGC API - Processes job result endpoint @@ -495,27 +492,28 @@ def get_job_result(job_id=None): processes_api.get_job_result, request, job_id ) - @BLUEPRINT.route('/collections//position') - @BLUEPRINT.route('/collections//area') - @BLUEPRINT.route('/collections//cube') - @BLUEPRINT.route('/collections//radius') - @BLUEPRINT.route('/collections//trajectory') - @BLUEPRINT.route('/collections//corridor') - @BLUEPRINT.route('/collections//locations/') # noqa E501 - @BLUEPRINT.route('/collections//locations') - @BLUEPRINT.route('/collections//instances//position') # noqa E501 - @BLUEPRINT.route('/collections//instances//area') # noqa E501 - @BLUEPRINT.route('/collections//instances//cube') # noqa E501 - @BLUEPRINT.route('/collections//instances//radius') # noqa E501 - @BLUEPRINT.route('/collections//instances//trajectory') # noqa E501 - @BLUEPRINT.route('/collections//instances//corridor') # noqa E501 - @BLUEPRINT.route('/collections//instances//locations/') # noqa E501 - @BLUEPRINT.route('/collections//instances//locations') # noqa E501 - @BLUEPRINT.route('/collections//instances/') # noqa E501 - @BLUEPRINT.route('/collections//instances') + @blueprint.route('/collections//position') + @blueprint.route('/collections//area') + @blueprint.route('/collections//cube') + @blueprint.route('/collections//radius') + @blueprint.route('/collections//trajectory') + @blueprint.route('/collections//corridor') + @blueprint.route('/collections//locations/') # noqa E501 + @blueprint.route('/collections//locations') + @blueprint.route('/collections//instances//position') # noqa E501 + @blueprint.route('/collections//instances//area') # noqa E501 + @blueprint.route('/collections//instances//cube') # noqa E501 + @blueprint.route('/collections//instances//radius') # noqa E501 + @blueprint.route('/collections//instances//trajectory') # noqa E501 + @blueprint.route('/collections//instances//corridor') # noqa E501 + @blueprint.route('/collections//instances//locations/') # noqa E501 + @blueprint.route('/collections//instances//locations') # noqa E501 + @blueprint.route('/collections//instances/') # noqa E501 + @blueprint.route('/collections//instances') def get_collection_edr_query( - collection_id, instance_id=None, location_id=None - ): + collection_id: str, instance_id: str = None, + location_id: str = None + ) -> Response: """ OGC EDR API endpoints @@ -545,8 +543,8 @@ def get_collection_edr_query( instance_id, query_type, location_id, skip_valid_check=True ) - @BLUEPRINT.route('/stac-api') - def stac_landing_page(): + @blueprint.route('/stac-api') + def stac_landing_page() -> Response: """ STAC API landing page endpoint @@ -555,8 +553,8 @@ def stac_landing_page(): return execute_from_flask(stac_api.landing_page, request) - @BLUEPRINT.route('/stac-api/search', methods=['GET', 'POST']) - def stac_search(): + @blueprint.route('/stac-api/search', methods=['GET', 'POST']) + def stac_search() -> Response: """ STAC API search endpoint @@ -565,8 +563,8 @@ def stac_search(): return execute_from_flask(stac_api.search, request) - @BLUEPRINT.route('/stac') - def stac_catalog_root(): + @blueprint.route('/stac') + def stac_catalog_root() -> Response: """ STAC root endpoint @@ -575,8 +573,8 @@ def stac_catalog_root(): return execute_from_flask(stac_api.get_stac_root, request) - @BLUEPRINT.route('/stac/') - def stac_catalog_path(path): + @blueprint.route('/stac/') + def stac_catalog_path(path: str) -> Response: """ STAC path endpoint @@ -587,8 +585,8 @@ def stac_catalog_path(path): return execute_from_flask(stac_api.get_stac_path, request, path) - @ADMIN_BLUEPRINT.route('/admin/config', methods=['GET', 'PUT', 'PATCH']) - def admin_config(): + @admin_blueprint.route('/admin/config', methods=['GET', 'PUT', 'PATCH']) + def admin_config() -> Response: """ Admin endpoint @@ -610,8 +608,8 @@ def admin_config(): admin_api.patch_config, request, alternative_api=admin_ ) - @ADMIN_BLUEPRINT.route('/admin/config/resources', methods=['GET', 'POST']) - def admin_config_resources(): + @admin_blueprint.route('/admin/config/resources', methods=['GET', 'POST']) + def admin_config_resources() -> Response: """ Resources endpoint @@ -628,10 +626,10 @@ def admin_config_resources(): admin_api.post_resource, request, alternative_api=admin_ ) - @ADMIN_BLUEPRINT.route( + @admin_blueprint.route( '/admin/config/resources/', methods=['GET', 'PUT', 'PATCH', 'DELETE']) - def admin_config_resource(resource_id): + def admin_config_resource(resource_id: str) -> Response: """ Resource endpoint @@ -662,13 +660,13 @@ def admin_config_resource(resource_id): resource_id, alternative_api=admin_ ) - APP.register_blueprint(BLUEPRINT) + app.register_blueprint(blueprint) - if CONFIG['server'].get('admin'): - admin_ = Admin(CONFIG, OPENAPI) - APP.register_blueprint(ADMIN_BLUEPRINT) + if config['server'].get('admin'): + admin_ = Admin(config, openapi) + app.register_blueprint(admin_blueprint) - return APP + return app if os.environ.get('PYGEOAPI_DISABLE_ENV_CONFIGS', 'false') == 'false': @@ -676,12 +674,14 @@ def admin_config_resource(resource_id): config_location=None, openapi_location=None ) - CONFIG = get_config() + config = get_config() @click.command() @click.pass_context @click.option('--debug', '-d', default=False, is_flag=True, help='debug') - def serve(ctx, server=None, debug=False): + def serve( + ctx: click.Context, server: str = None, debug: bool = False + ) -> None: """ Serve pygeoapi via Flask. Runs pygeoapi as a flask server. Not recommend for production. @@ -693,8 +693,8 @@ def serve(ctx, server=None, debug=False): """ # setup_logger(CONFIG['logging']) - APP.run(debug=True, host=CONFIG['server']['bind']['host'], - port=CONFIG['server']['bind']['port']) + APP.run(debug=True, host=config['server']['bind']['host'], + port=config['server']['bind']['port']) if __name__ == '__main__': # run locally, for testing serve() diff --git a/tests/util.py b/tests/util.py index 4cdd4373e..4431a00f1 100644 --- a/tests/util.py +++ b/tests/util.py @@ -126,7 +126,7 @@ def mock_flask(config_file: str = 'pygeoapi-test-config.yml', reload(flask_app) # Set server root path - url_parts = urlsplit(flask_app.CONFIG['server']['url']) + url_parts = urlsplit(flask_app.config['server']['url']) app_root = url_parts.path.rstrip('/') or '/' flask_app.APP.config['SERVER_NAME'] = url_parts.netloc flask_app.APP.config['APPLICATION_ROOT'] = app_root From fab8466161e5394cc0dc19aa0455ee2b3475d31f Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Mon, 27 Oct 2025 09:33:42 +0100 Subject: [PATCH 10/17] Refactor line breaks for improved readability in make_wsgi_app function --- pygeoapi/flask_app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index d2f05acea..f12bdda2a 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -171,9 +171,11 @@ def execute_from_flask( content: Union[str, bytes] if not skip_valid_check and not api_request.is_valid(): - headers, status, content = actual_api.get_format_exception(api_request) # noqa: E501 + headers, status, content = \ + actual_api.get_format_exception(api_request) else: - headers, status, content = api_function(actual_api, api_request, *args) # noqa: E501 + headers, status, content = \ + api_function(actual_api, api_request, *args) content = apply_gzip(headers, content) response = make_response(content, status) @@ -693,7 +695,7 @@ def serve( """ # setup_logger(CONFIG['logging']) - APP.run(debug=True, host=config['server']['bind']['host'], + APP.run(debug=debug, host=config['server']['bind']['host'], port=config['server']['bind']['port']) if __name__ == '__main__': # run locally, for testing From 0ea15ef43a95b5b0869d8c94e35f057cba68472e Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Mon, 27 Oct 2025 09:36:18 +0100 Subject: [PATCH 11/17] Fix indentation for improved readability in make_wsgi_app function --- pygeoapi/flask_app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index f12bdda2a..07c49016d 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -247,7 +247,7 @@ def collections(collection_id: str = None) -> Response: """ return execute_from_flask( core_api.describe_collections, request, collection_id - ) + ) @blueprint.route('/collections//schema') def collection_schema(collection_id: str) -> Response: @@ -261,7 +261,7 @@ def collection_schema(collection_id: str) -> Response: return execute_from_flask( core_api.get_collection_schema, request, collection_id - ) + ) @blueprint.route('/collections//queryables') def collection_queryables(collection_id: str) -> Response: @@ -275,7 +275,7 @@ def collection_queryables(collection_id: str) -> Response: return execute_from_flask( itemtypes_api.get_collection_queryables, request, collection_id - ) + ) @blueprint.route( '/collections//items', @@ -352,7 +352,7 @@ def collection_coverage(collection_id: str) -> Response: return execute_from_flask( coverages_api.get_collection_coverage, request, collection_id, skip_valid_check=True - ) + ) @blueprint.route('/collections//tiles') def get_collection_tiles(collection_id: str) -> Response: From fcd449e4dd9320407834e78a1f4e608960b9cacf Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Mon, 27 Oct 2025 09:37:34 +0100 Subject: [PATCH 12/17] Fix line breaks for improved readability in make_wsgi_app function --- pygeoapi/flask_app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 07c49016d..172ad2350 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -365,7 +365,8 @@ def get_collection_tiles(collection_id: str) -> Response: """ return execute_from_flask( - tiles_api.get_collection_tiles, request, collection_id) + tiles_api.get_collection_tiles, request, collection_id + ) @blueprint.route('/collections//tiles/') # noqa E501 @blueprint.route('/collections//tiles//metadata') # noqa E501 @@ -436,7 +437,8 @@ def get_processes(process_id: str = None) -> Response: """ return execute_from_flask( - processes_api.describe_processes, request, process_id) + processes_api.describe_processes, request, process_id + ) @blueprint.route('/jobs') @blueprint.route( From f9d21ed880549687186c8a1dbd1eef2b516def30 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Mon, 27 Oct 2025 09:52:15 +0100 Subject: [PATCH 13/17] Add collection items endpoint and refactor item handling in make_wsgi_app --- pygeoapi/flask_app.py | 75 ++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 172ad2350..182a0b848 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -282,14 +282,49 @@ def collection_queryables(collection_id: str) -> Response: methods=['GET', 'POST', 'OPTIONS'], provide_automatic_options=False ) + def collection_items(collection_id: str) -> Response: + """ + OGC API collections items endpoint + + :param collection_id: collection identifier + + :returns: HTTP response + """ + + if request.method == 'POST': # filter or manage items + if request.content_type == 'application/geo+json': + return execute_from_flask( + itemtypes_api.manage_collection_item, + request, 'create', collection_id, + skip_valid_check=True + ) + else: + return execute_from_flask( + itemtypes_api.get_collection_items, request, + collection_id, skip_valid_check=True + ) + + elif request.method == 'OPTIONS': + return execute_from_flask( + itemtypes_api.manage_collection_item, request, 'options', + collection_id, skip_valid_check=True + ) + + # GET: list items + return execute_from_flask( + itemtypes_api.get_collection_items, + request, collection_id, + skip_valid_check=True + ) + @blueprint.route( '/collections//items/', methods=['GET', 'PUT', 'DELETE', 'OPTIONS'], provide_automatic_options=False ) - def collection_items(collection_id: str, item_id: str = None) -> Response: + def collection_item(collection_id: str, item_id: str) -> Response: """ - OGC API collections items endpoint + OGC API collections item endpoint :param collection_id: collection identifier :param item_id: item identifier @@ -297,47 +332,29 @@ def collection_items(collection_id: str, item_id: str = None) -> Response: :returns: HTTP response """ - if item_id is None: - if request.method == 'POST': # filter or manage items - if request.content_type is not None: - if request.content_type == 'application/geo+json': - return execute_from_flask( - itemtypes_api.manage_collection_item, - request, 'create', collection_id, - skip_valid_check=True) - else: - return execute_from_flask( - itemtypes_api.get_collection_items, request, - collection_id, skip_valid_check=True) - elif request.method == 'OPTIONS': - return execute_from_flask( - itemtypes_api.manage_collection_item, request, 'options', - collection_id, skip_valid_check=True) - else: # GET: list items - return execute_from_flask( - itemtypes_api.get_collection_items, - request, collection_id, - skip_valid_check=True) - - elif request.method == 'DELETE': + if request.method == 'DELETE': return execute_from_flask( itemtypes_api.manage_collection_item, request, 'delete', collection_id, item_id, - skip_valid_check=True) + skip_valid_check=True + ) elif request.method == 'PUT': return execute_from_flask( itemtypes_api.manage_collection_item, request, 'update', collection_id, item_id, - skip_valid_check=True) + skip_valid_check=True + ) elif request.method == 'OPTIONS': return execute_from_flask( itemtypes_api.manage_collection_item, request, 'options', collection_id, item_id, - skip_valid_check=True) + skip_valid_check=True + ) else: return execute_from_flask( itemtypes_api.get_collection_item, request, - collection_id, item_id) + collection_id, item_id + ) @blueprint.route('/collections//coverage') def collection_coverage(collection_id: str) -> Response: From d63ea39e2417ce11b77cf29672bc86bbce6406e1 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Mon, 27 Oct 2025 10:15:23 +0100 Subject: [PATCH 14/17] Refactor route definitions and response handling for improved readability in make_wsgi_app function --- pygeoapi/flask_app.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 182a0b848..69f809abe 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -401,13 +401,20 @@ def get_collection_tiles_metadata( return execute_from_flask( tiles_api.get_collection_tiles_metadata, request, collection_id, tileMatrixSetId, - skip_valid_check=True) + skip_valid_check=True + ) - @blueprint.route('/collections//tiles/\ - ///') + @blueprint.route( + '/collections//tiles/' + '///' + ) def get_collection_tiles_data( - collection_id: str, tileMatrixSetId: str, - tileMatrix: str, tileRow: str, tileCol: str) -> Response: + collection_id: str, + tileMatrixSetId: str, + tileMatrix: str, + tileRow: str, + tileCol: str + ) -> Response: """ OGC open api collection tiles service data @@ -637,16 +644,15 @@ def admin_config_resources() -> Response: :returns: HTTP response """ - if request.method == 'GET': - return execute_from_flask( - admin_api.get_resources, request, alternative_api=admin_ - ) - - elif request.method == 'POST': + if request.method == 'POST': return execute_from_flask( admin_api.post_resource, request, alternative_api=admin_ ) + return execute_from_flask( + admin_api.get_resources, request, alternative_api=admin_ + ) + @admin_blueprint.route( '/admin/config/resources/', methods=['GET', 'PUT', 'PATCH', 'DELETE']) @@ -657,13 +663,7 @@ def admin_config_resource(resource_id: str) -> Response: :returns: HTTP response """ - if request.method == 'GET': - return execute_from_flask( - admin_api.get_resource, request, - resource_id, alternative_api=admin_ - ) - - elif request.method == 'DELETE': + if request.method == 'DELETE': return execute_from_flask( admin_api.delete_resource, request, resource_id, alternative_api=admin_ @@ -681,6 +681,12 @@ def admin_config_resource(resource_id: str) -> Response: resource_id, alternative_api=admin_ ) + # GET + return execute_from_flask( + admin_api.get_resource, request, + resource_id, alternative_api=admin_ + ) + app.register_blueprint(blueprint) if config['server'].get('admin'): From 71c9350528a889a147cee5a2c02646e49cfc4c84 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 28 Oct 2025 09:57:30 +0100 Subject: [PATCH 15/17] Fix indentation for improved readability in get_jobs endpoint definition --- pygeoapi/flask_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 69f809abe..9e3673e15 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -467,7 +467,7 @@ def get_processes(process_id: str = None) -> Response: @blueprint.route('/jobs') @blueprint.route( '/jobs/', methods=['GET', 'DELETE'] - ) + ) def get_jobs(job_id: str = None) -> Response: """ OGC API - Processes jobs endpoint From 8321213df046d8b343cd8e75ee36b4df5255f989 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 28 Oct 2025 10:44:59 +0100 Subject: [PATCH 16/17] Fix image reference in vulnerabilities workflow for consistency --- .github/workflows/vulnerabilities.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/vulnerabilities.yml b/.github/workflows/vulnerabilities.yml index 45af41345..113dfc113 100644 --- a/.github/workflows/vulnerabilities.yml +++ b/.github/workflows/vulnerabilities.yml @@ -34,7 +34,7 @@ jobs: scan-ref: . - name: Build locally the image from Dockerfile run: | - docker buildx build -t ${{ github.repository }}:${{ github.sha }} --platform linux/amd64 --no-cache -f Dockerfile . + docker buildx build -t pygeoapi:${{ github.sha }} --platform linux/amd64 --no-cache -f Dockerfile . - name: Scan locally built Docker image for vulnerabilities with trivy uses: aquasecurity/trivy-action@master env: @@ -46,4 +46,4 @@ jobs: ignore-unfixed: true severity: CRITICAL,HIGH vuln-type: os,library - image-ref: '${{ github.repository }}:${{ github.sha }}' + image-ref: 'pygeoapi:${{ github.sha }}' From 299c0b0676cc3199ab1aeb5ff590de3129aa1bd6 Mon Sep 17 00:00:00 2001 From: DJ Bulsink Date: Tue, 28 Oct 2025 10:57:26 +0100 Subject: [PATCH 17/17] [no ci] Revert fix trivy action --- .github/workflows/vulnerabilities.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/vulnerabilities.yml b/.github/workflows/vulnerabilities.yml index 113dfc113..45af41345 100644 --- a/.github/workflows/vulnerabilities.yml +++ b/.github/workflows/vulnerabilities.yml @@ -34,7 +34,7 @@ jobs: scan-ref: . - name: Build locally the image from Dockerfile run: | - docker buildx build -t pygeoapi:${{ github.sha }} --platform linux/amd64 --no-cache -f Dockerfile . + docker buildx build -t ${{ github.repository }}:${{ github.sha }} --platform linux/amd64 --no-cache -f Dockerfile . - name: Scan locally built Docker image for vulnerabilities with trivy uses: aquasecurity/trivy-action@master env: @@ -46,4 +46,4 @@ jobs: ignore-unfixed: true severity: CRITICAL,HIGH vuln-type: os,library - image-ref: 'pygeoapi:${{ github.sha }}' + image-ref: '${{ github.repository }}:${{ github.sha }}'