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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 08:15 +0000
1# =================================================================
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# =================================================================
43import json
44import logging
45import urllib.parse
46from copy import deepcopy
47from datetime import datetime, timezone
48from http import HTTPStatus
49from typing import Tuple
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)
65from . import APIRequest, API, SYSTEM_LOCALE
67LOGGER = logging.getLogger(__name__)
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]
78def describe_processes(api: API, request: APIRequest,
79 process: str | None = None
80 ) -> Tuple[dict, int, str]:
81 """
82 Provide processes metadata
84 :param request: A request object
85 :param process: process identifier, defaults to None to obtain
86 information about all processes
88 :returns: tuple of headers, status code, content
89 """
91 processes = []
93 headers = request.get_response_headers(**api.api_headers)
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)
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))
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
128 if process is None:
129 p2.pop('inputs')
130 p2.pop('outputs')
131 p2.pop('example', None)
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')
137 p2['outputTransmission'] = ['value']
139 p2['links'] = p2.get('links', [])
141 jobs_url = f"{api.base_url}/jobs"
142 process_url = f"{api.base_url}/processes/{key}"
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)
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)
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)
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)
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)
190 processes.append(p2)
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 }
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)
227 return headers, HTTPStatus.OK, response
229 return headers, HTTPStatus.OK, to_json(response, api.pretty_print)
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
238 :param request: A request object
239 :param job_id: id of job
241 :returns: tuple of headers, status code, content
242 """
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))
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)
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']
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
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 }
321 # TODO: translate
322 if JobStatus[job_['status']] in (
323 JobStatus.successful, JobStatus.running, JobStatus.accepted):
325 job_result_url = f"{api.base_url}/jobs/{job_['identifier']}/results" # noqa
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 }]
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]):
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 })
349 serialized_jobs['jobs'].append(job2)
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=',')
359 uri = f'{api.base_url}/jobs'
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 })
371 next_link = False
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
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 })
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'
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)
405 return headers, HTTPStatus.OK, response
407 return headers, HTTPStatus.OK, to_json(serialized_jobs,
408 api.pretty_print)
411def execute_process(api: API, request: APIRequest,
412 process_id: str) -> Tuple[dict, int, str]:
413 """
414 Execute process
416 :param request: A request object
417 :param process_id: id of process
419 :returns: tuple of headers, status code, content
420 """
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)
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)
440 try:
441 # Parse bytes data, if applicable
442 data = data.decode()
443 LOGGER.debug(data)
444 except (UnicodeDecodeError, AttributeError):
445 pass
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)
456 data_dict = data.get('inputs', {})
457 LOGGER.debug(data_dict)
459 requested_outputs = data.get('outputs')
460 LOGGER.debug(f'outputs: {requested_outputs}')
462 requested_response = data.get('response', 'raw')
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 )
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 {})
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}'
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)
505 response = {}
506 if status == JobStatus.failed:
507 response = outputs
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
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
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
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_)
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)
549 return headers, http_status, response2
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)
557 :param request: A request object
558 :param job_id: ID of job
560 :returns: tuple of headers, status code, content
561 """
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 )
573 status = JobStatus[job['status']]
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)
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)
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)
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 )
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)
620 return headers, HTTPStatus.OK, content
623def delete_job(api: API, request: APIRequest,
624 job_id: str) -> Tuple[dict, int, str]:
625 """
626 Delete a process job
628 :param job_id: job identifier
630 :returns: tuple of headers, status code, content
631 """
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"
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)
669def get_oas_30(cfg: dict, locale: str
670 ) -> tuple[list[dict[str, str]], dict[str, dict]]: # noqa
671 """
672 Get OpenAPI fragments
674 :param cfg: `dict` of configuration
675 :param locale: `str` of locale
677 :returns: `tuple` of `list` of tag objects, and `dict` of path objects
678 """
680 from pygeoapi.openapi import OPENAPI_YAML
682 LOGGER.debug('setting up processes endpoints')
684 oas = {'tags': []}
686 paths = {}
688 process_manager = get_manager(cfg)
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 }
707 LOGGER.debug('setting up processes')
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']
732 oas['tags'].append(tag)
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 }
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 }
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
793 try:
794 first_key = list(p.metadata['outputs'])[0]
795 p_output = p.metadata['outputs'][first_key]
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')
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
814 name_in_path = {
815 'name': 'jobId',
816 'in': 'path',
817 'description': 'job identifier',
818 'required': True,
819 'schema': {
820 'type': 'string'
821 }
822 }
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 }
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 }
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 }
888 return [{'name': 'processes'}, {'name': 'jobs'}], {'paths': paths}