From 3fa874146b36b10735e8c286c70b147dffdd1e21 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:37:21 +0200 Subject: [PATCH 1/2] Include Accept headers in API schema And make them more explicit. The wildcard */* will default to application/json. --- client/src/api/schema/schema.ts | 13 +++-- lib/galaxy/schema/fields.py | 10 ++++ lib/galaxy/webapps/galaxy/api/histories.py | 18 +++++- .../webapps/galaxy/api/history_contents.py | 56 ++++++++++++------- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 2720918058d5..996f9ac4f44a 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -18035,6 +18035,8 @@ export interface operations { order?: string | null; }; header?: { + /** @description Accept header to determine the response format. Default is 'application/json'. */ + accept?: "application/json" | "application/vnd.galaxy.history.contents.stats+json"; /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ "run-as"?: string | null; }; @@ -19094,6 +19096,8 @@ export interface operations { order?: string | null; }; header?: { + /** @description Accept header to determine the response format. Default is 'application/json'. */ + accept?: "application/json" | "application/vnd.galaxy.history.contents.stats+json"; /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ "run-as"?: string | null; }; @@ -19105,12 +19109,11 @@ export interface operations { }; }; responses: { - /** @description Successful Response */ + /** @description The contents of the history that match the query. */ 200: { content: { - "application/json": - | components["schemas"]["HistoryContentsResult"] - | components["schemas"]["HistoryContentsWithStatsResult"]; + "application/json": components["schemas"]["HistoryContentsResult"]; + "application/vnd.galaxy.history.contents.stats+json": components["schemas"]["HistoryContentsWithStatsResult"]; }; }; /** @description Request Error */ @@ -19686,6 +19689,8 @@ export interface operations { offset?: number | null; }; header?: { + /** @description Accept header to determine the response format. Default is 'application/json'. */ + accept?: "application/json" | "application/vnd.galaxy.task.export+json"; /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ "run-as"?: string | null; }; diff --git a/lib/galaxy/schema/fields.py b/lib/galaxy/schema/fields.py index 311a0cf105e5..46c80143d028 100644 --- a/lib/galaxy/schema/fields.py +++ b/lib/galaxy/schema/fields.py @@ -133,3 +133,13 @@ def ModelClassField(default_value): description="The name of the database model class.", json_schema_extra={"const": literal_to_value(default_value), "type": "string"}, ) + + +def accept_wildcard_defaults_to_json(v): + assert isinstance(v, str) + if v == "*/*": + return "application/json" + return v + + +AcceptHeaderValidator = BeforeValidator(accept_wildcard_defaults_to_json) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index ad4af8499c27..57b18c1f1c17 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -8,6 +8,7 @@ from typing import ( Any, List, + Literal, Optional, Union, ) @@ -33,7 +34,10 @@ FilterQueryParams, SerializationParams, ) -from galaxy.schema.fields import DecodedDatabaseIdField +from galaxy.schema.fields import ( + AcceptHeaderValidator, + DecodedDatabaseIdField, +) from galaxy.schema.history import ( HistoryIndexQueryPayload, HistorySortByEnum, @@ -163,6 +167,16 @@ class CreateHistoryFormData(CreateHistoryPayload): """Uses Form data instead of JSON""" +IndexExportsAcceptHeader = Annotated[ + Literal[ + "application/json", + "application/vnd.galaxy.task.export+json", + ], + AcceptHeaderValidator, + Header(description="Accept header to determine the response format. Default is 'application/json'."), +] + + @router.cbv class FastAPIHistories: service: HistoriesService = depends(HistoriesService) @@ -516,7 +530,7 @@ def index_exports( trans: ProvidesHistoryContext = DependsOnTrans, limit: Optional[int] = LimitQueryParam, offset: Optional[int] = OffsetQueryParam, - accept: str = Header(default="application/json", include_in_schema=False), + accept: IndexExportsAcceptHeader = "application/json", ) -> Union[JobExportHistoryArchiveListResponse, ExportTaskListResponse]: """ By default the legacy job-based history exports (jeha) are returned. diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 39d7d64c8e7d..95641f244c85 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -5,6 +5,7 @@ import logging from typing import ( List, + Literal, Optional, Union, ) @@ -22,6 +23,7 @@ Response, StreamingResponse, ) +from typing_extensions import Annotated from galaxy import util from galaxy.managers.context import ProvidesHistoryContext @@ -30,7 +32,10 @@ SerializationParams, ValueFilterQueryParams, ) -from galaxy.schema.fields import DecodedDatabaseIdField +from galaxy.schema.fields import ( + AcceptHeaderValidator, + DecodedDatabaseIdField, +) from galaxy.schema.schema import ( AnyHistoryContentItem, AnyJobStateSummary, @@ -374,6 +379,32 @@ def parse_index_jobs_summary_params( return HistoryContentsIndexJobsSummaryParams(ids=util.listify(ids), types=util.listify(types)) +HistoryIndexAcceptContentTypes = Annotated[ + Literal[ + "application/json", + "application/vnd.galaxy.history.contents.stats+json", + ], + AcceptHeaderValidator, + Header(description="Accept header to determine the response format. Default is 'application/json'."), +] + +HistoryIndexResponsesSchema = { + 200: { + "description": ("The contents of the history that match the query."), + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/HistoryContentsResult"}, # HistoryContentsResult.schema(), + }, + HistoryContentsWithStatsResult.__accept_type__: { + "schema": { # HistoryContentsWithStatsResult.schema(), + "$ref": "#/components/schemas/HistoryContentsWithStatsResult" + }, + }, + }, + }, +} + + @router.cbv class FastAPIHistoryContents: service: HistoriesContentsService = depends(HistoriesContentsService) @@ -381,6 +412,7 @@ class FastAPIHistoryContents: @router.get( "/api/histories/{history_id}/contents/{type}s", summary="Returns the contents of the given history filtered by type.", + responses=HistoryIndexResponsesSchema, operation_id="history_contents__index_typed", response_model_exclude_unset=True, ) @@ -393,7 +425,7 @@ def index_typed( legacy_params: LegacyHistoryContentsIndexParams = Depends(get_legacy_index_query_params), serialization_params: SerializationParams = Depends(query_serialization_params), filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - accept: str = Header(default="application/json", include_in_schema=False), + accept: HistoryIndexAcceptContentTypes = "application/json", ) -> Union[HistoryContentsResult, HistoryContentsWithStatsResult]: """ Return a list of either `HDA`/`HDCA` data for the history with the given ``ID``. @@ -419,23 +451,7 @@ def index_typed( "/api/histories/{history_id}/contents", name="history_contents", summary="Returns the contents of the given history.", - responses={ - 200: { - "description": ("The contents of the history that match the query."), - "content": { - "application/json": { - "schema": { # HistoryContentsResult.schema(), - "$ref": "#/components/schemas/HistoryContentsResult" - }, - }, - HistoryContentsWithStatsResult.__accept_type__: { - "schema": { # HistoryContentsWithStatsResult.schema(), - "$ref": "#/components/schemas/HistoryContentsWithStatsResult" - }, - }, - }, - }, - }, + responses=HistoryIndexResponsesSchema, operation_id="history_contents__index", response_model_exclude_unset=True, ) @@ -448,7 +464,7 @@ def index( legacy_params: LegacyHistoryContentsIndexParams = Depends(get_legacy_index_query_params), serialization_params: SerializationParams = Depends(query_serialization_params), filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - accept: str = Header(default="application/json", include_in_schema=False), + accept: HistoryIndexAcceptContentTypes = "application/json", ) -> Union[HistoryContentsResult, HistoryContentsWithStatsResult]: """ Return a list of `HDA`/`HDCA` data for the history with the given ``ID``. From aa33d29dd52a37fa478ec39659f5b7bfb7832217 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:56:22 +0200 Subject: [PATCH 2/2] Fix accept_wildcard_defaults_to_json To handle possible multiple comma-separated values in Accept header --- lib/galaxy/schema/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/schema/fields.py b/lib/galaxy/schema/fields.py index 46c80143d028..eb332962e0f0 100644 --- a/lib/galaxy/schema/fields.py +++ b/lib/galaxy/schema/fields.py @@ -137,7 +137,9 @@ def ModelClassField(default_value): def accept_wildcard_defaults_to_json(v): assert isinstance(v, str) - if v == "*/*": + # Accept header can have multiple comma separated values. + # If any of these values is the wildcard - we default to application/json. + if "*/*" in v: return "application/json" return v