Coverage for pygeoapi/api/processes.py: 79%

307 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# Bernhard Mallinger <bernhard.mallinger@eox.at> 

10# Francesco Martinelli <francesco.martinelli@ingv.it> 

11# 

12# Copyright (c) 2026 Tom Kralidis 

13# Copyright (c) 2025 Francesco Bartoli 

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

15# Copyright (c) 2023 Ricardo Garcia Silva 

16# Copyright (c) 2024 Bernhard Mallinger 

17# Copyright (c) 2026 Francesco Martinelli 

18# 

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

20# obtaining a copy of this software and associated documentation 

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

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

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

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

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

26# conditions: 

27# 

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

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

30# 

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

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

33# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 

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

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

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

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

38# OTHER DEALINGS IN THE SOFTWARE. 

39# 

40# ================================================================= 

41 

42 

43import json 

44import logging 

45import urllib.parse 

46from copy import deepcopy 

47from datetime import datetime, timezone 

48from http import HTTPStatus 

49from typing import Tuple 

50 

51from pygeoapi import l10n 

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

53from pygeoapi.api import evaluate_limit 

54from pygeoapi.api.pubsub import publish_message 

55from pygeoapi.process.base import ( 

56 JobNotFoundError, 

57 JobResultNotFoundError, 

58 ProcessorExecuteError, 

59) 

60from pygeoapi.process.manager.base import get_manager, Subscriber 

61from pygeoapi.util import ( 

62 json_serial, render_j2_template, JobStatus, RequestedProcessExecutionMode, 

63 to_json, DATETIME_FORMAT) 

64 

65from . import APIRequest, API, SYSTEM_LOCALE 

66 

67LOGGER = logging.getLogger(__name__) 

68 

69CONFORMANCE_CLASSES = [ 

70 'http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/ogc-process-description', # noqa 

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

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

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

74 'http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/callback' 

75] 

76 

77 

78def describe_processes(api: API, request: APIRequest, 

79 process: str | None = None 

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

81 """ 

82 Provide processes metadata 

83 

84 :param request: A request object 

85 :param process: process identifier, defaults to None to obtain 

86 information about all processes 

87 

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

89 """ 

90 

91 processes = [] 

92 

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

94 

95 if process is not None: 

96 if process not in api.manager.processes.keys(): 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 msg = 'Identifier not found' 

98 return api.get_exception( 

99 HTTPStatus.NOT_FOUND, headers, 

100 request.format, 'NoSuchProcess', msg) 

101 

102 if len(api.manager.processes) > 0: 102 ↛ 192line 102 didn't jump to line 192 because the condition on line 102 was always true

103 if process is not None: 

104 relevant_processes = [process] 

105 else: 

106 LOGGER.debug('Processing limit parameter') 

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

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

109 'Please use limits at the server or collection ' 

110 'level (RFC5)') 

111 LOGGER.warning(msg) 

112 try: 

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

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

115 {}) 

116 relevant_processes = list(api.manager.processes)[:limit] 

117 except ValueError as err: 

118 return api.get_exception( 

119 HTTPStatus.BAD_REQUEST, headers, request.format, 

120 'InvalidParameterValue', str(err)) 

121 

122 for key in relevant_processes: 

123 p = api.manager.get_processor(key) 

124 p2 = l10n.translate_struct(deepcopy(p.metadata), 

125 request.locale) 

126 p2['id'] = key 

127 

128 if process is None: 

129 p2.pop('inputs') 

130 p2.pop('outputs') 

131 p2.pop('example', None) 

132 

133 p2['jobControlOptions'] = ['sync-execute'] 

134 if api.manager.is_async: 134 ↛ 137line 134 didn't jump to line 137 because the condition on line 134 was always true

135 p2['jobControlOptions'].append('async-execute') 

136 

137 p2['outputTransmission'] = ['value'] 

138 

139 p2['links'] = p2.get('links', []) 

140 

141 jobs_url = f"{api.base_url}/jobs" 

