Coverage for pygeoapi/api/itemtypes.py: 71%

601 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 08:15 +0000

1# ================================================================= 

2# 

3# Authors: Tom Kralidis <tomkralidis@gmail.com> 

4# Francesco Bartoli <xbartolone@gmail.com> 

5# Sander Schaminee <sander.schaminee@geocat.net> 

6# John A Stevenson <jostev@bgs.ac.uk> 

7# Colin Blackburn <colb@bgs.ac.uk> 

8# Ricardo Garcia Silva <ricardo.garcia.silva@geobeyond.it> 

9# 

10# Copyright (c) 2026 Tom Kralidis 

11# Copyright (c) 2025 Francesco Bartoli 

12# Copyright (c) 2022 John A Stevenson and Colin Blackburn 

13# Copyright (c) 2023 Ricardo Garcia Silva 

14# 

15# Permission is hereby granted, free of charge, to any person 

16# obtaining a copy of this software and associated documentation 

17# files (the "Software"), to deal in the Software without 

18# restriction, including without limitation the rights to use, 

19# copy, modify, merge, publish, distribute, sublicense, and/or sell 

20# copies of the Software, and to permit persons to whom the 

21# Software is furnished to do so, subject to the following 

22# conditions: 

23# 

24# The above copyright notice and this permission notice shall be 

25# included in all copies or substantial portions of the Software. 

26# 

27# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 

28# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 

29# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 

30# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 

31# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 

32# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 

33# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

34# OTHER DEALINGS IN THE SOFTWARE. 

35# 

36# ================================================================= 

37 

38from collections import ChainMap 

39from copy import deepcopy 

40from datetime import datetime, UTC 

41from http import HTTPStatus 

42import logging 

43from typing import Any, Tuple, Union 

44import urllib.parse 

45 

46from pygeofilter.parsers.ecql import parse as parse_ecql_text 

47from pygeofilter.parsers.cql2_json import parse as parse_cql2_json 

48from pyproj.exceptions import CRSError 

49 

50from pygeoapi import l10n 

51from pygeoapi.api import evaluate_limit 

52from pygeoapi.api.pubsub import publish_message 

53from pygeoapi.crs import (DEFAULT_CRS, DEFAULT_STORAGE_CRS, 

54 create_crs_transform_spec, get_supported_crs_list, 

55 modify_pygeofilter, transform_bbox, 

56 set_content_crs_header) 

57from pygeoapi.formats import F_JSON, FORMAT_TYPES, F_HTML, F_JSONLD 

58from pygeoapi.formatter.base import FormatterSerializationError 

59from pygeoapi.linked_data import geojson2jsonld 

60from pygeoapi.openapi import get_oas_30_parameters 

61from pygeoapi.plugin import load_plugin, PLUGINS 

62from pygeoapi.provider import filter_providers_by_type, get_provider_by_type 

63from pygeoapi.provider.base import ( 

64 ProviderGenericError, ProviderItemNotFoundError, 

65 ProviderTypeError, SchemaType) 

66 

67from pygeoapi.util import (to_json, filter_dict_by_key_value, str2bool, 

68 render_j2_template, get_dataset_formatters) 

69 

70from . import APIRequest, API, SYSTEM_LOCALE, validate_bbox, validate_datetime 

71 

72LOGGER = logging.getLogger(__name__) 

73 

74OGC_RELTYPES_BASE = 'http://www.opengis.net/def/rel/ogc/1.0' 

75 

76 

77CONFORMANCE_CLASSES_FEATURES = [ 

78 'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core', 

79 'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30', 

80 'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html', 

81 'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson', 

82 'http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs', 

83 'http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables', 

84 'http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables-query-parameters', # noqa 

85 'http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/create-replace-delete', # noqa 

86 'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/schemas', 

87 'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/core-roles-features' # noqa 

88] 

89 

90CONFORMANCE_CLASSES_RECORDS = [ 

91 'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/core', 

92 'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/sorting', 

93 'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/opensearch', 

94 'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/json', 

95 'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/html' 

96] 

97 

98 

99def get_collection_queryables(api: API, request: Union[APIRequest, Any], 

100 dataset: str | None = None 

101 ) -> Tuple[dict, int, str]: 

102 """ 

103 Provide collection queryables 

104 

105 :param request: A request object 

106 :param dataset: name of collection 

107 

108 :returns: tuple of headers, status code, content 

109 """ 

110 

111 domains = {} 

112 headers = request.get_response_headers(**api.api_headers) 

113 

114 if any([dataset is None, 114 ↛ 117line 114 didn't jump to line 117 because the condition on line 114 was never true

115 dataset not in api.config['resources'].keys()]): 

116 

117 msg = 'Collection not found' 

118 return api.get_exception( 

119 HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg) 

120 

121 LOGGER.debug('Creating collection queryables') 

122 

123 p = None 

124 for pt in ['feature', 'coverage', 'record']: 124 ↛ 133line 124 didn't jump to line 133 because the loop on line 124 didn't complete

125 try: 

126 LOGGER.debug(f'Loading {pt} provider') 

127 p = load_plugin('provider', get_provider_by_type( 

128 api.config['resources'][dataset]['providers'], pt)) 

129 break 

130 except ProviderTypeError: 

131 LOGGER.debug(f'Providing type {pt} not found') 

132 

133 if p is None: 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true

134 msg = 'queryables not available for this collection' 

135 return api.get_exception( 

136 

137 HTTPStatus.BAD_REQUEST, headers, request.format, 

138 'NoApplicableError', msg) 

139 

140 LOGGER.debug('Processing profile') 

141 profile = request.params.get('profile', '') 

142 

143 LOGGER.debug('Processing properties') 

144 val = request.params.get('properties') 

145 if val is not None: 

146 properties = [x for x in val.split(',') if x] 

147 properties_to_check = set(p.properties) | set(p.fields.keys()) 

148 

149 if len(list(set(properties) - set(properties_to_check))) > 0: 

150 msg = 'unknown properties specified' 

151 return api.get_exception( 

152 HTTPStatus.BAD_REQUEST, headers, request.format, 

153 'InvalidParameterValue', msg) 

154 else: 

155 properties = [] 

156 

157 queryables_id = f'{api.get_collections_url()}/{dataset}/queryables' 

158 

159 if request.params: 

160 queryables_id += '?' + urllib.parse.urlencode(request.params) 

161 

162 queryables = { 

163 'type': 'object', 

164 'title': l10n.translate( 

165 api.config['resources'][dataset]['title'], request.locale), 

166 'properties': {}, 

167 '$schema': 'http://json-schema.org/draft/2019-09/schema', 

168 '$id': queryables_id 

169 } 

170 

171 if p.fields: 171 ↛ 177line 171 didn't jump to line 177 because the condition on line 171 was always true

172 queryables['properties']['geometry'] = { 

173 'format': 'geometry-any', 

174 'x-ogc-role': 'primary-geometry' 

175 } 

176 

177 if profile == 'actual-domain': 

178 try: 

179 domains, _ = p.get_domains(properties) 

180 except NotImplementedError: 

181 LOGGER.debug('Domains are not suported by this provider') 

182 domains = {} 

183 

