Skip to content

Commit 61c60e5

Browse files
committed
add support for feature/record domain queries
1 parent ab25b63 commit 61c60e5

File tree

12 files changed

+277
-29
lines changed

12 files changed

+277
-29
lines changed

docs/source/data-publishing/ogcapi-features.rst

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,24 @@ parameters.
1616

1717

1818
.. csv-table::
19-
:header: Provider, property filters/display, resulttype, bbox, datetime, sortby, skipGeometry, CQL, transactions, crs
19+
:header: Provider, property filters/display, resulttype, bbox, datetime, sortby, skipGeometry, domains, CQL, transactions, crs
2020
:align: left
2121

22-
`CSV`_,✅/✅,results/hits,❌,❌,❌,✅,❌,❌,✅
23-
`Elasticsearch`_,✅/✅,results/hits,✅,✅,✅,✅,✅,✅,✅
24-
`ERDDAP Tabledap Service`_,❌/❌,results/hits,✅,✅,❌,❌,❌,❌,✅
25-
`ESRI Feature Service`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,✅
26-
`GeoJSON`_,✅/✅,results/hits,❌,❌,❌,✅,❌,❌,✅
27-
`MongoDB`_,✅/❌,results,✅,✅,✅,✅,❌,❌,✅
28-
`OGR`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,✅
29-
`OpenSearch`_,✅/✅,results/hits,✅,✅,✅,✅,✅,✅,✅
30-
`Oracle`_,✅/✅,results/hits,✅,❌,✅,✅,❌,❌,✅
31-
`Parquet`_,✅/✅,results/hits,✅,✅,❌,✅,❌,❌,✅
32-
`PostgreSQL`_,✅/✅,results/hits,✅,✅,✅,✅,✅,✅,✅
33-
`SQLiteGPKG`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,✅
34-
`SensorThings API`_,✅/✅,results/hits,✅,✅,✅,✅,❌,✅,✅
35-
`Socrata`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,✅
36-
`TinyDB`_,✅/✅,results/hits,✅,✅,✅,✅,❌,✅,✅
22+
`CSV`_,✅/✅,results/hits,❌,❌,❌,✅,❌,❌,❌,
23+
`Elasticsearch`_,✅/✅,results/hits,✅,✅,✅,✅,✅,✅,✅,✅
24+
`ERDDAP Tabledap Service`_,❌/❌,results/hits,✅,✅,❌,❌,❌,❌,❌,
25+
`ESRI Feature Service`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,❌,
26+
`GeoJSON`_,✅/✅,results/hits,❌,❌,❌,✅,❌,❌,❌,
27+
`MongoDB`_,✅/❌,results,✅,✅,✅,✅,❌,❌,❌,
28+
`OGR`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,❌,
29+
`OpenSearch`_,✅/✅,results/hits,✅,✅,✅,✅,❌,✅,✅,✅
30+
`Oracle`_,✅/✅,results/hits,✅,❌,✅,✅,❌,❌,❌,
31+
`Parquet`_,✅/✅,results/hits,✅,✅,❌,✅,❌,❌,❌,
32+
`PostgreSQL`_,✅/✅,results/hits,✅,✅,✅,✅,❌,✅,✅,✅
33+
`SQLiteGPKG`_,✅/❌,results/hits,✅,❌,❌,✅,❌,❌,❌,
34+
`SensorThings API`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,✅,✅
35+
`Socrata`_,✅/✅,results/hits,✅,✅,✅,✅,❌,❌,❌,
36+
`TinyDB`_,✅/✅,results/hits,✅,✅,✅,✅,✅,❌,✅,✅
3737

3838
.. note::
3939

@@ -755,18 +755,35 @@ Data access examples
755755
* list all collections
756756

757757
* http://localhost:5000/collections
758+
758759
* overview of dataset
759760

760761
* http://localhost:5000/collections/foo
762+
761763
* queryables
762764

763765
* http://localhost:5000/collections/foo/queryables
766+
767+
* queryables on specific properties
768+
769+
* http://localhost:5000/collections/foo/queryables?properties=title,type
770+
771+
* queryables with current domain values
772+
773+
* http://localhost:5000/collections/foo/queryables?profile=current
774+
775+
* queryables on specific properties with current domain values
776+
777+
* http://localhost:5000/collections/foo/queryables?profile=current&properties=title,type
778+
764779
* browse features
765780

766781
* http://localhost:5000/collections/foo/items
782+
767783
* paging
768784

769785
* http://localhost:5000/collections/foo/items?offset=10&limit=10
786+
770787
* CSV outputs
771788

772789
* http://localhost:5000/collections/foo/items?f=csv
@@ -779,24 +796,31 @@ Data access examples
779796
* query features (attribute)
780797

781798
* http://localhost:5000/collections/foo/items?propertyname=foo
799+
782800
* query features (temporal)
783801

784802
* http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z
803+
785804
* query features (temporal) and sort ascending by a property (if no +/- indicated, + is assumed)
786805

