From 74982b1e6aea04721d0570ac48ca15930e43e90e Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Sat, 31 Jan 2026 21:12:36 -0500 Subject: [PATCH] RFC5: apply distance limiting --- pygeoapi/api/__init__.py | 50 ++++++++++++++++++++ pygeoapi/api/coverages.py | 10 ++-- pygeoapi/api/environmental_data_retrieval.py | 8 +++- pygeoapi/api/itemtypes.py | 10 ++-- tests/api/test_api.py | 26 ++++++++-- 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/pygeoapi/api/__init__.py b/pygeoapi/api/__init__.py index ffc0a8810..61b17dab8 100644 --- a/pygeoapi/api/__init__.py +++ b/pygeoapi/api/__init__.py @@ -1747,3 +1747,53 @@ def evaluate_limit(requested: Union[None, int], server_limits: dict, else: LOGGER.debug('limit requested') return min(requested2, max_) + + +def evaluate_limit_distance(server_limits: dict, collection_limits: dict, + request_bbox: list = []) -> bool: + """ + Helper function to evaluate limit parameter + + :param server_limits: `dict` of server limits + :param collection_limits: `dict` of collection limits + :param request_bbox: `list` bbox of request + + :returns: `bool` successful distance check + """ + + exceed_msg = None + + effective_limits = ChainMap(collection_limits, server_limits) + + on_exceed = effective_limits.get('on_exceed', 'throttle') + max_distance_x = effective_limits.get('max_distance_x') + max_distance_y = effective_limits.get('max_distance_y') + max_distance_units = effective_limits.get('max_distance_units') + + LOGGER.debug(f'On exceed: {on_exceed}') + LOGGER.debug(f'Maximum distance x: {max_distance_x}') + LOGGER.debug(f'Maximum distance y: {max_distance_y}') + LOGGER.debug(f'Maximum distance units: {max_distance_units}') + + # TODO: assess distance units + + if not request_bbox: + LOGGER.debug('no bbox requested') + return True + + if max_distance_x is not None: + requested_distance_x = abs(request_bbox[2] - request_bbox[0]) + if requested_distance_x > max_distance_x: + exceed_msg = 'Maximum distance x exceeded' + + if max_distance_y is not None: + requested_distance_y = abs(request_bbox[3] - request_bbox[1]) + if requested_distance_y > max_distance_y: + exceed_msg = 'Maximum distance y exceeded' + + if exceed_msg is not None: + LOGGER.warning(exceed_msg) + if on_exceed == 'error': + raise ValueError(exceed_msg) + + return True diff --git a/pygeoapi/api/coverages.py b/pygeoapi/api/coverages.py index b44a15fce..767ffb1ea 100644 --- a/pygeoapi/api/coverages.py +++ b/pygeoapi/api/coverages.py @@ -51,9 +51,8 @@ ) from . import ( - APIRequest, API, F_JSON, SYSTEM_LOCALE, validate_bbox, validate_datetime, - validate_subset -) + APIRequest, API, F_JSON, SYSTEM_LOCALE, evaluate_limit_distance, + validate_bbox, validate_datetime, validate_subset) LOGGER = logging.getLogger(__name__) @@ -112,6 +111,11 @@ def get_collection_coverage( else: try: bbox = validate_bbox(bbox) + server_limits = api.config['server'].get('limits', {}) + collection_limits = api.config['resources'][dataset].get('limits', {}) # noqa + + _ = evaluate_limit_distance(request.params.get('bbox', []), + server_limits, collection_limits) except ValueError as err: msg = str(err) return api.get_exception( diff --git a/pygeoapi/api/environmental_data_retrieval.py b/pygeoapi/api/environmental_data_retrieval.py index 853559156..39b6d1537 100644 --- a/pygeoapi/api/environmental_data_retrieval.py +++ b/pygeoapi/api/environmental_data_retrieval.py @@ -48,7 +48,7 @@ from shapely.wkt import loads as shapely_loads from pygeoapi import l10n -from pygeoapi.api import evaluate_limit +from pygeoapi.api import evaluate_limit, evaluate_limit_distance from pygeoapi.formatter.base import FormatterSerializationError from pygeoapi.crs import (create_crs_transform_spec, set_content_crs_header) from pygeoapi.openapi import get_oas_30_parameters @@ -345,6 +345,12 @@ def get_collection_edr_query(api: API, request: APIRequest, LOGGER.debug('Processing cube bbox') try: bbox = validate_bbox(request.params.get('bbox')) + server_limits = api.config['server'].get('limits', {}) + collection_limits = api.config['resources'][dataset].get('limits', {}) # noqa + + _ = evaluate_limit_distance(request.params.get('bbox', []), + server_limits, collection_limits) + if not bbox and query_type == 'cube': raise ValueError('bbox parameter required by cube queries') except ValueError as err: diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 19306f57e..8e874c4b7 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -48,7 +48,7 @@ from pyproj.exceptions import CRSError from pygeoapi import l10n -from pygeoapi.api import evaluate_limit +from pygeoapi.api import evaluate_limit, evaluate_limit_distance from pygeoapi.api.pubsub import publish_message from pygeoapi.crs import (DEFAULT_CRS, DEFAULT_STORAGE_CRS, create_crs_transform_spec, get_supported_crs_list, @@ -290,9 +290,13 @@ def get_collection_items( 'level (RFC5)') LOGGER.warning(msg) try: + server_limits = api.config['server'].get('limits', {}) + collection_limits = collections[dataset].get('limits', {}) + limit = evaluate_limit(request.params.get('limit'), - api.config['server'].get('limits', {}), - collections[dataset].get('limits', {})) + server_limits, collection_limits) + _ = evaluate_limit_distance(request.params.get('bbox', []), + server_limits, collection_limits) except ValueError as err: return api.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 0f8e785c1..ab7d271f8 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -41,9 +41,8 @@ from pygeoapi.api import ( API, APIRequest, FORMAT_TYPES, F_HTML, F_JSON, F_JSONLD, F_GZIP, __version__, validate_bbox, validate_datetime, evaluate_limit, - validate_subset, landing_page, openapi_, conformance, describe_collections, - get_collection_schema, -) + evaluate_limit_distance, validate_subset, landing_page, openapi_, + conformance, describe_collections, get_collection_schema) from pygeoapi.util import yaml_load, get_api_rules, get_base_url from tests.util import (get_test_file_path, mock_api_request, mock_flask, @@ -913,3 +912,24 @@ def test_evaluate_limit(): assert evaluate_limit(None, server, collection) == 10 assert evaluate_limit('40', server, collection) == 3 + + +def test_evaluate_limit_distance(): + collection = { + 'on_exceed': 'error', + 'max_distance_x': 10, + 'max_distance_y': 10 + } + + with pytest.raises(ValueError): + assert evaluate_limit_distance( + {}, collection, request_bbox=[-180, -90, 180, 90]) + + assert evaluate_limit_distance({}, collection, + request_bbox=[-142, 42, -140, 44]) + + assert evaluate_limit_distance({}, {}, request_bbox=[-180, -90, 180, 90]) + + collection['on_exceed'] = 'throttle' + assert evaluate_limit_distance( + {}, collection, request_bbox=[-180, -90, 180, 90])