Skip to content

Commit 0eec84f

Browse files
committed
add enhanced support for limits (RFC5) (#1856)
1 parent b4d3535 commit 0eec84f

23 files changed

+205
-89
lines changed

.github/workflows/containers.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
contents: read
2626
steps:
2727
- name: Check out the repo
28-
uses: actions/checkout@v3
28+
uses: actions/checkout@master
2929

3030
- name: Set up QEMU
3131
uses: docker/[email protected]

.github/workflows/docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ jobs:
2323
include:
2424
- python-version: '3.10'
2525
steps:
26-
- uses: actions/checkout@v2
27-
- uses: actions/setup-python@v2
26+
- uses: actions/checkout@master
27+
- uses: actions/setup-python@v5
2828
name: Setup Python ${{ matrix.python-version }}
2929
with:
3030
python-version: ${{ matrix.python-version }}

.github/workflows/flake8.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ jobs:
77
flake8_py3:
88
runs-on: ubuntu-22.04
99
steps:
10-
- uses: actions/checkout@v3
11-
- uses: actions/setup-python@v3
10+
- uses: actions/checkout@master
11+
- uses: actions/setup-python@v5
1212
name: setup Python
1313
with:
1414
python-version: '3.10'

docker/default.config.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ server:
4646
cors: true
4747
pretty_print: true
4848
admin: ${PYGEOAPI_SERVER_ADMIN:-false}
49-
limit: 10
49+
limits:
50+
defaultitems: 10
51+
maxitems: 50
5052
# templates: /path/to/templates
5153
map:
5254
url: https://tile.openstreetmap.org/{z}/{x}/{y}.png

docs/source/configuration.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ For more information related to API design rules (the ``api_rules`` property in
4949
gzip: false # default server config to gzip/compress responses to requests with gzip in the Accept-Encoding header
5050
cors: true # boolean on whether server should support CORS
5151
pretty_print: true # whether JSON responses should be pretty-printed
52-
limit: 10 # server limit on number of items to return
52+
53+
limits: # server limits on number of items to return. This property can also be defined at the resource level to override global server settings
54+
defaultitems: 10
55+
maxitems: 100
56+
maxdistance: [25, 25]
57+
on_exceed: throttle # throttle or error (default=throttle)
58+
5359
admin: false # whether to enable the Admin API
5460
5561
# optional configuration to specify a different set of templates for HTML pages. Recommend using absolute paths. Omit this to use the default provided templates

pygeoapi/api/__init__.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
Returns content from plugins and sets responses.
4141
"""
4242

43-
from collections import OrderedDict
43+
from collections import ChainMap, OrderedDict
4444
from copy import deepcopy
4545
from datetime import datetime
4646
from functools import partial
@@ -1599,3 +1599,43 @@ def validate_subset(value: str) -> dict:
15991599
subsets[subset_name] = list(map(get_typed_value, values))
16001600

16011601
return subsets
1602+
1603+
1604+
def evaluate_limit(requested: Union[None, int], server_limits: dict,
1605+
collection_limits: dict) -> int:
1606+
"""
1607+
Helper function to evaluate limit parameter
1608+
1609+
:param requested: the limit requested by the client
1610+
:param server_limits: `dict` of server limits
1611+
:param collection_limits: `dict` of collection limits
1612+
1613+
:returns: `int` of evaluated limit
1614+
"""
1615+
1616+
effective_limits = ChainMap(collection_limits, server_limits)
1617+
1618+
default = effective_limits.get('defaultitems', 10)
1619+
max_ = effective_limits.get('maxitems', 10)
1620+
on_exceed = effective_limits.get('on_exceed', 'throttle')
1621+
1622+
LOGGER.debug(f'Requested limit: {requested}')
1623+
LOGGER.debug(f'Default limit: {default}')
1624+
LOGGER.debug(f'Maximum limit: {max_}')
1625+
LOGGER.debug(f'On exceed: {on_exceed}')
1626+
1627+
if requested is None:
1628+
LOGGER.debug('no limit requested; returning default')
1629+
return default
1630+
1631+
requested2 = get_typed_value(requested)
1632+
if not isinstance(requested2, int):
1633+
raise ValueError('limit value should be an integer')
1634+
1635+
if requested2 <= 0:
1636+
raise ValueError('limit value should be strictly positive')
1637+
elif requested2 > max_ and on_exceed == 'error':
1638+
raise RuntimeError('Limit exceeded; throwing errror')
1639+
else:
1640+
LOGGER.debug('limit requested')
1641+
return min(requested2, max_)

pygeoapi/api/environmental_data_retrieval.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from shapely.wkt import loads as shapely_loads
4848

4949
from pygeoapi import l10n
50+
from pygeoapi.api import evaluate_limit
5051
from pygeoapi.plugin import load_plugin, PLUGINS
5152
from pygeoapi.provider.base import (
5253
ProviderGenericError, ProviderItemNotFoundError)
@@ -342,6 +343,16 @@ def get_collection_edr_query(api: API, request: APIRequest,
342343
HTTPStatus.BAD_REQUEST, headers, request.format,
343344
'InvalidParameterValue', msg)
344345

346+
LOGGER.debug('Processing limit parameter')
347+
try:
348+
limit = evaluate_limit(request.params.get('limit'),
349+
api.config['server'].get('limits', {}),
350+
collections[dataset].get('limits', {}))
351+
except ValueError as err:
352+
return api.get_exception(
353+
HTTPStatus.BAD_REQUEST, headers, request.format,
354+
'InvalidParameterValue', str(err))
355+
345356
query_args = dict(
346357
query_type=query_type,
347358
instance=instance,
@@ -353,8 +364,8 @@ def get_collection_edr_query(api: API, request: APIRequest,
353364
bbox=bbox,
354365
within=within,
355366
within_units=within_units,
356-
limit=int(api.config['server']['limit']),
357-
location_id=location_id,
367+
limit=limit,
368+
location_id=location_id
358369
)
359370

360371
try:

pygeoapi/api/itemtypes.py

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from pyproj.exceptions import CRSError
4949

5050
from pygeoapi import l10n
51+
from pygeoapi.api import evaluate_limit
5152
from pygeoapi.formatter.base import FormatterSerializationError
5253
from pygeoapi.linked_data import geojson2jsonld
5354
from pygeoapi.plugin import load_plugin, PLUGINS
@@ -240,33 +241,24 @@ def get_collection_items(
240241
return api.get_exception(
241242
HTTPStatus.BAD_REQUEST, headers, request.format,
242243
'InvalidParameterValue', msg)
243-
except TypeError as err:
244-
LOGGER.warning(err)
245-
offset = 0
246244
except ValueError:
247245
msg = 'offset value should be an integer'
248246
return api.get_exception(
249247
HTTPStatus.BAD_REQUEST, headers, request.format,
250248
'InvalidParameterValue', msg)
249+
except TypeError as err:
250+
LOGGER.warning(err)
251+
offset = 0
251252

252253
LOGGER.debug('Processing limit parameter')
253254
try:
254-
limit = int(request.params.get('limit'))
255-
# TODO: We should do more validation, against the min and max
256-
# allowed by the server configuration
257-
if limit <= 0:
258-
msg = 'limit value should be strictly positive'
259-
return api.get_exception(
260-
HTTPStatus.BAD_REQUEST, headers, request.format,
261-
'InvalidParameterValue', msg)
262-
except TypeError as err:
263-
LOGGER.warning(err)
264-
limit = int(api.config['server']['limit'])
265-
except ValueError:
266-
msg = 'limit value should be an integer'
255+
limit = evaluate_limit(request.params.get('limit'),
256+
api.config['server'].get('limits', {}),
257+
collections[dataset].get('limits', {}))
258+
except ValueError as err:
267259
return api.get_exception(
268260
HTTPStatus.BAD_REQUEST, headers, request.format,
269-
'InvalidParameterValue', msg)
261+
'InvalidParameterValue', str(err))
270262

271263
resulttype = request.params.get('resulttype') or 'results'
272264

@@ -693,22 +685,13 @@ def post_collection_items(
693685

694686
LOGGER.debug('Processing limit parameter')
695687
try:
696-
limit = int(request.params.get('limit'))
697-
# TODO: We should do more validation, against the min and max
698-
# allowed by the server configuration
699-
if limit <= 0:
700-
msg = 'limit value should be strictly positive'
701-
return api.get_exception(
702-
HTTPStatus.BAD_REQUEST, headers, request.format,
703-
'InvalidParameterValue', msg)
704-
except TypeError as err:
705-
LOGGER.warning(err)
706-
limit = int(api.config['server']['limit'])
707-
except ValueError:
708-
msg = 'limit value should be an integer'
688+
limit = evaluate_limit(request.params.get('limit'),
689+
api.config['server'].get('limits', {}),
690+
collections[dataset].get('limits', {}))
691+
except ValueError as err:
709692
return api.get_exception(
710693
HTTPStatus.BAD_REQUEST, headers, request.format,
711-
'InvalidParameterValue', msg)
694+
'InvalidParameterValue', str(err))
712695

713696
resulttype = request.params.get('resulttype') or 'results'
714697

pygeoapi/api/processes.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import urllib.parse
5050

5151
from pygeoapi import l10n
52+
from pygeoapi.api import evaluate_limit
5253
from pygeoapi.util import (
5354
json_serial, render_j2_template, JobStatus, RequestedProcessExecutionMode,
5455
to_json, DATETIME_FORMAT)
@@ -101,23 +102,14 @@ def describe_processes(api: API, request: APIRequest,
101102
else:
102103
LOGGER.debug('Processing limit parameter')
103104
try:
104-
limit = int(request.params.get('limit'))
105-
106-
if limit <= 0:
107-
msg = 'limit value should be strictly positive'
108-
return api.get_exception(
109-
HTTPStatus.BAD_REQUEST, headers, request.format,
110-
'InvalidParameterValue', msg)
111-
105+
limit = evaluate_limit(request.params.get('limit'),
106+
api.config['server'].get('limits', {}),
107+
{})
112108
relevant_processes = list(api.manager.processes)[:limit]
113-
except TypeError:
114-
LOGGER.debug('returning all processes')
115-
relevant_processes = api.manager.processes.keys()
116-
except ValueError:
117-
msg = 'limit value should be an integer'
109+
except ValueError as err:
118110
return api.get_exception(
119111
HTTPStatus.BAD_REQUEST, headers, request.format,
120-
'InvalidParameterValue', msg)
112+
'InvalidParameterValue', str(err))
121113

122114
for key in relevant_processes:
123115
p = api.manager.get_processor(key)
@@ -244,21 +236,13 @@ def get_jobs(api: API, request: APIRequest,
244236
**api.api_headers)
245237
LOGGER.debug('Processing limit parameter')
246238
try:
247-
limit = int(request.params.get('limit'))
248-
249-
if limit <= 0:
250-
msg = 'limit value should be strictly positive'
251-
return api.get_exception(
252-
HTTPStatus.BAD_REQUEST, headers, request.format,
253-
'InvalidParameterValue', msg)
254-
except TypeError:
255-
limit = int(api.config['server']['limit'])
256-
LOGGER.debug('returning all jobs')
257-
except ValueError:
258-
msg = 'limit value should be an integer'
239+
limit = evaluate_limit(request.params.get('limit'),
240+
api.config['server'].get('limits', {}),
241+
{})
242+
except ValueError as err:
259243
return api.get_exception(
260244
HTTPStatus.BAD_REQUEST, headers, request.format,
261-
'InvalidParameterValue', msg)
245+
'InvalidParameterValue', str(err))
262246

263247
LOGGER.debug('Processing offset parameter')
264248
try:

pygeoapi/schemas/config/pygeoapi-config-0.x.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,36 @@ properties:
6060
default: false
6161
limit:
6262
type: integer
63-
description: server limit on number of items to return
6463
default: 10
64+
description: "limit of items to return. DEPRECATED: use limits instead"
65+
limits: &x-limits
66+
type: object
67+
description: server level limits on number of items to return
68+
properties:
69+
maxitems:
70+
type: integer
71+
description: maximum limit of items to return for feature and record providers
72+
minimum: 1
73+
default: 10
74+
defaultitems:
75+
type: integer
76+
description: default limit of items to return for feature and record providers
77+
minimum: 1
78+
default: 10
79+
maxdistance:
80+
type: array
81+
description: maximum distance in x and y for all data providers
82+
minItems: 2
83+
maxItems: 2
84+
items:
85+
type: number
86+
on_exceed:
87+
type: string
88+
description: how to handle limit exceeding
89+
default: throttle
90+
enum:
91+
- error
92+
- throttle
6593
templates:
6694
type: object
6795
description: optional configuration to specify a different set of templates for HTML pages. Recommend using absolute paths. Omit this to use the default provided templates
@@ -417,6 +445,9 @@ properties:
417445
default: 'http://www.opengis.net/def/uom/ISO-8601/0/Gregorian'
418446
required:
419447
- spatial
448+
limits:
449+
<<: *x-limits
450+
description: collection level limits on number of items to return
420451
providers:
421452
type: array
422453
description: required connection information

0 commit comments

Comments
 (0)