184 for k, v in p.fields.items(): 

185 show_field = False 

186 if properties and k not in properties: 

187 continue 

188 if p.properties: 188 ↛ 189line 188 didn't jump to line 189 because the condition on line 188 was never true

189 if k in p.properties: 

190 show_field = True 

191 else: 

192 show_field = True 

193 

194 if show_field: 194 ↛ 184line 194 didn't jump to line 184 because the condition on line 194 was always true

195 queryables['properties'][k] = { 

196 'title': k, 

197 'type': v['type'] 

198 } 

199 if v['type'] == 'float': 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

200 queryables['properties'][k]['type'] = 'number' 

201 if v.get('format') is not None: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 queryables['properties'][k]['format'] = v['format'] 

203 if 'values' in v: 203 ↛ 204line 203 didn't jump to line 204 because the condition on line 203 was never true

204 queryables['properties'][k]['enum'] = v['values'] 

205 

206 if k == p.id_field: 

207 queryables['properties'][k]['x-ogc-role'] = 'id' 

208 if k == p.time_field: 

209 queryables['properties'][k]['x-ogc-role'] = 'primary-instant' # noqa 

210 if domains.get(k): 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true

211 queryables['properties'][k]['enum'] = domains[k] 

212 

213 if request.format == F_HTML: # render 

214 tpl_config = api.get_dataset_templates(dataset) 

215 

216 queryables['title'] = l10n.translate( 

217 api.config['resources'][dataset]['title'], request.locale) 

218 

219 queryables['collections_path'] = api.get_collections_url() 

220 queryables['dataset_path'] = f'{api.get_collections_url()}/{dataset}' 

221 

222 content = render_j2_template(api.tpl_config, tpl_config, 

223 'collections/queryables.html', 

224 queryables, request.locale) 

225 

226 return headers, HTTPStatus.OK, content 

227 

228 headers['Content-Type'] = 'application/schema+json' 

229 

230 return headers, HTTPStatus.OK, to_json(queryables, api.pretty_print) 

231 

232 

233def get_collection_items( 

234 api: API, request: Union[APIRequest, Any], 

235 dataset: str | None = None) -> Tuple[dict, int, str]: 

236 """ 

237 Queries collection 

238 

239 :param request: A request object 

240 :param dataset: dataset name 

241 

242 :returns: tuple of headers, status code, content 

243 """ 

244 

245 # Set Content-Language to system locale until provider locale 

246 # has been determined 

247 headers = request.get_response_headers(SYSTEM_LOCALE, 

248 **api.api_headers) 

249 

250 properties = [] 

251 reserved_fieldnames = ['bbox', 'bbox-crs', 'crs', 'f', 'lang', 'limit', 

252 'offset', 'resulttype', 'datetime', 'sortby', 

253 'properties', 'skipGeometry', 'q', 

254 'filter', 'filter-lang', 'filter-crs'] 

255 

256 collections = filter_dict_by_key_value(api.config['resources'], 

257 'type', 'collection') 

258 

259 if dataset not in collections.keys(): 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true

260 msg = 'Collection not found' 

261 return api.get_exception( 

262 HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg) 

263 

264 LOGGER.debug('Processing query parameters') 

265 

266 LOGGER.debug('Processing offset parameter') 

267 try: 

268 offset = int(request.params.get('offset')) 

269 if offset < 0: 

270 msg = 'offset value should be positive or zero' 

271 return api.get_exception( 

272 HTTPStatus.BAD_REQUEST, headers, request.format, 

273 'InvalidParameterValue', msg) 

274 except ValueError: 

275 msg = 'offset value should be an integer' 

276 return api.get_exception( 

277 HTTPStatus.BAD_REQUEST, headers, request.format, 

278 'InvalidParameterValue', msg) 

279 except TypeError as err: 

280 LOGGER.warning(err) 

281 offset = 0 

282 

283 LOGGER.debug('Processing limit parameter') 

284 if api.config['server'].get('limit') is not None: 284 ↛ 285line 284 didn't jump to line 285 because the condition on line 284 was never true

285 msg = ('server.limit is no longer supported! ' 

286 'Please use limits at the server or collection ' 

287 'level (RFC5)') 

288 LOGGER.warning(msg) 

289 try: 

290 limit = evaluate_limit(request.params.get('limit'), 

291 api.config['server'].get('limits', {}), 

292 collections[dataset].get('limits', {})) 

293 except ValueError as err: 

294 return api.get_exception( 

295 HTTPStatus.BAD_REQUEST, headers, request.format, 

296 'InvalidParameterValue', str(err)) 

297 

298 resulttype = request.params.get('resulttype') or 'results' 

299 

300 LOGGER.debug('Processing bbox parameter') 

301 

302 bbox = request.params.get('bbox') 

303 

304 if bbox is None: 

305 bbox = [] 

306 else: 

307 try: 

308 bbox = validate_bbox(bbox) 

309 except ValueError as err: 

310 msg = str(err) 

311 return api.get_exception( 

312 HTTPStatus.BAD_REQUEST, headers, request.format, 

313 'InvalidParameterValue', msg) 

314 

315 LOGGER.debug('Processing datetime parameter') 

316 datetime_ = request.params.get('datetime') 

317 try: 

318 datetime_ = validate_datetime(collections[dataset]['extents'], 

319 datetime_) 

320 except ValueError as err: 

321 msg = str(err) 

322 return api.get_exception( 

323 HTTPStatus.BAD_REQUEST, headers, request.format, 

324 'InvalidParameterValue', msg) 

325 

326 LOGGER.debug('processing q parameter') 

327 q = request.params.get('q') or None 

328 

329 LOGGER.debug('Loading provider') 

330 

331 provider_def = None 

332 try: 

333 provider_type = 'feature' 

334 provider_def = get_provider_by_type( 

335 collections[dataset]['providers'], provider_type) 

336 p = load_plugin('provider', provider_def) 

337 except ProviderTypeError: 

338 try: 

339 provider_type = 'record' 

340 provider_def = get_provider_by_type( 

341 collections[dataset]['providers'], provider_type) 

342 p = load_plugin('provider', provider_def) 

343 except ProviderTypeError: 

344 msg = 'Invalid provider type' 

345 return api.get_exception( 

346 HTTPStatus.BAD_REQUEST, headers, request.format, 

347 'NoApplicableCode', msg) 

348 except ProviderGenericError as err: 

349 return api.get_exception( 

350 err.http_status_code, headers, request.format, 

351 err.ogc_exception_code, err.message) 

352 

353 LOGGER.debug('Validating requested format') 

354 dataset_formatters = get_dataset_formatters(collections[dataset]) 

355 

356 if dataset_formatters: 356 ↛ 364line 356 didn't jump to line 364 because the condition on line 356 was always true

357 LOGGER.debug(f'Dataset formatters: {dataset_formatters}') 

358 request._format = request._get_format( 

359 request.get_request_headers(request.headers), 

360 {v.f: v.mimetype for v in dataset_formatters.values()}) 

361 

362 LOGGER.debug(f'Request format: {request.format}') 

363 

364 if not request.is_valid(dataset_formatters.keys()): 

365 return api.get_format_exception(request) 

366 

367 crs_transform_spec = None 