142 process_url = f"{api.base_url}/processes/{key}" 

143 

144 # TODO translation support 

145 link = { 

146 'type': FORMAT_TYPES[F_JSON], 

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

148 'href': f'{process_url}?f={F_JSON}', 

149 'title': l10n.translate('Process description as JSON', request.locale), # noqa 

150 'hreflang': api.default_locale 

151 } 

152 p2['links'].append(link) 

153 

154 link = { 

155 'type': FORMAT_TYPES[F_HTML], 

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

157 'href': f'{process_url}?f={F_HTML}', 

158 'title': l10n.translate('Process description as HTML', request.locale), # noqa 

159 'hreflang': api.default_locale 

160 } 

161 p2['links'].append(link) 

162 

163 link = { 

164 'type': FORMAT_TYPES[F_HTML], 

165 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/job-list', 

166 'href': f'{jobs_url}?f={F_HTML}', 

167 'title': l10n.translate('Jobs list as HTML', request.locale), # noqa 

168 'hreflang': api.default_locale 

169 } 

170 p2['links'].append(link) 

171 

172 link = { 

173 'type': FORMAT_TYPES[F_JSON], 

174 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/job-list', 

175 'href': f'{jobs_url}?f={F_JSON}', 

176 'title': l10n.translate('Jobs list as JSON', request.locale), # noqa 

177 'hreflang': api.default_locale 

178 } 

179 p2['links'].append(link) 

180 

181 link = { 

182 'type': FORMAT_TYPES[F_JSON], 

183 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/execute', 

184 'href': f'{process_url}/execution?f={F_JSON}', 

185 'title': l10n.translate('Execution for this process as JSON', request.locale), # noqa 

186 'hreflang': api.default_locale 

187 } 

188 p2['links'].append(link) 

189 

190 processes.append(p2) 

191 

192 if process is not None: 

193 response = processes[0] 

194 else: 

195 process_url = f"{api.base_url}/processes" 

196 response = { 

197 'processes': processes, 

198 'links': [{ 

199 'type': FORMAT_TYPES[F_JSON], 

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

201 'title': l10n.translate('This document as JSON', request.locale), # noqa 

202 'href': f'{process_url}?f={F_JSON}' 

203 }, { 

204 'type': FORMAT_TYPES[F_JSONLD], 

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

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

207 'href': f'{process_url}?f={F_JSONLD}' 

208 }, { 

209 'type': FORMAT_TYPES[F_HTML], 

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

211 'title': l10n.translate('This document as HTML', request.locale), # noqa 

212 'href': f'{process_url}?f={F_HTML}' 

213 }] 

214 } 

215 

216 if request.format == F_HTML: # render 

217 if process is not None: 

218 tpl_config = api.get_dataset_templates(process) 

219 response = render_j2_template(api.tpl_config, tpl_config, 

220 'processes/process.html', 

221 response, request.locale) 

222 else: 

223 response = render_j2_template( 

224 api.tpl_config, api.config['server']['templates'], 

225 'processes/index.html', response, request.locale) 

226 

227 return headers, HTTPStatus.OK, response 

228 

229 return headers, HTTPStatus.OK, to_json(response, api.pretty_print) 

230 

231 

232# TODO: get_jobs doesn't have tests 

233def get_jobs(api: API, request: APIRequest, 

234 job_id: str | None = None) -> Tuple[dict, int, str]: 

235 """ 

236 Get process jobs 

237 

238 :param request: A request object 

239 :param job_id: id of job 

240 

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

242 """ 

243 

244 headers = request.get_response_headers(SYSTEM_LOCALE, 

245 **api.api_headers) 

246 LOGGER.debug('Processing limit parameter') 

247 try: 

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

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

250 {}) 

251 except ValueError as err: 

252 return api.get_exception( 

253 HTTPStatus.BAD_REQUEST, headers, request.format, 

254 'InvalidParameterValue', str(err)) 

255 

256 LOGGER.debug('Processing offset parameter') 

257 try: 

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