787806
* http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z&sortby=+datetime
807+
788808
* query features (temporal) and sort descending by a property
789809

790810
* http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z&sortby=-datetime
811+
791812
* query features in a given (and supported) CRS
792813

793814
* http://localhost:5000/collections/foo/items?crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F32633
815+
794816
* query features in a given bounding BBOX and return in given CRS
795817

796818
* http://localhost:5000/collections/foo/items?bbox=120000,450000,130000,460000&bbox-crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F28992&crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F32633
819+
797820
* fetch a specific feature
798821

799822
* http://localhost:5000/collections/foo/items/123
823+
800824
* fetch a specific feature in a given (and supported) CRS
801825

802826
* http://localhost:5000/collections/foo/items/123?crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F32633

docs/source/data-publishing/ogcapi-records.rst

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ pygeoapi core record providers are listed below, along with a matrix of supporte
1515
parameters.
1616

1717
.. csv-table::
18-
:header: Provider, properties (filters), resulttype, q, bbox, datetime, sortby, properties (display), CQL, transactions
18+
:header: Provider, properties (filters), resulttype, q, bbox, datetime, sortby, properties (display), domains, CQL, transactions
1919
:align: left
2020

2121
`ElasticsearchCatalogue`_,✅,results/hits,✅,✅,✅,✅,✅,✅,✅
22-
`TinyDBCatalogue`_,✅,results/hits,✅,✅,✅,✅,❌,✅,✅
23-
`CSWFacade`_,✅,results/hits,✅,✅,✅,❌,❌,❌,❌
22+
`TinyDBCatalogue`_,✅,results/hits,✅,✅,✅,✅,❌,✅,✅,✅,✅
23+
`CSWFacade`_,✅,results/hits,✅,✅,✅,❌,❌,✅,❌,❌
2424

2525

2626
Below are specific connection examples based on supported providers.
@@ -107,6 +107,18 @@ Metadata search examples
107107

108108
* http://localhost:5000/collections/foo/queryables
109109

110+
* queryables on specific properties
111+
112+
* http://localhost:5000/collections/foo/queryables?properties=title,type
113+
114+
* queryables with current domain values
115+
116+
* http://localhost:5000/collections/foo/queryables?profile=current
117+
118+
* queryables on specific properties with current domain values
119+
120+
* http://localhost:5000/collections/foo/queryables?profile=current&properties=title,type
121+
110122
* browse records
111123

112124
* http://localhost:5000/collections/foo/items