368 if provider_type == 'feature': 368 ↛ 384line 368 didn't jump to line 384 because the condition on line 368 was always true

369 # crs query parameter is only available for OGC API - Features 

370 # right now, not for OGC API - Records. 

371 LOGGER.debug('Processing crs parameter') 

372 query_crs_uri = request.params.get('crs') 

373 try: 

374 crs_transform_spec = create_crs_transform_spec( 

375 provider_def, query_crs_uri 

376 ) 

377 except (ValueError, CRSError) as err: 

378 msg = str(err) 

379 return api.get_exception( 

380 HTTPStatus.BAD_REQUEST, headers, request.format, 

381 'InvalidParameterValue', msg) 

382 set_content_crs_header(headers, provider_def, query_crs_uri) 

383 

384 LOGGER.debug('Processing bbox-crs parameter') 

385 bbox_crs = request.params.get('bbox-crs') 

386 if bbox_crs is not None: 

387 # Validate bbox-crs parameter 

388 if len(bbox) == 0: 

389 msg = 'bbox-crs specified without bbox parameter' 

390 return api.get_exception( 

391 HTTPStatus.BAD_REQUEST, headers, request.format, 

392 'NoApplicableCode', msg) 

393 

394 if len(bbox_crs) == 0: 394 ↛ 395line 394 didn't jump to line 395 because the condition on line 394 was never true

395 msg = 'bbox-crs specified but is empty' 

396 return api.get_exception( 

397 HTTPStatus.BAD_REQUEST, headers, request.format, 

398 'NoApplicableCode', msg) 

399 

400 supported_crs_list = get_supported_crs_list(provider_def) 

401 if bbox_crs not in supported_crs_list: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true

402 msg = f'bbox-crs {bbox_crs} not supported for this collection' 

403 return api.get_exception( 

404 HTTPStatus.BAD_REQUEST, headers, request.format, 

405 'NoApplicableCode', msg) 

406 elif len(bbox) > 0: 406 ↛ 408line 406 didn't jump to line 408 because the condition on line 406 was never true

407 # bbox but no bbox-crs param: assume bbox is in default CRS 

408 bbox_crs = DEFAULT_STORAGE_CRS 

409 

410 # Transform bbox to storage_crs 

411 # when bbox-crs different from storage_crs. 

412 if len(bbox) > 0: 

413 try: 

414 # Get a pyproj CRS instance for the Collection's Storage CRS 

415 storage_crs = provider_def.get('storage_crs', DEFAULT_STORAGE_CRS) 

416 

417 # Do the (optional) Transform to the Storage CRS 

418 bbox = transform_bbox(bbox, bbox_crs, storage_crs) 

419 except CRSError as e: 

420 return api.get_exception( 

421 HTTPStatus.BAD_REQUEST, headers, request.format, 

422 'NoApplicableCode', str(e)) 

423 

424 LOGGER.debug('processing property parameters') 

425 for k, v in request.params.items(): 

426 include_query_param = False 

427 if k not in reserved_fieldnames: 

428 if k in list(p.fields.keys()) or p.include_extra_query_parameters: 

429 include_query_param = True 

430 

431 if include_query_param: 

432 LOGGER.debug(f'Including query parameter {k}={v}') 

433 properties.append((k, v)) 

434 else: 

435 LOGGER.debug(f'Discarding query parameter {k}={v}') 

436 

437 LOGGER.debug('processing sort parameter') 

438 val = request.params.get('sortby') 

439 

440 if val is not None: 

441 sortby = [] 

442 sorts = val.split(',') 

443 for s in sorts: 443 ↛ 460line 443 didn't jump to line 460 because the loop on line 443 didn't complete

444 prop = s 

445 order = '+' 

446 if s and s[0] in ['+', '-']: 

447 order = s[0] 

448 prop = s[1:] 

449 

450 if prop not in p.fields.keys(): 450 ↛ 456line 450 didn't jump to line 456 because the condition on line 450 was always true

451 msg = 'bad sortby property' 

452 return api.get_exception( 

453 HTTPStatus.BAD_REQUEST, headers, request.format, 

454 'InvalidParameterValue', msg) 

455 

456 sortby.append({'property': prop, 'order': order}) 

457 else: 

458 sortby = [] 

459 

460 LOGGER.debug('processing properties parameter') 

461 val = request.params.get('properties') 

462 

463 if val is not None: 

464 select_properties = val.split(',') 

465 properties_to_check = set(p.properties) | set(p.fields.keys()) 

466 

467 if (len(list(set(select_properties) - 

468 set(properties_to_check))) > 0): 

469 msg = 'unknown properties specified' 

470 return api.get_exception( 

471 HTTPStatus.BAD_REQUEST, headers, request.format, 

472 'InvalidParameterValue', msg) 

473 else: 

474 select_properties = [] 

475 

476 LOGGER.debug('processing skipGeometry parameter') 

477 val = request.params.get('skipGeometry') 

478 if val is not None: 

479 skip_geometry = str2bool(val) 

480 else: 

481 skip_geometry = False 

482 

483 LOGGER.debug('Processing filter-crs parameter') 

484 filter_crs_uri = request.params.get('filter-crs', DEFAULT_CRS) 

485 

486 LOGGER.debug('processing filter parameter') 

487 cql_text = request.params.get('filter') 

488 

489 if cql_text is not None: 489 ↛ 490line 489 didn't jump to line 490 because the condition on line 489 was never true

490 try: 

491 filter_ = parse_ecql_text(cql_text) 

492 filter_ = modify_pygeofilter( 

493 filter_, 

494 filter_crs_uri=filter_crs_uri, 

495 storage_crs_uri=provider_def.get('storage_crs'), 

496 geometry_column_name=provider_def.get('geom_field'), 

497 ) 

498 except Exception: 

499 msg = 'Bad CQL text' 

500 LOGGER.error(f'{msg}: {cql_text}') 

501 return api.get_exception( 

502 HTTPStatus.BAD_REQUEST, headers, request.format, 

503 'InvalidParameterValue', msg) 

504 elif request.data: 

505 try: 

506 request_data = request.data.decode() 

507 filter_ = parse_cql2_json(request_data) 

508 filter_ = modify_pygeofilter( 

509 filter_, 

510 filter_crs_uri=filter_crs_uri, 

511 storage_crs_uri=provider_def.get('storage_crs'), 

512 geometry_column_name=provider_def.get('geom_field'), 

513 ) 

514 except Exception: 

515 msg = 'Bad CQL JSON' 

516 LOGGER.error(f'{msg}: {request_data}') 

517 return api.get_exception( 

518 HTTPStatus.BAD_REQUEST, headers, request.format, 

519 'InvalidParameterValue', msg) 

520 else: 

521 filter_ = None 

522 

523 LOGGER.debug('Processing filter-lang parameter') 

524 filter_lang = request.params.get('filter-lang') 

525 # Currently only cql-text is handled, but it is optional 

526 if filter_lang not in [None, 'cql-json', 'cql-text']: 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true

527 msg = 'Invalid filter language' 

528 return api.get_exception( 

529 HTTPStatus.BAD_REQUEST, headers, request.format, 

530 'InvalidParameterValue', msg) 