259 if offset < 0: 

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

261 return api.get_exception( 

262 HTTPStatus.BAD_REQUEST, headers, request.format, 

263 'InvalidParameterValue', msg) 

264 except TypeError as err: 

265 LOGGER.warning(err) 

266 offset = 0 

267 except ValueError: 

268 msg = 'offset value should be an integer' 

269 return api.get_exception( 

270 HTTPStatus.BAD_REQUEST, headers, request.format, 

271 'InvalidParameterValue', msg) 

272 

273 if job_id is None: 

274 jobs_data = api.manager.get_jobs(limit=limit, offset=offset) 

275 # TODO: For pagination to work, the provider has to do the sorting. 

276 # Here we do sort again in case the provider doesn't support 

277 # pagination yet and always returns all jobs. 

278 jobs = sorted(jobs_data['jobs'], 

279 key=lambda k: k['started'], 

280 reverse=True) 

281 numberMatched = jobs_data['numberMatched'] 

282 

283 else: 

284 try: 

285 jobs = [api.manager.get_job(job_id)] 

286 except JobNotFoundError: 

287 return api.get_exception( 

288 HTTPStatus.NOT_FOUND, headers, request.format, 

289 'InvalidParameterValue', job_id) 

290 numberMatched = 1 

291 

292 serialized_jobs = { 

293 'jobs': [], 

294 'links': [{ 

295 'href': f"{api.base_url}/jobs?f={F_HTML}", 

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

297 'type': FORMAT_TYPES[F_HTML], 

298 'title': l10n.translate('Jobs list as HTML', request.locale) 

299 }, { 

300 'href': f"{api.base_url}/jobs?f={F_JSON}", 

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

302 'type': FORMAT_TYPES[F_JSON], 

303 'title': l10n.translate('Jobs list as JSON', request.locale) 

304 }] 

305 } 

306 for job_ in jobs: 

307 job2 = { 

308 'type': 'process', 

309 'processID': job_['process_id'], 

310 'jobID': job_['identifier'], 

311 'status': job_['status'], 

312 'message': job_['message'], 

313 'progress': job_['progress'], 

314 'parameters': job_.get('parameters'), 

315 'created': job_['created'], 

316 'started': job_['started'], 

317 'finished': job_['finished'], 

318 'updated': job_['updated'] 

319 } 

320 

321 # TODO: translate 

322 if JobStatus[job_['status']] in ( 

323 JobStatus.successful, JobStatus.running, JobStatus.accepted): 

324 

325 job_result_url = f"{api.base_url}/jobs/{job_['identifier']}/results" # noqa 

326 

327 job2['links'] = [{ 

328 'href': f'{job_result_url}?f={F_HTML}', 

329 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/results', 

330 'type': FORMAT_TYPES[F_HTML], 

331 'title': l10n.translate(f'Results of job as HTML', request.locale), # noqa 

332 }, { 

333 'href': f'{job_result_url}?f={F_JSON}', 

334 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/results', 

335 'type': FORMAT_TYPES[F_JSON], 

336 'title': l10n.translate(f'Results of job as JSON', request.locale), # noqa 

337 }] 

338 

339 if job_['mimetype'] not in (FORMAT_TYPES[F_JSON], 339 ↛ 349line 339 didn't jump to line 349 because the condition on line 339 was always true

340 FORMAT_TYPES[F_HTML]): 

341 

342 job2['links'].append({ 

343 'href': job_result_url, 

344 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/results', # noqa 

345 'type': job_['mimetype'], 

346 'title': f"Results of job {job_id} as {job_['mimetype']}" # noqa 

347 }) 

348 

349 serialized_jobs['jobs'].append(job2) 

350 

351 serialized_query_params = '' 

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

353 if k not in ('f', 'offset'): 353 ↛ 354line 353 didn't jump to line 354 because the condition on line 353 was never true

354 serialized_query_params += '&' 

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

356 serialized_query_params += '=' 

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

358 

359 uri = f'{api.base_url}/jobs' 

360 

