diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 0bdc84809f33..3d737e261904 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -908,6 +908,17 @@ export interface paths { /** Prepare history for export-style download and write to supplied URI. */ post: operations["write_store_api_histories__history_id__write_store_post"]; }; + "/api/invocations": { + /** Get the list of a user's workflow invocations. */ + get: operations["index_invocations_api_invocations_get"]; + }; + "/api/invocations/from_store": { + /** + * Create Invocations From Store + * @description Create invocation(s) from a supplied model store. + */ + post: operations["create_invocations_from_store_api_invocations_from_store_post"]; + }; "/api/invocations/steps/{step_id}": { /** Show details of workflow invocation step. */ get: operations["step_api_invocations_steps__step_id__get"]; @@ -1788,6 +1799,10 @@ export interface paths { */ put: operations["enable_link_access_api_workflows__workflow_id__enable_link_access_put"]; }; + "/api/workflows/{workflow_id}/invocations": { + /** Get the list of a user's workflow invocations. */ + get: operations["index_invocations_api_workflows__workflow_id__invocations_get"]; + }; "/api/workflows/{workflow_id}/invocations/{invocation_id}": { /** * Get detailed description of a workflow invocation. @@ -1893,6 +1908,13 @@ export interface paths { */ put: operations["unpublish_api_workflows__workflow_id__unpublish_put"]; }; + "/api/workflows/{workflow_id}/usage": { + /** + * Get the list of a user's workflow invocations. + * @deprecated + */ + get: operations["index_invocations_api_workflows__workflow_id__usage_get"]; + }; "/api/workflows/{workflow_id}/usage/{invocation_id}": { /** * Get detailed description of a workflow invocation. @@ -3022,6 +3044,41 @@ export interface components { /** Store Dict */ store_dict?: Record | null; }; + /** CreateInvocationsFromStorePayload */ + CreateInvocationsFromStorePayload: { + /** + * History ID + * @description The ID of the history associated with the invocations. + * @example 0123456789ABCDEF + */ + history_id: string; + /** + * Legacy Job State + * @deprecated + * @description Populate the invocation step state with the job state instead of the invocation step state. + * This will also produce one step per job in mapping jobs to mimic the older behavior with respect to collections. + * Partially scheduled steps may provide incomplete information and the listed steps outputs + * are not the mapped over step outputs but the individual job outputs. + * @default false + */ + legacy_job_state?: boolean; + model_store_format?: components["schemas"]["ModelStoreFormat"] | null; + /** + * Include step details + * @description Include details for individual invocation steps and populate a steps attribute in the resulting dictionary + * @default false + */ + step_details?: boolean; + /** Store Content Uri */ + store_content_uri?: string | null; + /** Store Dict */ + store_dict?: Record | null; + /** + * View + * @description The name of the view used to serialize this item. This will return a predefined set of attributes of the item. + */ + view?: components["schemas"]["InvocationSerializationView"] | null; + }; /** CreateLibrariesFromStore */ CreateLibrariesFromStore: { model_store_format?: components["schemas"]["ModelStoreFormat"] | null; @@ -4559,11 +4616,6 @@ export interface components { }; /** ExportTaskListResponse */ ExportTaskListResponse: components["schemas"]["ObjectExportTaskResponse"][]; - /** - * ExtendedInvocationStepState - * @enum {string} - */ - ExtendedInvocationStepState: "new" | "ready" | "scheduled" | "ok"; /** ExtraFileEntry */ ExtraFileEntry: { /** @description The class of this entry, either File or Directory. */ @@ -6962,6 +7014,11 @@ export interface components { * @description Report describing workflow invocation */ InvocationReport: { + /** + * Errors + * @description Errors associated with the invocation. + */ + errors?: Record | null; /** * Galaxy Version * @description The version of Galaxy this object was generated with. @@ -6972,6 +7029,21 @@ export interface components { * @description The version of Galaxy this object was generated with. */ generate_version?: string | null; + /** + * Histories + * @description Histories associated with the invocation. + */ + histories?: Record | null; + /** + * History dataset collections + * @description History dataset collections associated with the invocation. + */ + history_dataset_collections?: Record | null; + /** + * History datasets + * @description History datasets associated with the invocation. + */ + history_datasets?: Record | null; /** * Workflow ID * @description The workflow this invocation has been triggered for. @@ -6981,13 +7053,21 @@ export interface components { /** * Markdown * @description Raw galaxy-flavored markdown contents of the report. - * @default */ invocation_markdown?: string | null; + /** + * Invocations + * @description Other invocations associated with the invocation. + */ + invocations?: Record | null; + /** + * Jobs + * @description Jobs associated with the invocation. + */ + jobs?: Record | null; /** * Markdown * @description Raw galaxy-flavored markdown contents of the report. - * @default */ markdown?: string | null; /** @@ -7013,8 +7093,22 @@ export interface components { * @description The name of the user who owns this report. */ username: string; - [key: string]: unknown | undefined; + /** + * Workflows + * @description Workflows associated with the invocation. + */ + workflows?: Record | null; }; + /** + * InvocationSerializationView + * @enum {string} + */ + InvocationSerializationView: "element" | "collection"; + /** + * InvocationSortByEnum + * @enum {string} + */ + InvocationSortByEnum: "create_time" | "update_time" | "None"; /** * InvocationState * @enum {string} @@ -7074,7 +7168,7 @@ export interface components { * State of the invocation step * @description Describes where in the scheduling process the workflow invocation step is. */ - state?: components["schemas"]["ExtendedInvocationStepState"] | null; + state?: components["schemas"]["InvocationStepState"] | components["schemas"]["JobState"] | null; /** Subworkflow Invocation Id */ subworkflow_invocation_id: string | null; /** @@ -7214,6 +7308,11 @@ export interface components { */ uuid?: string | null; }; + /** + * InvocationStepState + * @enum {string} + */ + InvocationStepState: "new" | "ready" | "scheduled"; /** InvocationUnexpectedFailureResponse */ InvocationUnexpectedFailureResponse: { /** @@ -16465,6 +16564,86 @@ export interface operations { }; }; }; + index_invocations_api_invocations_get: { + /** Get the list of a user's workflow invocations. */ + parameters?: { + /** @description Return only invocations for this Workflow ID */ + /** @description Return only invocations for this History ID */ + /** @description Return only invocations for this Job ID */ + /** @description Return invocations for this User ID. */ + /** @description Sort Workflow Invocations by this attribute */ + /** @description Sort in descending order? */ + /** @description Set to false to only include terminal Invocations. */ + /** @description Limit the number of invocations to return. */ + /** @description Number of invocations to skip. */ + /** @description Is provided workflow id for Workflow instead of StoredWorkflow? */ + /** @description View to be passed to the serializer */ + /** @description Include details for individual invocation steps and populate a steps attribute in the resulting dictionary. */ + query?: { + workflow_id?: string | null; + history_id?: string | null; + job_id?: string | null; + user_id?: string | null; + sort_by?: components["schemas"]["InvocationSortByEnum"] | null; + sort_desc?: boolean; + include_terminal?: boolean | null; + limit?: number | null; + offset?: number | null; + instance?: boolean | null; + view?: string | null; + step_details?: boolean; + }; + /** @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. */ + header?: { + "run-as"?: string | null; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowInvocationResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_invocations_from_store_api_invocations_from_store_post: { + /** + * Create Invocations From Store + * @description Create invocation(s) from a supplied model store. + */ + parameters?: { + /** @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. */ + header?: { + "run-as"?: string | null; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateInvocationsFromStorePayload"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowInvocationResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; step_api_invocations_steps__step_id__get: { /** Show details of workflow invocation step. */ parameters: { @@ -21294,6 +21473,57 @@ export interface operations { }; }; }; + index_invocations_api_workflows__workflow_id__invocations_get: { + /** Get the list of a user's workflow invocations. */ + parameters: { + /** @description Return only invocations for this History ID */ + /** @description Return only invocations for this Job ID */ + /** @description Return invocations for this User ID. */ + /** @description Sort Workflow Invocations by this attribute */ + /** @description Sort in descending order? */ + /** @description Set to false to only include terminal Invocations. */ + /** @description Limit the number of invocations to return. */ + /** @description Number of invocations to skip. */ + /** @description Is provided workflow id for Workflow instead of StoredWorkflow? */ + /** @description View to be passed to the serializer */ + /** @description Include details for individual invocation steps and populate a steps attribute in the resulting dictionary. */ + query?: { + history_id?: string | null; + job_id?: string | null; + user_id?: string | null; + sort_by?: components["schemas"]["InvocationSortByEnum"] | null; + sort_desc?: boolean; + include_terminal?: boolean | null; + limit?: number | null; + offset?: number | null; + instance?: boolean | null; + view?: string | null; + step_details?: boolean; + }; + /** @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. */ + header?: { + "run-as"?: string | null; + }; + /** @description The encoded database identifier of the Stored Workflow. */ + path: { + workflow_id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowInvocationResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; show_workflow_invocation_api_workflows__workflow_id__invocations__invocation_id__get: { /** * Get detailed description of a workflow invocation. @@ -21908,6 +22138,60 @@ export interface operations { }; }; }; + index_invocations_api_workflows__workflow_id__usage_get: { + /** + * Get the list of a user's workflow invocations. + * @deprecated + */ + parameters: { + /** @description Return only invocations for this History ID */ + /** @description Return only invocations for this Job ID */ + /** @description Return invocations for this User ID. */ + /** @description Sort Workflow Invocations by this attribute */ + /** @description Sort in descending order? */ + /** @description Set to false to only include terminal Invocations. */ + /** @description Limit the number of invocations to return. */ + /** @description Number of invocations to skip. */ + /** @description Is provided workflow id for Workflow instead of StoredWorkflow? */ + /** @description View to be passed to the serializer */ + /** @description Include details for individual invocation steps and populate a steps attribute in the resulting dictionary. */ + query?: { + history_id?: string | null; + job_id?: string | null; + user_id?: string | null; + sort_by?: components["schemas"]["InvocationSortByEnum"] | null; + sort_desc?: boolean; + include_terminal?: boolean | null; + limit?: number | null; + offset?: number | null; + instance?: boolean | null; + view?: string | null; + step_details?: boolean; + }; + /** @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. */ + header?: { + "run-as"?: string | null; + }; + /** @description The encoded database identifier of the Stored Workflow. */ + path: { + workflow_id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowInvocationResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; show_workflow_invocation_api_workflows__workflow_id__usage__invocation_id__get: { /** * Get detailed description of a workflow invocation. diff --git a/lib/galaxy/schema/invocation.py b/lib/galaxy/schema/invocation.py index 097f56ee82e1..df81f450de32 100644 --- a/lib/galaxy/schema/invocation.py +++ b/lib/galaxy/schema/invocation.py @@ -28,6 +28,7 @@ from galaxy.schema import schema from galaxy.schema.fields import ( + DecodedDatabaseIdField, EncodedDatabaseIdField, literal_to_value, ModelClassField, @@ -41,6 +42,7 @@ JOB_MODEL_CLASS, JobState, Model, + StoreContentSource, UpdateTimeField, WithModelClass, ) @@ -304,15 +306,6 @@ class InvocationStepState(str, Enum): # FAILED = 'failed', TODO: implement and expose -class ExtendedInvocationStepState(str, Enum): - NEW = "new" # Brand new workflow invocation step - READY = "ready" # Workflow invocation step ready for another iteration of scheduling. - SCHEDULED = "scheduled" # Workflow invocation step has been scheduled. - # CANCELLED = 'cancelled', TODO: implement and expose - # FAILED = 'failed', TODO: implement and expose - OK = "ok" # Workflow invocation step has completed successfully - TODO: is this a correct description? - - class InvocationStepOutput(Model): src: INVOCATION_STEP_OUTPUT_SRC = Field( literal_to_value(INVOCATION_STEP_OUTPUT_SRC), @@ -378,9 +371,7 @@ class InvocationStep(Model, WithModelClass): ), ] ] - # TODO The state can differ from InvocationStepState is this intended? - # InvocationStepState is equal to the states attribute of the WorkflowInvocationStep class - state: Optional[ExtendedInvocationStepState] = Field( + state: Optional[Union[InvocationStepState, JobState]] = Field( default=None, title="State of the invocation step", description="Describes where in the scheduling process the workflow invocation step is.", @@ -427,12 +418,12 @@ class InvocationReport(Model, WithModelClass): description="Format of the invocation report.", ) markdown: Optional[str] = Field( - default="", + default=None, title="Markdown", description="Raw galaxy-flavored markdown contents of the report.", ) invocation_markdown: Optional[str] = Field( - default="", + default=None, title="Markdown", description="Raw galaxy-flavored markdown contents of the report.", ) @@ -454,7 +445,43 @@ class InvocationReport(Model, WithModelClass): ) generate_time: Optional[str] = schema.GenerateTimeField generate_version: Optional[str] = schema.GenerateVersionField - model_config = ConfigDict(extra="allow") + + errors: Optional[Dict[str, Any]] = Field( + default=None, + title="Errors", + description="Errors associated with the invocation.", + ) + + history_datasets: Optional[Dict[str, Any]] = Field( + default=None, + title="History datasets", + description="History datasets associated with the invocation.", + ) + workflows: Optional[Dict[str, Any]] = Field( + default=None, + title="Workflows", + description="Workflows associated with the invocation.", + ) + history_dataset_collections: Optional[Dict[str, Any]] = Field( + default=None, + title="History dataset collections", + description="History dataset collections associated with the invocation.", + ) + jobs: Optional[Dict[str, Any]] = Field( + default=None, + title="Jobs", + description="Jobs associated with the invocation.", + ) + histories: Optional[Dict[str, Any]] = Field( + default=None, + title="Histories", + description="Histories associated with the invocation.", + ) + invocations: Optional[Dict[str, Any]] = Field( + default=None, + title="Invocations", + description="Other invocations associated with the invocation.", + ) class InvocationUpdatePayload(Model): @@ -462,11 +489,6 @@ class InvocationUpdatePayload(Model): class InvocationIOBase(Model): - # TODO - resolve - # the tests in test/integration/test_workflow_tasks.py , - # between line 42 and 56 fail, if this id is not allowed be None - # They all fail, when trying to populate the response of the show_invocation operation - # Is it intended to allow None here? id: Optional[EncodedDatabaseIdField] = Field( default=None, title="ID", description="The encoded ID of the dataset/dataset collection." ) @@ -591,3 +613,50 @@ class InvocationStepJobsResponseJobModel(InvocationJobsSummaryBaseModel): class InvocationStepJobsResponseCollectionJobsModel(InvocationJobsSummaryBaseModel): model: IMPLICIT_COLLECTION_JOBS_MODEL_CLASS + + +class CreateInvocationFromStore(StoreContentSource): + history_id: int = Field( + default=..., title="History ID", description="The ID of the history associated with the invocations." + ) + + +class InvocationSerializationView(str, Enum): + element = "element" + collection = "collection" + + +class InvocationSerializationParams(BaseModel): + """Contains common parameters for customizing model serialization.""" + + view: Optional[InvocationSerializationView] = Field( + default=None, + title="View", + description=( + "The name of the view used to serialize this item. " + "This will return a predefined set of attributes of the item." + ), + examples=["element"], + ) + step_details: bool = Field( + default=False, + title="Include step details", + description="Include details for individual invocation steps and populate a steps attribute in the resulting dictionary", + ) + legacy_job_state: bool = Field( + default=False, + description="""Populate the invocation step state with the job state instead of the invocation step state. + This will also produce one step per job in mapping jobs to mimic the older behavior with respect to collections. + Partially scheduled steps may provide incomplete information and the listed steps outputs + are not the mapped over step outputs but the individual job outputs.""", + # TODO: also deprecate on python side, https://github.com/pydantic/pydantic/issues/2255 + json_schema_extra={"deprecated": True}, + ) + + +class CreateInvocationsFromStorePayload(CreateInvocationFromStore, InvocationSerializationParams): + history_id: DecodedDatabaseIdField = Field( + default=..., + title="History ID", + description="The ID of the history associated with the invocations.", + ) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index 7e59383d133a..876576ba6681 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -1429,18 +1429,14 @@ class InvocationSortByEnum(str, Enum): class InvocationIndexQueryPayload(Model): - workflow_id: Optional[DecodedDatabaseIdField] = Field( + workflow_id: Optional[int] = Field( None, title="Workflow ID", description="Return only invocations for this Workflow ID" ) - history_id: Optional[DecodedDatabaseIdField] = Field( + history_id: Optional[int] = Field( None, title="History ID", description="Return only invocations for this History ID" ) - job_id: Optional[DecodedDatabaseIdField] = Field( - None, title="Job ID", description="Return only invocations for this Job ID" - ) - user_id: Optional[DecodedDatabaseIdField] = Field( - None, title="User ID", description="Return invocations for this User ID" - ) + job_id: Optional[int] = Field(None, title="Job ID", description="Return only invocations for this Job ID") + user_id: Optional[int] = Field(None, title="User ID", description="Return invocations for this User ID") sort_by: Optional[InvocationSortByEnum] = Field( None, title="Sort By", description="Sort Workflow Invocations by this attribute" ) diff --git a/lib/galaxy/webapps/galaxy/api/common.py b/lib/galaxy/webapps/galaxy/api/common.py index 1e2cb1641956..f4f38ed0a20d 100644 --- a/lib/galaxy/webapps/galaxy/api/common.py +++ b/lib/galaxy/webapps/galaxy/api/common.py @@ -87,11 +87,13 @@ Path(..., title="Quota ID", description="The ID of the Quota."), ] -SerializationViewQueryParam: Optional[str] = Query( - None, - title="View", - description="View to be passed to the serializer", -) +SerializationViewQueryParam = Annotated[ + Optional[str], + Query( + title="View", + description="View to be passed to the serializer", + ), +] SerializationKeysQueryParam: Optional[str] = Query( None, @@ -151,7 +153,7 @@ def parse_serialization_params( def query_serialization_params( - view: Optional[str] = SerializationViewQueryParam, + view: SerializationViewQueryParam = None, keys: Optional[str] = SerializationKeysQueryParam, ) -> SerializationParams: return parse_serialization_params(view=view, keys=keys) diff --git a/lib/galaxy/webapps/galaxy/api/configuration.py b/lib/galaxy/webapps/galaxy/api/configuration.py index daa50afaf822..7450103197dc 100644 --- a/lib/galaxy/webapps/galaxy/api/configuration.py +++ b/lib/galaxy/webapps/galaxy/api/configuration.py @@ -60,7 +60,7 @@ def whoami(self, trans: ProvidesUserContext = DependsOnTrans) -> Optional[UserMo def index( self, trans: ProvidesUserContext = DependsOnTrans, - view: Optional[str] = SerializationViewQueryParam, + view: SerializationViewQueryParam = None, keys: Optional[str] = SerializationKeysQueryParam, ) -> Dict[str, Any]: """ diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index a67481078558..10e130a8781f 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -23,7 +23,6 @@ ) from gxformat2._yaml import ordered_dump from markupsafe import escape -from pydantic import ConfigDict from starlette.responses import StreamingResponse from typing_extensions import Annotated @@ -36,7 +35,10 @@ stream_url_to_str, validate_uri_access, ) -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.workflows import ( MissingToolsException, RefactorRequest, @@ -48,9 +50,12 @@ from galaxy.model.store import BcoExportOptions from galaxy.schema.fields import DecodedDatabaseIdField from galaxy.schema.invocation import ( + CreateInvocationFromStore, + CreateInvocationsFromStorePayload, InvocationJobsResponse, InvocationMessageResponseModel, InvocationReport, + InvocationSerializationParams, InvocationStep, InvocationStepJobsResponseCollectionJobsModel, InvocationStepJobsResponseJobModel, @@ -61,11 +66,11 @@ from galaxy.schema.schema import ( AsyncFile, AsyncTaskResultSummary, + InvocationSortByEnum, SetSlugPayload, ShareWithPayload, ShareWithStatus, SharingStatus, - StoreContentSource, WorkflowSortByEnum, ) from galaxy.structured_app import StructuredApp @@ -77,7 +82,6 @@ from galaxy.version import VERSION from galaxy.web import ( expose_api, - expose_api_anonymous, expose_api_anonymous_and_sessionless, expose_api_raw_anonymous_and_sessionless, format_return_as_json, @@ -96,13 +100,13 @@ Router, search_query_param, ) +from galaxy.webapps.galaxy.api.common import SerializationViewQueryParam from galaxy.webapps.galaxy.services.base import ( ConsumesModelStores, ServesExportStores, ) from galaxy.webapps.galaxy.services.invocations import ( InvocationIndexPayload, - InvocationSerializationParams, InvocationsService, PrepareStoreDownloadPayload, WriteInvocationStoreToPayload, @@ -121,11 +125,6 @@ router = Router(tags=["workflows"]) -class CreateInvocationFromStore(StoreContentSource): - history_id: Optional[str] - model_config = ConfigDict(extra="allow") - - class WorkflowsAPIController( BaseGalaxyAPIController, UsesStoredWorkflowMixin, @@ -791,80 +790,6 @@ def invoke(self, trans: GalaxyWebTransaction, workflow_id, payload, **kwd): else: return encoded_invocations[0] - @expose_api - def index_invocations(self, trans: GalaxyWebTransaction, **kwd): - """ - GET /api/workflows/{workflow_id}/invocations - GET /api/invocations - - Get the list of a user's workflow invocations. If workflow_id is supplied - (either via URL or query parameter) it should be an encoded StoredWorkflow id - and returned invocations will be restricted to that workflow. history_id (an encoded - History id) can be used to further restrict the query. If neither a workflow_id or - history_id is supplied, all the current user's workflow invocations will be indexed - (as determined by the invocation being executed on one of the user's histories). - - :param workflow_id: an encoded stored workflow id to restrict query to - :type workflow_id: str - - :param instance: true if fetch by Workflow ID instead of StoredWorkflow id, false - by default. - :type instance: boolean - - :param history_id: an encoded history id to restrict query to - :type history_id: str - - :param job_id: an encoded job id to restrict query to - :type job_id: str - - :param user_id: an encoded user id to restrict query to, must be own id if not admin user - :type user_id: str - - :param view: level of detail to return per invocation 'element' or 'collection'. - :type view: str - - :param step_details: If 'view' is 'element', also include details on individual steps. - :type step_details: bool - - :raises: exceptions.MessageException, exceptions.ObjectNotFound - """ - invocation_payload = InvocationIndexPayload(**kwd) - serialization_params = InvocationSerializationParams(**kwd) - invocations, total_matches = self.invocations_service.index(trans, invocation_payload, serialization_params) - trans.response.headers["total_matches"] = total_matches - return [i.model_dump(mode="json") for i in invocations] - - @expose_api_anonymous - def create_invocations_from_store(self, trans, payload, **kwd): - """ - POST /api/invocations/from_store - - Create invocation(s) from a supplied model store. - - Input can be an archive describing a Galaxy model store containing an - workflow invocation - for instance one created with with write_store - or prepare_store_download endpoint. - """ - create_payload = CreateInvocationFromStore(**payload) - serialization_params = InvocationSerializationParams(**payload) - # refactor into a service... - return [i.model_dump(mode="json") for i in self._create_from_store(trans, create_payload, serialization_params)] - - def _create_from_store( - self, trans, payload: CreateInvocationFromStore, serialization_params: InvocationSerializationParams - ): - history = self.history_manager.get_owned( - self.decode_id(payload.history_id), trans.user, current_history=trans.history - ) - object_tracker = self.create_objects_from_store( - trans, - payload, - history=history, - ) - return self.invocations_service.serialize_workflow_invocations( - object_tracker.invocations_by_key.values(), serialization_params - ) - def _workflow_from_dict(self, trans, data, workflow_create_options, source=None): """Creates a workflow from a dict. @@ -997,9 +922,12 @@ def __get_stored_workflow(self, trans, workflow_id, **kwd): title="Number of workflows to skip in sorted query (to enable pagination).", ) -InstanceQueryParam: Optional[bool] = Query( - default=False, title="True when fetching by Workflow ID, False when fetching by StoredWorkflow ID." -) +InstanceQueryParam = Annotated[ + Optional[bool], + Query( + title="True when fetching by Workflow ID, False when fetching by StoredWorkflow ID.", + ), +] query_tags = [ IndexQueryTag("name", "The stored workflow's name.", "n"), @@ -1195,7 +1123,7 @@ def show_versions( self, workflow_id: StoredWorkflowIDPathParam, trans: ProvidesUserContext = DependsOnTrans, - instance: Optional[bool] = InstanceQueryParam, + instance: InstanceQueryParam = False, ): return self.service.get_versions(trans, workflow_id, instance) @@ -1247,21 +1175,93 @@ def get_workflow_menu( ), ] - -HistoryIdQueryParam: Annotated[ +WorkflowIdQueryParam = Annotated[ Optional[DecodedDatabaseIdField], Query( - default=None, - description="Optional identifier of a History. Use it to restrict the search within a particular History.", + title="Workflow ID", + description="Return only invocations for this Workflow ID", ), ] +HistoryIdQueryParam = Annotated[ + Optional[DecodedDatabaseIdField], + Query( + title="History ID", + description="Return only invocations for this History ID", + ), +] JobIdQueryParam = Annotated[ - DecodedDatabaseIdField, + Optional[DecodedDatabaseIdField], Query( title="Job ID", - description="The ID of the job", + description="Return only invocations for this Job ID", + ), +] + +UserIdQueryParam = Annotated[ + Optional[DecodedDatabaseIdField], + Query( + title="User ID", + description="Return invocations for this User ID.", + ), +] + +InvocationsSortByQueryParam = Annotated[ + Optional[InvocationSortByEnum], + Query( + title="Sort By", + description="Sort Workflow Invocations by this attribute", + ), +] + +InvocationsSortDescQueryParam = Annotated[ + bool, + Query( + title="Sort Descending", + description="Sort in descending order?", + ), +] + +InvocationsIncludeTerminalQueryParam = Annotated[ + Optional[bool], + Query( + title="Include Terminal", + description="Set to false to only include terminal Invocations.", + ), +] + +InvocationsLimitQueryParam = Annotated[ + Optional[int], + Query( + title="Limit", + description="Limit the number of invocations to return.", + ), +] + +InvocationsOffsetQueryParam = Annotated[ + Optional[int], + Query( + title="Offset", + description="Number of invocations to skip.", + ), +] + + +InvocationsInstanceQueryParam = Annotated[ + Optional[bool], + Query( + title="Instance", + description="Is provided workflow id for Workflow instead of StoredWorkflow?", + ), +] + +CreateInvocationsFromStoreBody = Annotated[ + CreateInvocationsFromStorePayload, + Body( + default=..., + title="Create invocations from store", + description="The values and serialization parameters for creating invocations from a supplied model store.", ), ] @@ -1270,6 +1270,113 @@ def get_workflow_menu( class FastAPIInvocations: invocations_service: InvocationsService = depends(InvocationsService) + @router.post( + "/api/invocations/from_store", + name="create_invocations_from_store", + description="Create invocation(s) from a supplied model store.", + ) + def create_invocations_from_store( + self, + payload: CreateInvocationsFromStoreBody, + trans: ProvidesHistoryContext = DependsOnTrans, + ) -> List[WorkflowInvocationResponse]: + """ + Input can be an archive describing a Galaxy model store containing an + workflow invocation - for instance one created with with write_store + or prepare_store_download endpoint. + """ + create_payload = CreateInvocationFromStore(**payload.model_dump()) + serialization_params = InvocationSerializationParams(**payload.model_dump()) + return self.invocations_service.create_from_store(trans, create_payload, serialization_params) + + @router.get( + "/api/invocations", + summary="Get the list of a user's workflow invocations.", + name="index_invocations", + ) + def index_invocations( + self, + response: Response, + workflow_id: WorkflowIdQueryParam = None, + history_id: HistoryIdQueryParam = None, + job_id: JobIdQueryParam = None, + user_id: UserIdQueryParam = None, + sort_by: InvocationsSortByQueryParam = None, + sort_desc: InvocationsSortDescQueryParam = False, + include_terminal: InvocationsIncludeTerminalQueryParam = True, + limit: InvocationsLimitQueryParam = None, + offset: InvocationsOffsetQueryParam = None, + instance: InvocationsInstanceQueryParam = False, + view: SerializationViewQueryParam = None, + step_details: StepDetailQueryParam = False, + trans: ProvidesUserContext = DependsOnTrans, + ) -> List[WorkflowInvocationResponse]: + invocation_payload = InvocationIndexPayload( + workflow_id=workflow_id, + history_id=history_id, + job_id=job_id, + user_id=user_id, + sort_by=sort_by, + sort_desc=sort_desc, + include_terminal=include_terminal, + limit=limit, + offset=offset, + instance=instance, + ) + serialization_params = InvocationSerializationParams( + view=view, + step_details=step_details, + ) + invocations, total_matches = self.invocations_service.index(trans, invocation_payload, serialization_params) + response.headers["total_matches"] = str(total_matches) + return invocations + + @router.get( + "/api/workflows/{workflow_id}/invocations", + summary="Get the list of a user's workflow invocations.", + name="index_invocations", + ) + @router.get( + "/api/workflows/{workflow_id}/usage", + summary="Get the list of a user's workflow invocations.", + name="index_invocations", + deprecated=True, + ) + def index_workflow_invocations( + self, + response: Response, + workflow_id: StoredWorkflowIDPathParam, + history_id: HistoryIdQueryParam = None, + job_id: JobIdQueryParam = None, + user_id: UserIdQueryParam = None, + sort_by: InvocationsSortByQueryParam = None, + sort_desc: InvocationsSortDescQueryParam = False, + include_terminal: InvocationsIncludeTerminalQueryParam = True, + limit: InvocationsLimitQueryParam = None, + offset: InvocationsOffsetQueryParam = None, + instance: InvocationsInstanceQueryParam = False, + view: SerializationViewQueryParam = None, + step_details: StepDetailQueryParam = False, + trans: ProvidesUserContext = DependsOnTrans, + ) -> List[WorkflowInvocationResponse]: + invocations = self.index_invocations( + response=response, + workflow_id=workflow_id, + history_id=history_id, + job_id=job_id, + user_id=user_id, + sort_by=sort_by, + sort_desc=sort_desc, + include_terminal=include_terminal, + limit=limit, + offset=offset, + instance=instance, + view=view, + step_details=step_details, + trans=trans, + ) + return invocations + @router.post( "/api/invocations/{invocation_id}/prepare_store_download", summary="Prepare a workflow invocation export-style download.", @@ -1609,7 +1716,6 @@ def workflow_invocation_jobs_summary( """An alias for `GET /api/invocations/{invocation_id}/jobs_summary`. `workflow_id` is ignored.""" return self.invocation_jobs_summary(trans=trans, invocation_id=invocation_id) - # Should I even create models for those as they will be removed? # TODO: remove this endpoint after 23.1 release @router.get( "/api/invocations/{invocation_id}/biocompute", diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index a17a122d47da..0e2d74676b4b 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -673,22 +673,6 @@ def populate_api_routes(webapp, app): # conditions=dict(method=["POST"]), # ) - webapp.mapper.connect( - "list_invocations", - "/api/invocations", - controller="workflows", - action="index_invocations", - conditions=dict(method=["GET"]), - ) - - webapp.mapper.connect( - "create_invovactions_from_store", - "/api/invocations/from_store", - controller="workflows", - action="create_invocations_from_store", - conditions=dict(method=["POST"]), - ) - # API refers to usages and invocations - these mean the same thing but the # usage routes should be considered deprecated. invoke_names = { @@ -697,13 +681,6 @@ def populate_api_routes(webapp, app): } for noun, suffix in invoke_names.items(): name = f"{noun}{suffix}" - webapp.mapper.connect( - f"list_workflow_{name}", - "/api/workflows/{workflow_id}/%s" % noun, - controller="workflows", - action="index_invocations", - conditions=dict(method=["GET"]), - ) webapp.mapper.connect( f"workflow_{name}", "/api/workflows/{workflow_id}/%s" % noun, diff --git a/lib/galaxy/webapps/galaxy/services/invocations.py b/lib/galaxy/webapps/galaxy/services/invocations.py index 220d1567c01f..79912285c2ac 100644 --- a/lib/galaxy/webapps/galaxy/services/invocations.py +++ b/lib/galaxy/webapps/galaxy/services/invocations.py @@ -1,18 +1,13 @@ import logging -from enum import Enum from tempfile import NamedTemporaryFile from typing import ( Any, Dict, List, - Optional, Tuple, ) -from pydantic import ( - BaseModel, - Field, -) +from pydantic import Field from galaxy.celery.tasks import ( prepare_invocation_download, @@ -22,6 +17,7 @@ AdminRequiredException, ObjectNotFound, ) +from galaxy.managers.context import ProvidesHistoryContext from galaxy.managers.histories import HistoryManager from galaxy.managers.jobs import ( fetch_job_states, @@ -38,6 +34,9 @@ ) from galaxy.schema.fields import DecodedDatabaseIdField from galaxy.schema.invocation import ( + CreateInvocationFromStore, + InvocationSerializationParams, + InvocationSerializationView, InvocationStep, WorkflowInvocationResponse, ) @@ -57,6 +56,7 @@ from galaxy.short_term_storage import ShortTermStorageAllocator from galaxy.webapps.galaxy.services.base import ( async_task_summary, + ConsumesModelStores, ensure_celery_tasks_enabled, model_store_storage_target, ServiceBase, @@ -65,39 +65,6 @@ log = logging.getLogger(__name__) -class InvocationSerializationView(str, Enum): - element = "element" - collection = "collection" - - -class InvocationSerializationParams(BaseModel): - """Contains common parameters for customizing model serialization.""" - - view: Optional[InvocationSerializationView] = Field( - default=None, - title="View", - description=( - "The name of the view used to serialize this item. " - "This will return a predefined set of attributes of the item." - ), - examples=["element"], - ) - step_details: bool = Field( - default=False, - title="Include step details", - description="Include details for individual invocation steps and populate a steps attribute in the resulting dictionary", - ) - legacy_job_state: bool = Field( - default=False, - description="""Populate the invocation step state with the job state instead of the invocation step state. - This will also produce one step per job in mapping jobs to mimic the older behavior with respect to collections. - Partially scheduled steps may provide incomplete information and the listed steps outputs - are not the mapped over step outputs but the individual job outputs.""", - # TODO: also deprecate on python side, https://github.com/pydantic/pydantic/issues/2255 - json_schema_extra={"deprecated": True}, - ) - - class InvocationIndexPayload(InvocationIndexQueryPayload): instance: bool = Field(default=False, description="Is provided workflow id for Workflow instead of StoredWorkflow?") @@ -110,7 +77,7 @@ class WriteInvocationStoreToPayload(WriteStoreToPayload, BcoGenerationParameters pass -class InvocationsService(ServiceBase): +class InvocationsService(ServiceBase, ConsumesModelStores): def __init__( self, security: IdEncodingHelper, @@ -287,3 +254,17 @@ def deprecated_generate_invocation_bco( export_store.export_workflow_invocation(workflow_invocation) export_target.seek(0) return export_target.read() + + def create_from_store( + self, + trans: ProvidesHistoryContext, + payload: CreateInvocationFromStore, + serialization_params: InvocationSerializationParams, + ): + history = self._histories_manager.get_owned(payload.history_id, trans.user, current_history=trans.history) + object_tracker = self.create_objects_from_store( + trans, + payload, + history=history, + ) + return self.serialize_workflow_invocations(object_tracker.invocations_by_key.values(), serialization_params)