531 # Get provider locale (if any) 

532 prv_locale = l10n.get_plugin_locale(provider_def, request.raw_locale) 

533 

534 LOGGER.debug('Querying provider') 

535 LOGGER.debug(f'offset: {offset}') 

536 LOGGER.debug(f'limit: {limit}') 

537 LOGGER.debug(f'resulttype: {resulttype}') 

538 LOGGER.debug(f'sortby: {sortby}') 

539 LOGGER.debug(f'bbox: {bbox}') 

540 if provider_type == 'feature': 540 ↛ 542line 540 didn't jump to line 542 because the condition on line 540 was always true

541 LOGGER.debug(f'crs: {query_crs_uri}') 

542 LOGGER.debug(f'datetime: {datetime_}') 

543 LOGGER.debug(f'properties: {properties}') 

544 LOGGER.debug(f'select properties: {select_properties}') 

545 LOGGER.debug(f'skipGeometry: {skip_geometry}') 

546 LOGGER.debug(f'language: {prv_locale}') 

547 LOGGER.debug(f'q: {q}') 

548 LOGGER.debug(f'cql_text: {cql_text}') 

549 LOGGER.debug(f'filter_: {filter_}') 

550 LOGGER.debug(f'filter-lang: {filter_lang}') 

551 LOGGER.debug(f'filter-crs: {filter_crs_uri}') 

552 

553 try: 

554 content = p.query(offset=offset, limit=limit, 

555 resulttype=resulttype, bbox=bbox, 

556 datetime_=datetime_, properties=properties, 

557 sortby=sortby, skip_geometry=skip_geometry, 

558 select_properties=select_properties, 

559 crs_transform_spec=crs_transform_spec, 

560 q=q, language=prv_locale, filterq=filter_) 

561 except ProviderGenericError as err: 

562 return api.get_exception( 

563 err.http_status_code, headers, request.format, 

564 err.ogc_exception_code, err.message) 

565 

566 serialized_query_params = '' 

567 for k, v in request.params.items(): 

568 if k not in ('f', 'offset'): 

569 serialized_query_params += '&' 

570 serialized_query_params += urllib.parse.quote(k, safe='') 

571 serialized_query_params += '=' 

572 serialized_query_params += urllib.parse.quote(str(v), safe=',') 

573 

574 if 'links' not in content: 574 ↛ 578line 574 didn't jump to line 578 because the condition on line 574 was always true

575 content['links'] = [] 

576 

577 # TODO: translate titles 

578 uri = f'{api.get_collections_url()}/{dataset}/items' 

579 content['links'].extend([{ 

580 'type': 'application/geo+json', 

581 'rel': request.get_linkrel(F_JSON), 

582 'title': l10n.translate('This document as GeoJSON', request.locale), 

583 'href': f'{uri}?f={F_JSON}{serialized_query_params}' 

584 }, { 

585 'rel': request.get_linkrel(F_JSONLD), 

586 'type': FORMAT_TYPES[F_JSONLD], 

587 'title': l10n.translate('This document as RDF (JSON-LD)', request.locale), # noqa 

588 'href': f'{uri}?f={F_JSONLD}{serialized_query_params}' 

589 }, { 

590 'type': FORMAT_TYPES[F_HTML], 

591 'rel': request.get_linkrel(F_HTML), 

592 'title': l10n.translate('This document as HTML', request.locale), 

593 'href': f'{uri}?f={F_HTML}{serialized_query_params}' 

594 }]) 

595 

596 for key, value in dataset_formatters.items(): 

597 content['links'].append({ 

598 'type': value.mimetype, 

599 'rel': 'alternate', 

600 'title': f'This document as {key}', 

601 'href': f'{uri}?f={value.name}{serialized_query_params}' 

602 }) 

603 

604 next_link = False 

605 prev_link = False 

606 

607 if 'next' in [link['rel'] for link in content['links']]: 607 ↛ 608line 607 didn't jump to line 608 because the condition on line 607 was never true

608 LOGGER.debug('Using next link from provider') 

609 else: 

610 if content.get('numberMatched', -1) > (limit + offset): 

611 next_link = True 

612 elif len(content['features']) == limit: 

613 next_link = True 

614 

615 if offset > 0: 

616 prev_link = True 

617 

618 print(request.format) 

619 if prev_link: 

620 prev = max(0, offset - limit) 

621 url = f'{uri}?offset={prev}{serialized_query_params}' 

622 if request.format is not None: 

623 url = f'{uri}?f={request.format}&offset={prev}{serialized_query_params}' # noqa 

624 

625 content['links'].append( 

626 { 

627 'type': 'application/geo+json', 

628 'rel': 'prev', 

629 'title': l10n.translate('Items (prev)', request.locale), 

630 'href': url 

631 }) 

632 

633 if next_link: 

634 next_ = offset + limit 

635 next_href = f'{uri}?offset={next_}{serialized_query_params}' 

636 if request.format is not None: 

637 next_href = f'{uri}?f={request.format}&offset={next_}{serialized_query_params}' # noqa 

638 content['links'].append( 

639 { 

640 'type': 'application/geo+json', 

641 'rel': 'next', 

642 'title': l10n.translate('Items (next)', request.locale), 

643 'href': next_href 

644 }) 

645 

646 content['links'].append( 

647 { 

648 'type': FORMAT_TYPES[F_JSON], 

649 'title': l10n.translate( 

650 collections[dataset]['title'], request.locale), 

651 'rel': 'collection', 

652 'href': '/'.join(uri.split('/')[:-1]) 

653 }) 

654 

655 content['timeStamp'] = datetime.now(UTC).strftime( 

656 '%Y-%m-%dT%H:%M:%S.%fZ') 

657 

658 # Set response language to requested provider locale 

659 # (if it supports language) and/or otherwise the requested pygeoapi 

660 # locale (or fallback default locale) 

661 l10n.set_response_language(headers, prv_locale, request.locale) 

662 

663 if request.format == F_HTML: # render 

664 tpl_config = api.get_dataset_templates(dataset) 

665 # For constructing proper URIs to items 

666 

667 content['itemtype'] = p.type 

668 content['items_path'] = uri 

669 content['dataset_path'] = '/'.join(uri.split('/')[:-1]) 

670 content['collections_path'] = api.get_collections_url() 

671 

672 content['offset'] = offset 

673 

674 content['id_field'] = p.id_field 

675 if p.uri_field is not None: 675 ↛ 676line 675 didn't jump to line 676 because the condition on line 675 was never true

676 content['uri_field'] = p.uri_field 

677 if p.title_field is not None: 677 ↛ 678line 677 didn't jump to line 678 because the condition on line 677 was never true

678 content['title_field'] = l10n.translate(p.title_field, 

679 request.locale) 

680 # If title exists, use it as id in html templates 

681 content['id_field'] = content['title_field'] 

682 content = render_j2_template(api.tpl_config, tpl_config, 

683 'collections/items/index.html', 

684 content, request.locale) 

685 return headers, HTTPStatus.OK, content 

686 elif request.format in [df.f for df in dataset_formatters.values()]: 

687 formatter = [v for v in dataset_formatters.values() if 

688 v.f == request.format][0] 

689 