361 if offset > 0: 361 ↛ 362line 361 didn't jump to line 362 because the condition on line 361 was never true

362 prev = max(0, offset - limit) 

363 serialized_jobs['links'].append( 

364 { 

365 'href': f'{uri}?offset={prev}{serialized_query_params}', 

366 'type': FORMAT_TYPES[F_JSON], 

367 'rel': 'prev', 

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

369 }) 

370 

371 next_link = False 

372 

373 if numberMatched > (limit + offset): 

374 next_link = True 

375 elif len(jobs) == limit: 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true

376 next_link = True 

377 

378 if next_link: 

379 next_ = offset + limit 

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

381 serialized_jobs['links'].append( 

382 { 

383 'href': next_href, 

384 'rel': 'next', 

385 'type': FORMAT_TYPES[F_JSON], 

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

387 }) 

388 

389 if job_id is None: 

390 j2_template = 'jobs/index.html' 

391 else: 

392 serialized_jobs = serialized_jobs['jobs'][0] 

393 j2_template = 'jobs/job.html' 

394 

395 if request.format == F_HTML: 

396 data = { 

397 'jobs': serialized_jobs, 

398 'offset': offset, 

399 'now': datetime.now(timezone.utc).strftime(DATETIME_FORMAT) 

400 } 

401 response = render_j2_template( 

402 api.tpl_config, api.config['server']['templates'], j2_template, 

403 data, request.locale) 

404 

405 return headers, HTTPStatus.OK, response 

406 

407 return headers, HTTPStatus.OK, to_json(serialized_jobs, 

408 api.pretty_print) 

409 

410 

411def execute_process(api: API, request: APIRequest, 

412 process_id: str) -> Tuple[dict, int, str]: 

413 """ 

414 Execute process 

415 

416 :param request: A request object 

417 :param process_id: id of process 

418 

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

420 """ 

421 

422 # Responses are always in US English only 

423 headers = request.get_response_headers(SYSTEM_LOCALE, 

424 **api.api_headers) 

425 if process_id not in api.manager.processes: 425 ↛ 426line 425 didn't jump to line 426 because the condition on line 425 was never true

426 msg = 'identifier not found' 

427 return api.get_exception( 

428 HTTPStatus.NOT_FOUND, headers, 

429 request.format, 'NoSuchProcess', msg) 

430 

431 data = request.data 

432 if not data: 

433 # TODO not all processes require input, e.g. time-dependent or 

434 # random value generators 

435 msg = 'missing request data' 

436 return api.get_exception( 

437 HTTPStatus.BAD_REQUEST, headers, request.format, 

438 'MissingParameterValue', msg) 

439 

440 try: 

441 # Parse bytes data, if applicable 

442 data = data.decode() 

443 LOGGER.debug(data) 

444 except (UnicodeDecodeError, AttributeError): 

445 pass 

446 

447 try: 

448 data = json.loads(data) 

449 except (json.decoder.JSONDecodeError, TypeError): 

450 # Input does not appear to be valid JSON 

451 msg = 'invalid request data' 

452 return api.get_exception( 

453 HTTPStatus.BAD_REQUEST, headers, request.format, 

454 'InvalidParameterValue', msg) 

455 

456 data_dict = data.get('inputs', {}) 

457 LOGGER.debug(data_dict) 

458 

459 requested_outputs = data.get('outputs') 

460 LOGGER.debug(f'outputs: {requested_outputs}') 

461 

462 requested_response = data.get('response', 'raw') 

463 

464 subscriber = None 

465 subscriber_dict = data.get('subscriber') 

466 if subscriber_dict: 

467 try: 

468 success_uri = subscriber_dict['successUri'] 

469 except KeyError: 

470 return api.get_exception( 

471 HTTPStatus.BAD_REQUEST, headers, request.format, 

472 'MissingParameterValue', 'Missing successUri') 

473 else: 