pygeoapi/api/itemtypes.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Colin Blackburn <colb@bgs.ac.uk>
88
# Ricardo Garcia Silva <ricardo.garcia.silva@geobeyond.it>
99
#
10-
# Copyright (c) 2024 Tom Kralidis
10+
# Copyright (c) 2025 Tom Kralidis
1111
# Copyright (c) 2025 Francesco Bartoli
1212
# Copyright (c) 2022 John A Stevenson and Colin Blackburn
1313
# Copyright (c) 2023 Ricardo Garcia Silva
@@ -111,6 +111,7 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any],
111111
:returns: tuple of headers, status code, content
112112
"""
113113

114+
domains = {}
114115
headers = request.get_response_headers(**api.api_headers)
115116

116117
if any([dataset is None,
@@ -135,16 +136,39 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any],
135136
if p is None:
136137
msg = 'queryables not available for this collection'
137138
return api.get_exception(
139+
138140
HTTPStatus.BAD_REQUEST, headers, request.format,
139141
'NoApplicableError', msg)
140142

143+
LOGGER.debug('Processing profile')
144+
profile = request.params.get('profile', '')
145+
146+
LOGGER.debug('Processing properties')
147+
val = request.params.get('properties')
148+
if val is not None:
149+
properties = [x for x in val.split(',') if x]
150+
properties_to_check = set(p.properties) | set(p.fields.keys())
151+
152+
if len(list(set(properties) - set(properties_to_check))) > 0:
153+
msg = 'unknown properties specified'
154+
return api.get_exception(
155+
HTTPStatus.BAD_REQUEST, headers, request.format,
156+
'InvalidParameterValue', msg)
157+
else:
158+
properties = []
159+
160+
queryables_id = f'{api.get_collections_url()}/{dataset}/queryables'
161+
162+
if request.params:
163+
queryables_id += '?' + urllib.parse.urlencode(request.params)
164+
141165
queryables = {
142166
'type': 'object',
143167
'title': l10n.translate(
144168
api.config['resources'][dataset]['title'], request.locale),
145169
'properties': {},
146170
'$schema': 'http://json-schema.org/draft/2019-09/schema',
147-
'$id': f'{api.get_collections_url()}/{dataset}/queryables'
171+
'$id': queryables_id
148172
}
149173

150174
if p.fields:
@@ -153,8 +177,17 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any],
153177
'x-ogc-role': 'primary-geometry'
154178
}
155179

180+
if profile == 'current':
181+
try:
182+
domains, _ = p.get_domains(properties)
183+
except NotImplementedError:
184+
LOGGER.debug('Domains are not suported by this provider')
185+
domains = {}
186+
156187
for k, v in p.fields.items():
157188
show_field = False
189+
if properties and k not in properties:
190+
continue
158191
if p.properties:
159192
if k in p.properties:
160193
show_field = True
@@ -175,6 +208,8 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any],
175208
queryables['properties'][k]['x-ogc-role'] = 'id'
176209
if k == p.time_field:
177210
queryables['properties'][k]['x-ogc-role'] = 'primary-instant' # noqa
211+
if domains.get(k):
212+
queryables['properties'][k]['enum'] = domains[k]
178213

179214
if request.format == F_HTML: # render
180215
tpl_config = api.get_dataset_templates(dataset)
@@ -1057,6 +1092,19 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
10571092
}
10581093
}
10591094

1095+
profile = {
1096+
'name': 'profile',
1097+
'in': 'query',
1098+
'description': 'The profile to be applied to a given request',
1099+
'required': False,
1100+
'style': 'form',
1101+
'explode': False,
1102+
'schema': {
1103+
'type': 'string',
1104+
'enum': ['current']
1105+
}
1106+
}
1107+
10601108
LOGGER.debug('setting up collection endpoints')
10611109
paths = {}
10621110

@@ -1190,7 +1238,9 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
11901238
'tags': [k],
11911239
'operationId': f'get{k.capitalize()}Queryables',
11921240
'parameters': [
1241+
coll_properties,
11931242
{'$ref': '#/components/parameters/f'},
1243+
profile,
11941244
{'$ref': '#/components/parameters/lang'}
11951245
],
11961246
'responses': {

pygeoapi/django_/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# Copyright (c) 2025 Francesco Bartoli
99
# Copyright (c) 2022 Luca Delucchi
1010
# Copyright (c) 2022 Krishna Lodha
11-
# Copyright (c) 2024 Tom Kralidis
11+
# Copyright (c) 2025 Tom Kralidis
1212
#
1313
# Permission is hereby granted, free of charge, to any person
1414
# obtaining a copy of this software and associated documentation

pygeoapi/flask_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Authors: Tom Kralidis <tomkralidis@gmail.com>
44
# Norman Barker <norman.barker@gmail.com>
55
#
6-
# Copyright (c) 2024 Tom Kralidis
6+
# Copyright (c) 2025 Tom Kralidis
77
#
88
# Permission is hereby granted, free of charge, to any person
99
# obtaining a copy of this software and associated documentation

pygeoapi/provider/base.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# Authors: Tom Kralidis <tomkralidis@gmail.com>
44
#
5-
# Copyright (c) 2022 Tom Kralidis
5+
# Copyright (c) 2025 Tom Kralidis
66
#
77
# Permission is hereby granted, free of charge, to any person
88
# obtaining a copy of this software and associated documentation
@@ -145,6 +145,20 @@ def get_metadata(self):
145145

146146
raise NotImplementedError()
147147

148+
def get_domains(self, properties=[], current=False):
149+
"""
150+
Get domains from dataset
151+
152+
:param properties: `list` of property names
153+
:param current: `bool` of whether to provide list of live
154+
values (default `False`)
155+
156+
:returns: `tuple` of domains and whether they are based on the
157+
current/live dataset
158+
"""
159+
160+
raise NotImplementedError()
161+
148162
def query(self):
149163
"""
150164
query the provider

pygeoapi/provider/csw_facade.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# Authors: Tom Kralidis <tomkralidis@gmail.com>
44
#
5-
# Copyright (c) 2023 Tom Kralidis
5+
# Copyright (c) 2025 Tom Kralidis
66
#
77
# Permission is hereby granted, free of charge, to any person
88
# obtaining a copy of this software and associated documentation
@@ -91,6 +91,35 @@ def get_fields(self):
9191

9292
return self._fields
9393

94+
def get_domains(self, properties=[], current=False) -> tuple:
95+
"""
96+
Get domains from dataset
97+
98+
:param properties: `list` of property names
99+
:param current: `bool` of whether to provide list of live
100+
values (default `False`)
101+
102+
:returns: `tuple` of domains and whether they are based on the
103+
current/live dataset
104+
"""
105+
106+
LOGGER.debug(f'Querying CSW: {self.data}')
107+
records = self.query()
108+
domains = {}
109+
110+
if properties:
111+
keys = properties
112+
else:
113+
keys = records['features'][0]['properties'].keys()
114+
115+
csw = self._get_csw()
116+
117+
for key in keys:
118+
csw.getdomain(key, dtype='property')
119+
domains[key] = csw.results['values']
120+
121+
return domains, True
122+
94123
@crs_transform
95124
def query(self, offset=0, limit=10, resulttype='results',
96125
bbox=[], datetime_=None, properties=[], sortby=[],

0 commit comments

Comments
 (0)