690 try: 

691 content = formatter.write( 

692 data=content, 

693 options={ 

694 'provider_def': get_provider_by_type( 

695 collections[dataset]['providers'], 

696 'feature') 

697 } 

698 ) 

699 except FormatterSerializationError: 

700 msg = 'Error serializing output' 

701 return api.get_exception( 

702 HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, 

703 'NoApplicableCode', msg) 

704 

705 headers['Content-Type'] = formatter.mimetype 

706 

707 if formatter.attachment: 707 ↛ 708line 707 didn't jump to line 708 because the condition on line 707 was never true

708 if p.filename is None: 

709 filename = f'{dataset}.{formatter.extension}' 

710 else: 

711 filename = f'{p.filename}' 

712 

713 cd = f'attachment; filename="{filename}"' 

714 headers['Content-Disposition'] = cd 

715 

716 return headers, HTTPStatus.OK, content 

717 

718 elif request.format == F_JSONLD: 

719 content = geojson2jsonld( 

720 api, content, dataset, id_field=(p.uri_field or 'id') 

721 ) 

722 

723 return headers, HTTPStatus.OK, content 

724 

725 return headers, HTTPStatus.OK, to_json(content, api.pretty_print) 

726 

727 

728def manage_collection_item( 

729 api: API, request: APIRequest, 

730 action: str, dataset: str, 

731 identifier: str | None = None) -> Tuple[dict, int, str]: 

732 """ 

733 Adds an item to a collection 

734 

735 :param request: A request object 

736 :param action: an action among 'create', 'update', 'delete', 'options' 

737 :param dataset: dataset name 

738 

739 :returns: tuple of headers, status code, content 

740 """ 

741 

742 if not request.is_valid(PLUGINS['formatter'].keys()): 742 ↛ 743line 742 didn't jump to line 743 because the condition on line 742 was never true

743 return api.get_format_exception(request) 

744 

745 # Set Content-Language to system locale until provider locale 

746 # has been determined 

747 headers = request.get_response_headers(SYSTEM_LOCALE, **api.api_headers) 

748 

749 collections = filter_dict_by_key_value(api.config['resources'], 

750 'type', 'collection') 

751 

752 http_status = HTTPStatus.OK 

753 payload = None 

754 

755 if dataset not in collections.keys(): 755 ↛ 756line 755 didn't jump to line 756 because the condition on line 755 was never true

756 msg = 'Collection not found' 

757 return api.get_exception( 

758 HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg) 

759 

760 LOGGER.debug('Loading provider') 

761 try: 

762 provider_def = get_provider_by_type( 

763 collections[dataset]['providers'], 'feature') 

764 p = load_plugin('provider', provider_def) 

765 except ProviderTypeError: 

766 try: 

767 provider_def = get_provider_by_type( 

768 collections[dataset]['providers'], 'record') 

769 p = load_plugin('provider', provider_def) 

770 except ProviderTypeError: 

771 msg = 'Invalid provider type' 

772 return api.get_exception( 

773 HTTPStatus.BAD_REQUEST, headers, request.format, 

774 'InvalidParameterValue', msg) 

775 

776 if action == 'options': 

777 headers['Allow'] = 'HEAD, GET' 

778 if p.editable: 778 ↛ 779line 778 didn't jump to line 779 because the condition on line 778 was never true

779 if identifier is None: 

780 headers['Allow'] += ', POST' 

781 else: 

782 headers['Allow'] += ', PUT, DELETE' 

783 return headers, HTTPStatus.OK, '' 

784 

785 if not p.editable: 785 ↛ 791line 785 didn't jump to line 791 because the condition on line 785 was always true

786 msg = 'Collection is not editable' 

787 return api.get_exception( 

788 HTTPStatus.BAD_REQUEST, headers, request.format, 

789 'InvalidParameterValue', msg) 

790 

791 if action in ['create', 'update'] and not request.data: 

792 msg = 'No data found' 

793 return api.get_exception( 

794 HTTPStatus.BAD_REQUEST, headers, request.format, 

795 'InvalidParameterValue', msg) 

796 

797 if action == 'create': 

798 LOGGER.debug('Creating item') 

799 try: 

800 payload = request.data 

801 identifier = p.create(payload) 

802 except TypeError as err: 

803 msg = str(err) 

804 return api.get_exception( 

805 HTTPStatus.BAD_REQUEST, headers, request.format, 

806 'InvalidParameterValue', msg) 

807 except ProviderGenericError as err: 

808 return api.get_exception( 

809 err.http_status_code, headers, request.format, 

810 err.ogc_exception_code, err.message) 

811 

812 headers['Location'] = f'{api.get_collections_url()}/{dataset}/items/{identifier}' # noqa 

813 

814 http_status = HTTPStatus.CREATED 

815 

816 if action == 'update': 

817 LOGGER.debug('Updating item') 

818 try: 

819 payload = request.data 

820 _ = p.update(identifier, payload) 

821 except TypeError as err: 

822 msg = str(err) 

823 return api.get_exception( 

824 HTTPStatus.BAD_REQUEST, headers, request.format, 

825 'InvalidParameterValue', msg) 

826 except ProviderGenericError as err: 

827 return api.get_exception( 

828 err.http_status_code, headers, request.format, 

829 err.ogc_exception_code, err.message) 

830 

831 http_status = HTTPStatus.NO_CONTENT 

832 

833 if action == 'delete': 

834 LOGGER.debug('Deleting item') 

835 try: 

836 _ = p.get(identifier) 

837 except ProviderItemNotFoundError as err: 

838 return api.get_exception( 

839 err.http_status_code, headers, request.format, 

840 err.ogc_exception_code, err.message) 

841 

842 try: 

843 _ = p.delete(identifier) 

844 except ProviderGenericError as err: 

845 return api.get_exception( 

846 err.http_status_code, headers, request.format, 

847 err.ogc_exception_code, err.message) 

848 

849 http_status = HTTPStatus.OK 

850 

851 if api.pubsub_client is not None: 

852 LOGGER.debug('Publishing message') 

853 publish_message(api.pubsub_client, api.base_url, action, dataset, 

854 identifier, payload) 

855 

856 return headers, http_status, '' 

857 

858 

859def get_collection_item(api: API, request: APIRequest, 

860 dataset: str, identifier: str 

861 ) -> Tuple[dict, int, str]: 

862 """ 

863 Get a single collection item 

864 

865 :param request: A request object 

866 :param dataset: dataset name 

867 :param identifier: item identifier 

868 

869 :returns: tuple of headers, status code, content 

870 """ 

871 

872 # Set Content-Language to system locale until provider locale 

873 # has been determined 

874 headers = request.get_response_headers(SYSTEM_LOCALE, **api.api_headers) 

875 

876 LOGGER.debug('Processing query parameters') 

877 

878 collections = filter_dict_by_key_value(api.config['resources'], 

879 'type', 'collection') 

880 

881 if dataset not in collections.keys(): 881 ↛ 882line 881 didn't jump to line 882 because the condition on line 881 was never true

882 msg = 'Collection not found' 

883 return api.get_exception( 

884 HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg) 

885 

886 LOGGER.debug('Loading provider') 

887 

888 try: 