474 subscriber = Subscriber( 

475 # NOTE: successUri is mandatory according to the standard 

476 success_uri=success_uri, 

477 in_progress_uri=subscriber_dict.get('inProgressUri'), 

478 failed_uri=subscriber_dict.get('failedUri'), 

479 ) 

480 

481 try: 

482 execution_mode = RequestedProcessExecutionMode( 

483 request.headers.get('Prefer', request.headers.get('prefer')) 

484 ) 

485 except ValueError: 

486 execution_mode = None 

487 try: 

488 LOGGER.debug('Executing process') 

489 result = api.manager.execute_process( 

490 process_id, data_dict, execution_mode=execution_mode, 

491 requested_outputs=requested_outputs, 

492 subscriber=subscriber, 

493 requested_response=requested_response) 

494 job_id, mime_type, outputs, status, additional_headers = result 

495 headers.update(additional_headers or {}) 

496 

497 if api.manager.is_async: 497 ↛ 505line 497 didn't jump to line 505 because the condition on line 497 was always true

498 headers['Location'] = f'{api.base_url}/jobs/{job_id}' 

499 

500 except ProcessorExecuteError as err: 

501 return api.get_exception( 

502 err.http_status_code, headers, 

503 request.format, err.ogc_exception_code, err.message) 

504 

505 response = {} 

506 if status == JobStatus.failed: 

507 response = outputs 

508 

509 if requested_response == 'raw': 

510 headers['Content-Type'] = mime_type 

511 response = outputs 

512 elif status not in (JobStatus.failed, JobStatus.accepted): 512 ↛ 513line 512 didn't jump to line 513 because the condition on line 512 was never true

513 response = outputs 

514 

515 if status == JobStatus.accepted: 

516 http_status = HTTPStatus.CREATED 

517 elif status == JobStatus.failed: 517 ↛ 520line 517 didn't jump to line 520 because the condition on line 517 was always true

518 http_status = HTTPStatus.BAD_REQUEST 

519 else: 

520 http_status = HTTPStatus.OK 

521 

522 if mime_type == 'application/json': 522 ↛ 529line 522 didn't jump to line 529 because the condition on line 522 was always true

523 if requested_response == 'document': 

524 pretty_print_ = api.pretty_print 

525 else: # raw 

526 pretty_print_ = False 

527 response2 = to_json(response, pretty_print_) 

528 else: 

529 response2 = response 

530 

531 if (headers.get('Preference-Applied', '') == RequestedProcessExecutionMode.respond_async.value): # noqa 

532 LOGGER.debug('Asynchronous mode detected, returning statusInfo') 

533 response2 = { 

534 'jobID': job_id, 

535 'type': 'process', 

536 'status': status.value 

537 } 

538 response2 = to_json(response2, pretty_print_) 

539 

540 if api.pubsub_client is not None: 540 ↛ 541line 540 didn't jump to line 541 because the condition on line 540 was never true

541 LOGGER.debug('Publishing message') 

542 try: 

543 publish_message(api.pubsub_client, api.base_url, 'process', 

544 process_id, job_id, response2) 

545 except Exception as err: 

546 msg = f'Could not publish message {err}' 

547 LOGGER.warning(msg) 

548 

549 return headers, http_status, response2 

550 

551 

552def get_job_result(api: API, request: APIRequest, 

553 job_id: str) -> Tuple[dict, int, str]: 

554 """ 

555 Get result of job (instance of a process) 

556 

557 :param request: A request object 

558 :param job_id: ID of job 

559 

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

561 """ 

562 

563 headers = request.get_response_headers(SYSTEM_LOCALE, 

564 **api.api_headers) 

565 try: 

566 job = api.manager.get_job(job_id) 

567 except JobNotFoundError: 

568 return api.get_exception( 

569 HTTPStatus.NOT_FOUND, headers, 

570 request.format, 'NoSuchJob', job_id 

571 ) 

572 

573 status = JobStatus[job['status']] 

574 

575 if status == JobStatus.running: 575 ↛ 576line 575 didn't jump to line 576 because the condition on line 575 was never true

