From c256c10479ed84e24b518b756aaf78686a4d7daf Mon Sep 17 00:00:00 2001 From: Marco Cucchi Date: Wed, 19 Feb 2025 12:18:26 +0100 Subject: [PATCH 1/4] improve swagger doc --- cads_processing_api_service/auth.py | 4 +++- cads_processing_api_service/costing.py | 4 ++-- cads_processing_api_service/main.py | 9 ++++++++- cads_processing_api_service/utils.py | 4 +++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cads_processing_api_service/auth.py b/cads_processing_api_service/auth.py index f5a597f..e39afde 100644 --- a/cads_processing_api_service/auth.py +++ b/cads_processing_api_service/auth.py @@ -130,7 +130,9 @@ def get_auth_info( jwt: str | None = fastapi.Header( None, description="JSON Web Token", alias="Authorization" ), - portal_header: str | None = fastapi.Header(None, alias=SETTINGS.portal_header_name), + portal_header: str | None = fastapi.Header( + None, alias=SETTINGS.portal_header_name, include_in_schema=False + ), ) -> models.AuthInfo | None: """Get authentication information from the incoming HTTP request. diff --git a/cads_processing_api_service/costing.py b/cads_processing_api_service/costing.py index 311be1f..83cc98d 100644 --- a/cads_processing_api_service/costing.py +++ b/cads_processing_api_service/costing.py @@ -35,8 +35,8 @@ class RequestOrigin(str, enum.Enum): @exceptions.exception_logger def estimate_cost( process_id: str = fastapi.Path(...), - request_origin: RequestOrigin = fastapi.Query("api"), - mandatory_inputs: bool = fastapi.Query(False), + request_origin: RequestOrigin = fastapi.Query("api", include_in_schema=False), + mandatory_inputs: bool = fastapi.Query(False, include_in_schema=False), execution_content: models.Execute = fastapi.Body(...), portals: tuple[str] | None = fastapi.Depends(utils.get_portals), ) -> models.RequestCost: diff --git a/cads_processing_api_service/main.py b/cads_processing_api_service/main.py index f378112..96b8e60 100644 --- a/cads_processing_api_service/main.py +++ b/cads_processing_api_service/main.py @@ -71,6 +71,11 @@ async def lifespan(application: fastapi.FastAPI) -> AsyncGenerator[Any, None]: app = ogc_api_processes_fastapi.instantiate_app( client=clients.DatabaseClient(), # type: ignore exception_handler=exceptions.exception_handler, + title="CADS Processing API", + description=( + "This is a FastAPI-based implementation of the " + "[OGC API - Processes standard](https://ogcapi.ogc.org/processes/)." + ), ) app = exceptions.include_exception_handlers(app) # FIXME : "app.router.lifespan_context" is not officially supported and would likely break @@ -95,7 +100,9 @@ async def lifespan(application: fastapi.FastAPI) -> AsyncGenerator[Any, None]: methods=["POST"], ) -app.router.add_api_route("/metrics", starlette_exporter.handle_metrics) +app.router.add_api_route( + "/metrics", starlette_exporter.handle_metrics, include_in_schema=False +) app.add_middleware(middlewares.ProcessingPrometheusMiddleware, group_paths=True) diff --git a/cads_processing_api_service/utils.py b/cads_processing_api_service/utils.py index efd7a31..733ac99 100644 --- a/cads_processing_api_service/utils.py +++ b/cads_processing_api_service/utils.py @@ -662,7 +662,9 @@ def make_status_info( def get_portals( - portal_header: str | None = fastapi.Header(None, alias=SETTINGS.portal_header_name), + portal_header: str | None = fastapi.Header( + None, alias=SETTINGS.portal_header_name, include_in_schema=False + ), ) -> tuple[str, ...] | None: """Get the list of portals from the incoming HTTP request's header. From 4b20128bc43fbb9a565d57ccfdfcf04995e1f226 Mon Sep 17 00:00:00 2001 From: Marco Cucchi Date: Wed, 19 Feb 2025 17:23:39 +0100 Subject: [PATCH 2/4] fix type --- cads_processing_api_service/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cads_processing_api_service/main.py b/cads_processing_api_service/main.py index 96b8e60..2282043 100644 --- a/cads_processing_api_service/main.py +++ b/cads_processing_api_service/main.py @@ -96,7 +96,7 @@ async def lifespan(application: fastapi.FastAPI) -> AsyncGenerator[Any, None]: app.router.add_api_route( "/processes/{process_id}/api-request", translators.get_api_request, - description="Get API request equivalent to the submitted prrocess execution json.", + description="Get API request equivalent to the submitted process execution json.", methods=["POST"], ) From 025a680478e19eccbe41ce23f3f1aed62d776eb4 Mon Sep 17 00:00:00 2001 From: Marco Cucchi Date: Wed, 19 Feb 2025 17:32:39 +0100 Subject: [PATCH 3/4] add configurable api title and description --- cads_processing_api_service/config.py | 10 ++++++++++ cads_processing_api_service/main.py | 8 +++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cads_processing_api_service/config.py b/cads_processing_api_service/config.py index d39e9ad..b9117d2 100644 --- a/cads_processing_api_service/config.py +++ b/cads_processing_api_service/config.py @@ -31,6 +31,13 @@ logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__) +API_TITLE = "CADS Processing API" +API_DESCRIPTION = ( + "This REST API service enables the submission of processing tasks (data retrieval) to the " + "CADS system, and their consequent monitoring and management. " + "The service is based on the [OGC API - Processes standard](https://ogcapi.ogc.org/processes/)." +) + API_REQUEST_TEMPLATE = """import cdsapi dataset = "{process_id}" @@ -189,6 +196,9 @@ def profiles_api_url(self) -> str: cache_resources_maxsize: int = 1000 cache_resources_ttl: int = 10 + api_title: str = API_TITLE + api_description: str = API_DESCRIPTION + api_request_template: str = API_REQUEST_TEMPLATE api_request_max_list_length: dict[str, int] = API_REQUEST_MAX_LIST_LENGTH missing_dataset_title: str = "Dataset not available" diff --git a/cads_processing_api_service/main.py b/cads_processing_api_service/main.py index 2282043..7f79e00 100644 --- a/cads_processing_api_service/main.py +++ b/cads_processing_api_service/main.py @@ -68,14 +68,12 @@ async def lifespan(application: fastapi.FastAPI) -> AsyncGenerator[Any, None]: logger = structlog.get_logger(__name__) + app = ogc_api_processes_fastapi.instantiate_app( client=clients.DatabaseClient(), # type: ignore exception_handler=exceptions.exception_handler, - title="CADS Processing API", - description=( - "This is a FastAPI-based implementation of the " - "[OGC API - Processes standard](https://ogcapi.ogc.org/processes/)." - ), + title=SETTINGS.api_title, + description=SETTINGS.api_description, ) app = exceptions.include_exception_handlers(app) # FIXME : "app.router.lifespan_context" is not officially supported and would likely break From d7fa8e6efaa571545e48ea144b53b9d73ea535df Mon Sep 17 00:00:00 2001 From: Marco Cucchi Date: Wed, 19 Feb 2025 18:17:13 +0100 Subject: [PATCH 4/4] update swagger documentation --- cads_processing_api_service/auth.py | 11 +++-- cads_processing_api_service/clients.py | 54 ++++++++++++++++------ cads_processing_api_service/config.py | 7 ++- cads_processing_api_service/constraints.py | 2 +- cads_processing_api_service/costing.py | 2 +- cads_processing_api_service/translators.py | 2 +- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/cads_processing_api_service/auth.py b/cads_processing_api_service/auth.py index e39afde..bdfb784 100644 --- a/cads_processing_api_service/auth.py +++ b/cads_processing_api_service/auth.py @@ -40,7 +40,7 @@ def get_auth_header(pat: str | None = None, jwt: str | None = None) -> tuple[str Parameters ---------- pat : str | None, optional - Personal Access Token + API Token jwt : str | None, optional JSON Web Token @@ -125,10 +125,13 @@ def authenticate_user( def get_auth_info( pat: str | None = fastapi.Header( - None, description="Personal Access Token", alias="PRIVATE-TOKEN" + None, description="API Token.", alias="PRIVATE-TOKEN" ), jwt: str | None = fastapi.Header( - None, description="JSON Web Token", alias="Authorization" + None, + description="JSON Web Token", + alias="Authorization", + include_in_schema=False, ), portal_header: str | None = fastapi.Header( None, alias=SETTINGS.portal_header_name, include_in_schema=False @@ -139,7 +142,7 @@ def get_auth_info( Parameters ---------- pat : str | None, optional - Personal Access Token + API Token jwt : str | None, optional JSON Web Token portal_header : str | None, optional diff --git a/cads_processing_api_service/clients.py b/cads_processing_api_service/clients.py index c948955..5dfbf2d 100644 --- a/cads_processing_api_service/clients.py +++ b/cads_processing_api_service/clients.py @@ -86,9 +86,12 @@ class DatabaseClient(ogc_api_processes_fastapi.clients.BaseClient): @exceptions.exception_logger def get_processes( self, - limit: int | None = fastapi.Query(10, ge=1, le=10000), + limit: int | None = fastapi.Query( + 10, ge=1, le=10000, description="Maximum number of results to return." + ), sortby: utils.ProcessSortCriterion | None = fastapi.Query( - utils.ProcessSortCriterion.resource_uid_asc + utils.ProcessSortCriterion.resource_uid_asc, + description="Sorting criterion.", ), cursor: str | None = fastapi.Query(None, include_in_schema=False), back: bool | None = fastapi.Query(None, include_in_schema=False), @@ -148,7 +151,7 @@ def get_processes( def get_process( self, response: fastapi.Response, - process_id: str = fastapi.Path(...), + process_id: str = fastapi.Path(..., description="Process identifier."), portals: tuple[str] | None = fastapi.Depends(utils.get_portals), ) -> ogc_api_processes_fastapi.models.ProcessDescription: """Implement OGC API - Processes `GET /processes/{process_id}` endpoint. @@ -201,7 +204,7 @@ def get_process( def post_process_execution( self, request: fastapi.Request, - process_id: str = fastapi.Path(...), + process_id: str = fastapi.Path(..., description="Process identifier."), execution_content: models.Execute = fastapi.Body(...), auth_info: models.AuthInfo = fastapi.Depends(auth.get_auth_info), ) -> models.StatusInfo: @@ -330,18 +333,30 @@ def post_process_execution( @exceptions.exception_logger def get_jobs( self, - processID: list[str] | None = fastapi.Query(None), + processID: list[str] | None = fastapi.Query( + None, + description=( + "Processes identifiers. Only jobs associated to the specified " + "processes shall be included in the response." + ), + ), status: list[models.StatusCode] | None = fastapi.Query( [ ogc_api_processes_fastapi.models.StatusCode.accepted, ogc_api_processes_fastapi.models.StatusCode.running, ogc_api_processes_fastapi.models.StatusCode.successful, ogc_api_processes_fastapi.models.StatusCode.failed, - ] + ], + description=( + "Job statuses. Only jobs with the specified statuses shall be included in " + "the response." + ), + ), + limit: int | None = fastapi.Query( + 10, ge=1, le=10000, description="Maximum number of results to return." ), - limit: int | None = fastapi.Query(10, ge=1, le=10000), sortby: utils.JobSortCriterion | None = fastapi.Query( - utils.JobSortCriterion.created_at_desc + utils.JobSortCriterion.created_at_desc, description="Sorting criterion." ), cursor: str | None = fastapi.Query(None, include_in_schema=False), back: bool | None = fastapi.Query(None, include_in_schema=False), @@ -459,12 +474,21 @@ def get_jobs( @exceptions.exception_logger def get_job( self, - job_id: str = fastapi.Path(...), - qos: bool = fastapi.Query(False), - request: bool = fastapi.Query(False), - log: bool = fastapi.Query(False), + job_id: str = fastapi.Path(..., description="Job identifier."), + qos: bool = fastapi.Query( + False, description="Whether to include job qos info in the response." + ), + request: bool = fastapi.Query( + False, + description="Whether to include the sumbitted request in the response.", + ), + log: bool = fastapi.Query( + False, description="Whether to include the job's log in the response." + ), log_start_time: datetime.datetime | None = fastapi.Query( - None, alias="logStartTime" + None, + alias="logStartTime", + description="Datetime of the first log message to be returned.", ), auth_info: models.AuthInfo = fastapi.Depends(auth.get_auth_info), ) -> models.StatusInfo: @@ -589,7 +613,7 @@ def get_job( @exceptions.exception_logger def get_job_results( self, - job_id: str = fastapi.Path(...), + job_id: str = fastapi.Path(..., description="Job identifier."), auth_info: models.AuthInfo = fastapi.Depends(auth.get_auth_info), ) -> ogc_api_processes_fastapi.models.Results: """Implement OGC API - Processes `GET /jobs/{job_id}/results` endpoint. @@ -652,7 +676,7 @@ def get_job_results( @exceptions.exception_logger def delete_job( self, - job_id: str = fastapi.Path(...), + job_id: str = fastapi.Path(..., description="Job identifier."), auth_info: models.AuthInfo = fastapi.Depends(auth.get_auth_info), ) -> ogc_api_processes_fastapi.models.StatusInfo: """Implement OGC API - Processes `DELETE /jobs/{job_id}` endpoint. diff --git a/cads_processing_api_service/config.py b/cads_processing_api_service/config.py index b9117d2..735ceae 100644 --- a/cads_processing_api_service/config.py +++ b/cads_processing_api_service/config.py @@ -35,7 +35,12 @@ API_DESCRIPTION = ( "This REST API service enables the submission of processing tasks (data retrieval) to the " "CADS system, and their consequent monitoring and management. " - "The service is based on the [OGC API - Processes standard](https://ogcapi.ogc.org/processes/)." + "The service is based on the [OGC API - Processes standard](https://ogcapi.ogc.org/processes/).\n\n" + "Being based on the OGC API - Processes standard, some terminology is inherited from it. " + "In the context of this specific API, each _process_ is associated with a specific dataset " + "and enables the retrieval of data from that dataset: as such, each _process_ identifier " + "corresponds to a specific dataset identifier.\n" + "A _job_, instead, is a specific data retrieval task that has been submitted for execution." ) API_REQUEST_TEMPLATE = """import cdsapi diff --git a/cads_processing_api_service/constraints.py b/cads_processing_api_service/constraints.py index 3dfa801..d032d5c 100644 --- a/cads_processing_api_service/constraints.py +++ b/cads_processing_api_service/constraints.py @@ -11,7 +11,7 @@ @exceptions.exception_logger def apply_constraints( - process_id: str = fastapi.Path(...), + process_id: str = fastapi.Path(..., description="Process identifier."), execution_content: models.Execute = fastapi.Body(...), portals: tuple[str] | None = fastapi.Depends(utils.get_portals), ) -> dict[str, Any]: diff --git a/cads_processing_api_service/costing.py b/cads_processing_api_service/costing.py index 83cc98d..1124b16 100644 --- a/cads_processing_api_service/costing.py +++ b/cads_processing_api_service/costing.py @@ -34,7 +34,7 @@ class RequestOrigin(str, enum.Enum): @exceptions.exception_logger def estimate_cost( - process_id: str = fastapi.Path(...), + process_id: str = fastapi.Path(..., description="Process identifier."), request_origin: RequestOrigin = fastapi.Query("api", include_in_schema=False), mandatory_inputs: bool = fastapi.Query(False, include_in_schema=False), execution_content: models.Execute = fastapi.Body(...), diff --git a/cads_processing_api_service/translators.py b/cads_processing_api_service/translators.py index d6f175c..ca966ee 100644 --- a/cads_processing_api_service/translators.py +++ b/cads_processing_api_service/translators.py @@ -337,7 +337,7 @@ def format_api_request( @exceptions.exception_logger def get_api_request( - process_id: str = fastapi.Path(...), + process_id: str = fastapi.Path(..., description="Process identifier."), request: dict[str, Any] = fastapi.Body(...), ) -> dict[str, str]: """Get CADS API request equivalent to the provided processing request.