889 provider_type = 'feature' 

890 provider_def = get_provider_by_type( 

891 collections[dataset]['providers'], provider_type) 

892 p = load_plugin('provider', provider_def) 

893 except ProviderTypeError: 

894 try: 

895 provider_type = 'record' 

896 provider_def = get_provider_by_type( 

897 collections[dataset]['providers'], provider_type) 

898 p = load_plugin('provider', provider_def) 

899 except ProviderTypeError: 

900 msg = 'Invalid provider type' 

901 return api.get_exception( 

902 HTTPStatus.BAD_REQUEST, headers, request.format, 

903 'InvalidParameterValue', msg) 

904 except ProviderGenericError as err: 

905 return api.get_exception( 

906 err.http_status_code, headers, request.format, 

907 err.ogc_exception_code, err.message) 

908 

909 crs_transform_spec = None 

910 if provider_type == 'feature': 910 ↛ 927line 910 didn't jump to line 927 because the condition on line 910 was always true

911 # crs query parameter is only available for OGC API - Features 

912 # right now, not for OGC API - Records. 

913 LOGGER.debug('Processing crs parameter') 

914 query_crs_uri = request.params.get('crs') 

915 try: 

916 crs_transform_spec = create_crs_transform_spec( 

917 provider_def, query_crs_uri 

918 ) 

919 except (ValueError, CRSError) as err: 

920 msg = str(err) 

921 return api.get_exception( 

922 HTTPStatus.BAD_REQUEST, headers, request.format, 

923 'InvalidParameterValue', msg) 

924 set_content_crs_header(headers, provider_def, query_crs_uri) 

925 

926 # Get provider language (if any) 

927 prv_locale = l10n.get_plugin_locale(provider_def, request.raw_locale) 

928 

929 try: 

930 LOGGER.debug(f'Fetching id {identifier}') 

931 content = p.get( 

932 identifier, 

933 language=prv_locale, 

934 crs_transform_spec=crs_transform_spec, 

935 ) 

936 except ProviderGenericError as err: 

937 return api.get_exception( 

938 err.http_status_code, headers, request.format, 

939 err.ogc_exception_code, err.message) 

940 

941 if content is None: 941 ↛ 942line 941 didn't jump to line 942 because the condition on line 941 was never true

942 msg = 'identifier not found' 

943 return api.get_exception(HTTPStatus.BAD_REQUEST, headers, 

944 request.format, 'NotFound', msg) 

945 

946 uri = content['properties'].get(p.uri_field) if p.uri_field else \ 

947 f'{api.get_collections_url()}/{dataset}/items/{identifier}' 

948 

949 if 'links' not in content: 949 ↛ 952line 949 didn't jump to line 952 because the condition on line 949 was always true

950 content['links'] = [] 

951 

952 content['links'].extend([{ 

953 'type': FORMAT_TYPES[F_JSON], 

954 'rel': 'root', 

955 'title': l10n.translate('The landing page of this server as JSON', request.locale), # noqa 

956 'href': f"{api.base_url}?f={F_JSON}" 

957 }, { 

958 'type': FORMAT_TYPES[F_HTML], 

959 'rel': 'root', 

960 'title': l10n.translate('The landing page of this server as HTML', request.locale), # noqa 

961 'href': f"{api.base_url}?f={F_HTML}" 

962 }, { 

963 'rel': request.get_linkrel(F_JSON), 

964 'type': 'application/geo+json', 

965 'title': l10n.translate('This document as JSON', request.locale), 

966 'href': f'{uri}?f={F_JSON}' 

967 }, { 

968 'rel': request.get_linkrel(F_JSONLD), 

969 'type': FORMAT_TYPES[F_JSONLD], 

970 'title': l10n.translate('This document as RDF (JSON-LD)', request.locale), # noqa 

971 'href': f'{uri}?f={F_JSONLD}' 

972 }, { 

973 'rel': request.get_linkrel(F_HTML), 

974 'type': FORMAT_TYPES[F_HTML], 

975 'title': l10n.translate('This document as HTML', request.locale), 

976 'href': f'{uri}?f={F_HTML}' 

977 }, { 

978 'rel': 'collection', 

979 'type': FORMAT_TYPES[F_JSON], 

980 'title': l10n.translate(collections[dataset]['title'], 

981 request.locale), 

982 'href': f'{api.get_collections_url()}/{dataset}' 

983 }]) 

984 

985 link_request_format = ( 

986 request.format if request.format is not None else F_JSON 

987 ) 

988 if 'prev' in content: 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true

989 content['links'].append({ 

990 'rel': 'prev', 

991 'type': FORMAT_TYPES[link_request_format], 

992 'href': f"{api.get_collections_url()}/{dataset}/items/{content['prev']}?f={link_request_format}" # noqa 

993 }) 

994 if 'next' in content: 994 ↛ 995line 994 didn't jump to line 995 because the condition on line 994 was never true

995 content['links'].append({ 

996 'rel': 'next', 

997 'type': FORMAT_TYPES[link_request_format], 

998 'href': f"{api.get_collections_url()}/{dataset}/items/{content['next']}?f={link_request_format}" # noqa 

999 }) 

1000 

1001 # Set response language to requested provider locale 

1002 # (if it supports language) and/or otherwise the requested pygeoapi 

1003 # locale (or fallback default locale) 

1004 l10n.set_response_language(headers, prv_locale, request.locale) 

1005 

1006 if request.format == F_HTML: # render 

1007 tpl_config = api.get_dataset_templates(dataset) 

1008 content['title'] = l10n.translate(collections[dataset]['title'], 

1009 request.locale) 

1010 content['id_field'] = p.id_field 

1011 if p.uri_field is not None: 1011 ↛ 1012line 1011 didn't jump to line 1012 because the condition on line 1011 was never true

1012 content['uri_field'] = p.uri_field 

1013 if p.title_field is not None: 1013 ↛ 1014line 1013 didn't jump to line 1014 because the condition on line 1013 was never true

1014 content['title_field'] = l10n.translate(p.title_field, 

1015 request.locale) 

1016 content['collections_path'] = api.get_collections_url() 

1017 

1018 content = render_j2_template(api.tpl_config, tpl_config, 

1019 'collections/items/item.html', 

1020 content, request.locale) 

1021 return headers, HTTPStatus.OK, content 

1022 

1023 elif request.format == F_JSONLD: 

1024 content = geojson2jsonld( 

1025 api, content, dataset, uri, (p.uri_field or 'id') 

1026 ) 

1027 

1028 return headers, HTTPStatus.OK, content 

1029 

1030 return headers, HTTPStatus.OK, to_json(content, api.pretty_print) 

1031 

1032 

1033def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str, dict]]: # noqa 

1034 """ 

1035 Get OpenAPI fragments 

1036 

1037 :param cfg: `dict` of configuration 

1038 :param locale: `str` of locale 

1039 

1040 :returns: `tuple` of `list` of tag objects, and `dict` of path objects 

1041 """ 

1042 

1043 from pygeoapi.openapi import OPENAPI_YAML, get_visible_collections 

1044 