576 msg = 'job still running' 

577 return api.get_exception( 

578 HTTPStatus.NOT_FOUND, headers, 

579 request.format, 'ResultNotReady', msg) 

580 

581 elif status == JobStatus.accepted: 

582 # NOTE: this case is not mentioned in the specification 

583 msg = 'job accepted but not yet running' 

584 return api.get_exception( 

585 HTTPStatus.NOT_FOUND, headers, 

586 request.format, 'ResultNotReady', msg) 

587 

588 elif status == JobStatus.failed: 588 ↛ 594line 588 didn't jump to line 594 because the condition on line 588 was always true

589 msg = 'job failed' 

590 return api.get_exception( 

591 HTTPStatus.BAD_REQUEST, headers, request.format, 

592 'InvalidParameterValue', msg) 

593 

594 try: 

595 mimetype, job_output = api.manager.get_job_result(job_id) 

596 except JobResultNotFoundError: 

597 return api.get_exception( 

598 HTTPStatus.INTERNAL_SERVER_ERROR, headers, 

599 request.format, 'JobResultNotFound', job_id 

600 ) 

601 

602 if mimetype not in (None, FORMAT_TYPES[F_JSON]): 

603 headers['Content-Type'] = mimetype 

604 content = job_output 

605 else: 

606 if request.format == F_JSON: 

607 content = json.dumps(job_output, sort_keys=True, indent=4, 

608 default=json_serial) 

609 else: 

610 # HTML 

611 headers['Content-Type'] = "text/html" 

612 data = { 

613 'job': {'id': job_id}, 

614 'result': job_output 

615 } 

616 content = render_j2_template( 

617 api.config, api.config['server']['templates'], 

618 'jobs/results/index.html', data, request.locale) 

619 

620 return headers, HTTPStatus.OK, content 

621 

622 

623def delete_job(api: API, request: APIRequest, 

624 job_id: str) -> Tuple[dict, int, str]: 

625 """ 

626 Delete a process job 

627 

628 :param job_id: job identifier 

629 

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

631 """ 

632 

633 response_headers = request.get_response_headers( 

634 SYSTEM_LOCALE, **api.api_headers) 

635 try: 

636 success = api.manager.delete_job(job_id) 

637 except JobNotFoundError: 

638 return api.get_exception( 

639 HTTPStatus.NOT_FOUND, response_headers, request.format, 

640 'NoSuchJob', job_id 

641 ) 

642 else: 

643 if success: 643 ↛ 660line 643 didn't jump to line 660 because the condition on line 643 was always true

644 http_status = HTTPStatus.OK 

645 jobs_url = f"{api.base_url}/jobs" 

646 

647 response = { 

648 'jobID': job_id, 

649 'status': JobStatus.dismissed.value, 

650 'message': 'Job dismissed', 

651 'progress': 100, 

652 'links': [{ 

653 'href': jobs_url, 

654 'rel': 'up', 

655 'type': FORMAT_TYPES[F_JSON], 

656 'title': l10n.translate('The job list for the current process', request.locale) # noqa 

657 }] 

658 } 

659 else: 

660 return api.get_exception( 

661 HTTPStatus.INTERNAL_SERVER_ERROR, response_headers, 

662 request.format, 'InternalError', job_id 

663 ) 

664 LOGGER.info(response) 

665 # TODO: this response does not have any headers 

666 return {}, http_status, to_json(response, api.pretty_print) 

667 

668 

669def get_oas_30(cfg: dict, locale: str 

670 ) -> tuple[list[dict[str, str]], dict[str, dict]]: # noqa 

671 """ 

672 Get OpenAPI fragments 

673 

674 :param cfg: `dict` of configuration 

675 :param locale: `str` of locale 

676 

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

678 """ 

679 

680 from pygeoapi.openapi import OPENAPI_YAML 

681 

682 LOGGER.debug('setting up processes endpoints') 

683 

684 oas = {'tags': []} 

685 

686 paths = {} 

687 

688 process_manager = get_manager(cfg) 