1045 limit = { 

1046 'name': 'limit', 

1047 'in': 'query', 

1048 'description': 'The optional limit parameter limits the number of items that are presented in the response document', # noqa 

1049 'required': False, 

1050 'schema': { 

1051 'type': 'integer', 

1052 'minimum': 1, 

1053 'maximum': 10000, 

1054 'default': 100 

1055 }, 

1056 'style': 'form', 

1057 'explode': False 

1058 } 

1059 

1060 profile = { 

1061 'name': 'profile', 

1062 'in': 'query', 

1063 'description': 'The profile to be applied to a given request', 

1064 'required': False, 

1065 'style': 'form', 

1066 'explode': False, 

1067 'schema': { 

1068 'type': 'string', 

1069 'enum': ['actual-domain', 'valid-domain'] 

1070 } 

1071 } 

1072 

1073 LOGGER.debug('setting up collection endpoints') 

1074 paths = {} 

1075 

1076 collections = filter_dict_by_key_value(cfg['resources'], 

1077 'type', 'collection') 

1078 

1079 for k, v in get_visible_collections(cfg).items(): 

1080 try: 

1081 ptype = None 

1082 

1083 if filter_providers_by_type( 1083 ↛ 1087line 1083 didn't jump to line 1087 because the condition on line 1083 was always true

1084 collections[k]['providers'], 'feature'): 

1085 ptype = 'feature' 

1086 

1087 if filter_providers_by_type( 1087 ↛ 1089line 1087 didn't jump to line 1089 because the condition on line 1087 was never true

1088 collections[k]['providers'], 'record'): 

1089 ptype = 'record' 

1090 

1091 p = load_plugin('provider', get_provider_by_type( 

1092 collections[k]['providers'], ptype)) 

1093 

1094 collection_name_path = f'/collections/{k}' 

1095 items_path = f'/collections/{k}/items' 

1096 title = l10n.translate(v['title'], locale) 

1097 description = l10n.translate(v['description'], locale) 

1098 

1099 oas_30_parameters = get_oas_30_parameters(cfg, locale) 

1100 

1101 coll_properties = deepcopy(oas_30_parameters)['properties'] 

1102 coll_properties['schema']['items']['enum'] = list(p.fields.keys()) 

1103 

1104 coll_limit = _derive_limit( 

1105 deepcopy(limit), cfg['server'].get('limits', {}), 

1106 v.get('limits', {}) 

1107 ) 

1108 

1109 dataset_formatters = get_dataset_formatters(v) 

1110 coll_f_parameter = deepcopy(oas_30_parameters)['f'] 

1111 for key, value in dataset_formatters.items(): 

1112 coll_f_parameter['schema']['enum'].append(value.f) 

1113 

1114 paths[items_path] = { 

1115 'get': { 

1116 'summary': f'Get {title} items', 

1117 'description': description, 

1118 'tags': [k], 

1119 'operationId': f'get{k.capitalize()}Features', 

1120 'parameters': [ 

1121 coll_f_parameter, 

1122 {'$ref': '#/components/parameters/lang'}, 

1123 {'$ref': '#/components/parameters/bbox'}, 

1124 coll_limit, 

1125 {'$ref': '#/components/parameters/crs'}, # noqa 

1126 {'$ref': '#/components/parameters/bbox-crs'}, 

1127 coll_properties, 

1128 {'$ref': '#/components/parameters/vendorSpecificParameters'}, # noqa 

1129 {'$ref': '#/components/parameters/skipGeometry'}, 

1130 {'$ref': f"{OPENAPI_YAML['oapir']}/parameters/sortby.yaml"}, # noqa 

1131 {'$ref': '#/components/parameters/offset'} 

1132 ], 

1133 'responses': { 

1134 '200': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/Features"}, # noqa 

1135 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa 

1136 '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa 

1137 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa 

1138 } 

1139 }, 

1140 'options': { 

1141 'summary': f'Options for {title} items', 

1142 'description': description, 

1143 'tags': [k], 

1144 'operationId': f'options{k.capitalize()}Features', 

1145 'responses': { 

1146 '200': {'description': 'options response'} 

1147 } 

1148 } 

1149 } 

1150 

1151 # TODO: update feature POSTs once updated in OGC API - Features 

1152 # https://github.com/opengeospatial/ogcapi-features/issues/771 

1153 paths[items_path]['post'] = { 

1154 'summary': f'Get {title} items with CQL2', 

1155 'description': description, 

1156 'tags': [k], 

1157 'operationId': f'getCQL2{k.capitalize()}Features', 

1158 'requestBody': { 

1159 'description': 'Get items with CQL2', 

1160 'content': { 

1161 'application/json': { # CQL2 

1162 'schema': { 

1163 '$ref': OPENAPI_YAML['cql2'] 

1164 } 

1165 } 

1166 }, 

1167 'required': True 

1168 }, 

1169 'responses': { 

1170 '200': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/Features"}, # noqa 

1171 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa 

1172 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa 

1173 } 

1174 } 

1175 

1176 if p.editable: 1176 ↛ 1177line 1176 didn't jump to line 1177 because the condition on line 1176 was never true

1177 LOGGER.debug('Provider is editable; adding post') 

1178 

1179 paths[items_path]['post']['operationId'] = f'getCQL2OrAdd{k.capitalize()}Features' # noqa 

1180 

1181 val = paths[items_path]['post']['summary'] 

1182 paths[items_path]['post']['summary'] = f'{val} or add item (see media type)' # noqa 

1183 

1184 val = paths[items_path]['post']['requestBody']['description'] 

1185 paths[items_path]['post']['requestBody']['description'] = f'{val} or add item to collection' # noqa 

1186 

1187 paths[items_path]['post']['requestBody']['content']['application/geo+json'] = { # noqa 

1188 'schema': {} 

1189 } 

1190 

1191 paths[items_path]['post']['responses']['201'] = { 

1192 'description': 'Successful creation' 

1193 } 

1194 

1195 try: 

1196 schema_ref = p.get_schema(SchemaType.create) 

1197 paths[items_path]['post']['requestBody']['content'][schema_ref[0]] = { # noqa 

1198 'schema': schema_ref[1] 

1199 } 

1200 except Exception as err: 

1201 LOGGER.debug(err) 

1202 

1203 if ptype == 'record': 1203 ↛ 1204line 1203 didn't jump to line 1204 because the condition on line 1203 was never true

1204 paths[items_path]['get']['parameters'].append( 

1205 {'$ref': f"{OPENAPI_YAML['oapir']}/parameters/q.yaml"}) 

1206 if p.fields: 1206 ↛ 1251line 1206 didn't jump to line 1251 because the condition on line 1206 was always true

1207 schema_path = f'{collection_name_path}/schema' 

1208 

1209 paths[schema_path] = { 

1210 'get': { 

1211 'summary': f'Get {title} schema', 

1212 'description': description, 

1213 'tags': [k], 

1214 'operationId': f'get{k.capitalize()}Schema', 

1215 'parameters': [ 

1216 {'$ref': '#/components/parameters/f'}, 

1217 {'$ref': '#/components/parameters/lang'} 

1218 ], 

1219 'responses': { 

1220 '200': {'$ref': '#/components/responses/Queryables'}, # noqa 

1221 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa 

1222 '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa 

1223 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"}, # noqa 

1224 } 

1225 } 

1226 } 

1227 

1228 queryables_path = f'{collection_name_path}/queryables' 

1229 

1230 paths[queryables_path] = { 

1231 'get': { 

1232 'summary': f'Get {title} queryables', 

1233 'description': description, 

1234 'tags': [k], 

1235 'operationId': f'get{k.capitalize()}Queryables', 

1236 'parameters': [ 

1237 coll_properties, 

1238 {'$ref': '#/components/parameters/f'}, 

1239 profile, 

1240 {'$ref': '#/components/parameters/lang'} 

1241 ], 

1242 'responses': { 

1243 '200': {'$ref': '#/components/responses/Queryables'}, # noqa 

1244 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa 

1245 '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa 

1246 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"}, # noqa 

1247 } 

1248 } 

1249 } 

1250 

1251 if p.time_field is not None: 

1252 paths[items_path]['get']['parameters'].append( 

1253 {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/datetime"}) # noqa 

1254 

1255 for field, type_ in p.fields.items(): 

1256 

1257 if p.properties and field not in p.properties: 1257 ↛ 1258line 1257 didn't jump to line 1258 because the condition on line 1257 was never true

1258 LOGGER.debug('Provider specified not to advertise property') # noqa 

1259 continue 

1260 

1261 if field == 'q' and ptype == 'record': 1261 ↛ 1262line 1261 didn't jump to line 1262 because the condition on line 1261 was never true

1262 LOGGER.debug('q parameter already declared, skipping') 

1263 continue 

1264 

1265 if type_ == 'date': 1265 ↛ 1266line 1265 didn't jump to line 1266 because the condition on line 1265 was never true

1266 schema = { 

1267 'type': 'string', 

1268 'format': 'date' 

1269 } 

1270 elif type_ == 'float': 1270 ↛ 1271line 1270 didn't jump to line 1271 because the condition on line 1270 was never true

1271 schema = { 

1272 'type': 'number', 

1273 'format': 'float' 

1274 } 

1275 elif type_ == 'long': 1275 ↛ 1276line 1275 didn't jump to line 1276 because the condition on line 1275 was never true

1276 schema = { 

1277 'type': 'integer', 

1278 'format': 'int64' 

1279 } 

1280 else: 

1281 schema = type_ 

1282 

1283 if schema.get('format') is None: 1283 ↛ 1286line 1283 didn't jump to line 1286 because the condition on line 1283 was always true

1284 schema.pop('format', None) 

1285 

1286 path_ = f'{collection_name_path}/items' 

1287 paths[path_]['get']['parameters'].append({ 

1288 'name': field, 

1289 'in': 'query', 

1290 'required': False, 

1291 'schema': schema, 

1292 'style': 'form', 

1293 'explode': False 

1294 }) 

1295 

1296 paths[f'{collection_name_path}/items/{{featureId}}'] = { 

1297 'get': { 

1298 'summary': f'Get {title} item by id', 

1299 'description': description, 

1300 'tags': [k], 

1301 'operationId': f'get{k.capitalize()}Feature', 

1302 'parameters': [ 

1303 {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/featureId"}, # noqa 

1304 {'$ref': '#/components/parameters/crs'}, # noqa 

1305 {'$ref': '#/components/parameters/f'}, 

1306 {'$ref': '#/components/parameters/lang'} 

1307 ], 

1308 'responses': { 

1309 '200': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/Feature"}, # noqa 

1310 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa 

1311 '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa 

1312 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa 

1313 } 

1314 }, 

1315 'options': { 

1316 'summary': f'Options for {title} item by id', 

1317 'description': description, 

1318 'tags': [k], 

1319 'operationId': f'options{k.capitalize()}Feature', 

1320 'parameters': [ 

1321 {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/featureId"} # noqa 

1322 ], 

1323 'responses': { 

1324 '200': {'description': 'options response'} 

1325 } 

1326 } 

1327 } 

1328 

1329 try: 

1330 schema_ref = p.get_schema() 

1331 paths[f'{collection_name_path}/items/{{featureId}}']['get']['responses']['200'] = { # noqa 

1332 'content': { 

1333 schema_ref[0]: { 

1334 'schema': schema_ref[1] 

1335 } 

1336 } 

1337 } 

1338 except Exception as err: 

1339 LOGGER.debug(err) 

1340 

1341 if p.editable: 1341 ↛ 1342line 1341 didn't jump to line 1342 because the condition on line 1341 was never true

1342 LOGGER.debug('Provider is editable; adding put/delete') 

1343 put_path = f'{collection_name_path}/items/{{featureId}}' # noqa 

1344 paths[put_path]['put'] = { # noqa 

1345 'summary': f'Update {title} items', 

1346 'description': description, 

1347 'tags': [k], 

1348 'operationId': f'update{k.capitalize()}Features', 

1349 'parameters': [ 

1350 {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/featureId"} # noqa 

1351 ], 

1352 'requestBody': { 

1353 'description': 'Updates item in collection', 

1354 'content': { 

1355 'application/geo+json': { 

1356 'schema': {} 

1357 } 

1358 }, 

1359 'required': True 

1360 }, 

1361 'responses': { 

1362 '204': {'$ref': '#/components/responses/204'}, 

1363 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa 

1364 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa 

1365 } 

1366 } 

1367 

1368 try: 

1369 schema_ref = p.get_schema(SchemaType.replace) 

1370 paths[put_path]['put']['requestBody']['content'][schema_ref[0]] = { # noqa 

1371 'schema': schema_ref[1] 

1372 } 

1373 except Exception as err: 

1374 LOGGER.debug(err) 

1375 

1376 paths[f'{collection_name_path}/items/{{featureId}}']['delete'] = { # noqa 

1377 'summary': f'Delete {title} items', 

1378 'description': description, 

1379 'tags': [k], 

1380 'operationId': f'delete{k.capitalize()}Features', 

1381 'parameters': [ 

1382 {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/featureId"}, # noqa 

1383 ], 

1384 'responses': { 

1385 '200': {'description': 'Successful delete'}, 

1386 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa 

1387 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa 

1388 } 

1389 } 

1390 

1391 except ProviderTypeError: 

1392 LOGGER.debug('collection is not feature/item based') 

1393 

1394 return [{'name': 'records'}, {'name': 'features'}], {'paths': paths} 

1395 

1396 

1397def _derive_limit(limit_object, server_limits, collection_limits) -> dict: 

1398 """ 

1399 Helper function to derive a limit object for a given collection 

1400 

1401 :param limit_object: OpenAPI limit parameter 

1402 :param server_limits: server level limits configuration 

1403 :param collection_limits: collection level limits configuration 

1404 

1405 :returns: updated limit object 

1406 """ 

1407 

1408 effective_limits = ChainMap(collection_limits, server_limits) 

1409 

1410 default_limit = effective_limits.get('default_items', 10) 

1411 max_limit = effective_limits.get('max_items', 10) 

1412 

1413 limit_object['schema']['default'] = default_limit 

1414 limit_object['schema']['maximum'] = max_limit 

1415 

1416 text = f' (maximum={max_limit}, default={default_limit}).' 

1417 limit_object['description'] += text 

1418 

1419 return limit_object