689 

690 if len(process_manager.processes) > 0: 690 ↛ 707line 690 didn't jump to line 707 because the condition on line 690 was always true

691 paths['/processes'] = { 

692 'get': { 

693 'summary': 'Processes', 

694 'description': 'Processes', 

695 'tags': ['server'], 

696 'operationId': 'getProcesses', 

697 'parameters': [ 

698 {'$ref': '#/components/parameters/f'} 

699 ], 

700 'responses': { 

701 '200': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/ProcessList.yaml"}, # noqa 

702 'default': {'$ref': '#/components/responses/default'} 

703 } 

704 } 

705 } 

706 

707 LOGGER.debug('setting up processes') 

708 

709 for k, v in process_manager.processes.items(): 

710 if k.startswith('_'): 710 ↛ 711line 710 didn't jump to line 711 because the condition on line 710 was never true

711 LOGGER.debug(f'Skipping hidden layer: {k}') 

712 continue 

713 name = l10n.translate(k, locale) 

714 p = process_manager.get_processor(k) 

715 md_desc = l10n.translate(p.metadata['description'], locale) 

716 process_name_path = f'/processes/{name}' 

717 tag = { 

718 'name': name, 

719 'description': md_desc, 

720 'externalDocs': {} 

721 } 

722 for link in p.metadata.get('links', []): 

723 if link.get('rel', '') == 'information': 723 ↛ 724line 723 didn't jump to line 724 because the condition on line 723 was never true

724 translated_link = l10n.translate(link, locale) 

725 tag['externalDocs']['description'] = translated_link[ 

726 'rel'] 

727 tag['externalDocs']['url'] = translated_link['href'] 

728 break 

729 if len(tag['externalDocs']) == 0: 729 ↛ 732line 729 didn't jump to line 732 because the condition on line 729 was always true

730 del tag['externalDocs'] 

731 

732 oas['tags'].append(tag) 

733 

734 paths[process_name_path] = { 

735 'get': { 

736 'summary': 'Get process metadata', 

737 'description': md_desc, 

738 'tags': [name], 

739 'operationId': f'describe{name.capitalize()}Process', 

740 'parameters': [ 

741 {'$ref': '#/components/parameters/f'} 

742 ], 

743 'responses': { 

744 '200': {'$ref': '#/components/responses/200'}, 

745 'default': {'$ref': '#/components/responses/default'} 

746 } 

747 } 

748 } 

749 

750 paths[f'{process_name_path}/execution'] = { 

751 'post': { 

752 'summary': f"Process {l10n.translate(p.metadata['title'], locale)} execution", # noqa 

753 'description': md_desc, 

754 'tags': [name], 

755 'operationId': f'execute{name.capitalize()}Job', 

756 'parameters': [{ 

757 'in': 'header', 

758 'name': 'Prefer', 

759 'required': False, 

760 'description': 'Indicates client preferences, including whether the client is capable of asynchronous processing.', # noqa 

761 'schema': { 

762 'type': 'string', 

763 'enum': [] 

764 } 

765 }], 

766 'responses': { 

767 '200': {'$ref': '#/components/responses/200'}, 

768 '201': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/ExecuteAsync.yaml"}, # noqa 

769 '404': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/NotFound.yaml"}, # noqa 

770 '500': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/ServerError.yaml"}, # noqa 

771 'default': {'$ref': '#/components/responses/default'} 

772 }, 

773 'requestBody': { 

774 'description': 'Mandatory execute request JSON', 

775 'required': True, 

776 'content': { 

777 'application/json': { 

778 'schema': { 

779 '$ref': f"{OPENAPI_YAML['oapip']}/schemas/execute.yaml" # noqa 

780 } 

781 } 

782 } 

783 } 

784 } 

785 } 

786 

787 jco = p.metadata.get('jobControlOptions', ['sync-execute']) 

788 if 'sync-execute' in jco: 788 ↛ 790line 788 didn't jump to line 790 because the condition on line 788 was always true

789 paths[f'{process_name_path}/execution']['post']['parameters'][0]['schema']['enum'].append('respond-sync') # noqa 

790 if 'async-execute' in jco: 790 ↛ 793line 790 didn't jump to line 793 because the condition on line 790 was always true

791 paths[f'{process_name_path}/execution']['post']['parameters'][0]['schema']['enum'].append('respond-async') # noqa 

792 

793 try: 

794 first_key = list(p.metadata['outputs'])[0] 

795 p_output = p.metadata['outputs'][first_key] 

796 

797 if p_output.get('schema') is not None: 797 ↛ 811line 797 didn't jump to line 811 because the condition on line 797 was always true

798 LOGGER.debug('Adding output schema') 

799 content_media_type = p_output['schema'].pop('contentMediaType', 'application/json') # noqa 

800 paths[f'{process_name_path}/execution']['post']['responses']['200'] = { # noqa 

801 'description': 'Process output schema', 

802 'content': { 

803 content_media_type: { 

804 'schema': p_output['schema'] 

805 } 

806 } 

807 } 

808 except (IndexError, KeyError): 

809 LOGGER.debug('No output defined') 

810 

811 if 'example' in p.metadata: 811 ↛ 709line 811 didn't jump to line 709 because the condition on line 811 was always true

812 paths[f'{process_name_path}/execution']['post']['requestBody']['content']['application/json']['example'] = p.metadata['example'] # noqa 

813 

814 name_in_path = { 

815 'name': 'jobId', 

816 'in': 'path', 

817 'description': 'job identifier', 

818 'required': True, 

819 'schema': { 

820 'type': 'string' 

821 } 

822 } 

823 

824 paths['/jobs'] = { 

825 'get': { 

826 'summary': 'Retrieve jobs list', 

827 'description': 'Retrieve a list of jobs', 

828 'tags': ['jobs'], 

829 'operationId': 'getJobs', 

830 'responses': { 

831 '200': {'$ref': '#/components/responses/200'}, 

832 '404': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/NotFound.yaml"}, # noqa 

833 'default': {'$ref': '#/components/responses/default'} 

834 } 

835 } 

836 } 

837 

838 paths['/jobs/{jobId}'] = { 

839 'get': { 

840 'summary': 'Retrieve job details', 

841 'description': 'Retrieve job details', 

842 'tags': ['jobs'], 

843 'parameters': [ 

844 name_in_path, 

845 {'$ref': '#/components/parameters/f'} 

846 ], 

847 'operationId': 'getJob', 

848 'responses': { 

849 '200': {'$ref': '#/components/responses/200'}, 

850 '404': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/NotFound.yaml"}, # noqa 

851 'default': {'$ref': '#/components/responses/default'} 

852 } 

853 }, 

854 'delete': { 

855 'summary': 'Cancel / delete job', 

856 'description': 'Cancel / delete job', 

857 'tags': ['jobs'], 

858 'parameters': [ 

859 name_in_path 

860 ], 

861 'operationId': 'deleteJob', 

862 'responses': { 

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

864 '404': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/NotFound.yaml"}, # noqa 

865 'default': {'$ref': '#/components/responses/default'} 

866 } 

867 }, 

868 } 

869 

870 paths['/jobs/{jobId}/results'] = { 

871 'get': { 

872 'summary': 'Retrieve job results', 

873 'description': 'Retrieve job results', 

874 'tags': ['jobs'], 

875 'parameters': [ 

876 name_in_path, 

877 {'$ref': '#/components/parameters/f'} 

878 ], 

879 'operationId': 'getJobResults', 

880 'responses': { 

881 '200': {'$ref': '#/components/responses/200'}, 

882 '404': {'$ref': f"{OPENAPI_YAML['oapip']}/responses/NotFound.yaml"}, # noqa 

883 'default': {'$ref': '#/components/responses/default'} 

884 } 

885 } 

886 } 

887 

888 return [{'name': 'processes'}, {'name': 'jobs'}], {'paths': paths}