diff --git a/client-api/src/api-types.ts b/client-api/src/api-types.ts index 07544c435106..e22000febf0a 100644 --- a/client-api/src/api-types.ts +++ b/client-api/src/api-types.ts @@ -26,7 +26,7 @@ export type DatasetStorageDetails = components["schemas"]["DatasetStorageDetails export type DatasetCollectionAttributes = components["schemas"]["DatasetCollectionAttributesResult"]; export type ConcreteObjectStoreModel = components["schemas"]["ConcreteObjectStoreModel"]; export type MessageException = components["schemas"]["MessageExceptionModel"]; -export type DatasetHash = components["schemas"]["DatasetHash"]; +export type DatasetHash = components["schemas"]["DatasetHash-Output"]; export type DatasetSource = components["schemas"]["DatasetSource"]; export type DatasetTransform = components["schemas"]["DatasetSourceTransform"]; export type StoreExportPayload = components["schemas"]["StoreExportPayload"]; diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 4f2ea81c5682..3bf519095574 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -308,7 +308,7 @@ export function canMutateHistory(history: AnyHistory): boolean { return !history.purged && !history.archived; } -export type DatasetHash = components["schemas"]["DatasetHash"]; +export type DatasetHash = components["schemas"]["DatasetHash-Output"]; export type DatasetSource = components["schemas"]["DatasetSource"]; export type DatasetTransform = components["schemas"]["DatasetSourceTransform"]; diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 829cdef6a741..3ac30ca03e26 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -1090,6 +1090,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/file_landings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create File Landing */ + post: operations["create_file_landing_api_file_landings_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/file_source_instances": { parameters: { query?: never; @@ -7400,6 +7417,75 @@ export interface components { /** Item Ids */ item_ids: string[]; }; + /** CollectionElementCollectionRequestUri */ + CollectionElementCollectionRequestUri: { + /** + * Class + * @constant + */ + class: "Collection"; + /** Collection Type */ + collection_type: string; + /** Elements */ + elements: ( + | components["schemas"]["CollectionElementCollectionRequestUri"] + | components["schemas"]["CollectionElementDataRequestUri"] + )[]; + /** + * Identifier + * @description A unique identifier for this element within the collection. + */ + identifier: string; + }; + /** CollectionElementDataRequestUri */ + CollectionElementDataRequestUri: { + /** + * Class + * @constant + */ + class: "File"; + /** Created From Basename */ + created_from_basename?: string | null; + /** + * Dbkey + * @default ? + */ + dbkey: string; + /** + * Deferred + * @default false + */ + deferred: boolean; + /** Ext */ + ext: string; + /** Hashes */ + hashes?: components["schemas"]["DatasetHash-Input"][] | null; + /** + * Identifier + * @description A unique identifier for this element within the collection. + */ + identifier: string; + /** Info */ + info?: string | null; + /** Location */ + location: string; + /** Name */ + name?: string | null; + /** + * Space To Tab + * @default false + */ + space_to_tab: boolean; + /** Src */ + src?: null; + /** Tags */ + tags?: string[] | null; + /** + * To Posix Lines + * @default false + */ + to_posix_lines: boolean; + }; /** CollectionElementIdentifier */ CollectionElementIdentifier: { /** @@ -7991,6 +8077,23 @@ export interface components { */ target: string; }; + /** CreateFileLandingPayload */ + CreateFileLandingPayload: { + /** Client Secret */ + client_secret?: string | null; + /** Origin */ + origin?: string | null; + /** + * Public + * @default false + */ + public: boolean; + /** Request State */ + request_state: ( + | components["schemas"]["FileRequestUri"] + | components["schemas"]["DataRequestCollectionUri"] + )[]; + }; /** CreateHistoryContentFromStore */ CreateHistoryContentFromStore: { model_store_format?: components["schemas"]["ModelStoreFormat"] | null; @@ -9589,6 +9692,30 @@ export interface components { */ type: "data"; }; + /** DataRequestCollectionUri */ + DataRequestCollectionUri: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + class: "Collection"; + /** Collection Type */ + collection_type: string; + /** + * Deferred + * @default false + */ + deferred: boolean; + /** Elements */ + elements: ( + | components["schemas"]["CollectionElementCollectionRequestUri"] + | components["schemas"]["CollectionElementDataRequestUri"] + )[]; + /** Name */ + name?: string | null; + /** Src */ + src?: null; + }; /** DatasetAssociationRoles */ DatasetAssociationRoles: { /** @@ -9666,7 +9793,17 @@ export interface components { */ DatasetExtraFiles: components["schemas"]["ExtraFileEntry"][]; /** DatasetHash */ - DatasetHash: { + "DatasetHash-Input": { + /** + * Hash Function + * @enum {string} + */ + hash_function: "MD5" | "SHA-1" | "SHA-256" | "SHA-512"; + /** Hash Value */ + hash_value: string; + }; + /** DatasetHash */ + "DatasetHash-Output": { /** * Extra Files Path * @description The path to the extra files used to generate the hash. @@ -11378,6 +11515,50 @@ export interface components { /** visible */ visible: boolean; }; + /** FileRequestUri */ + FileRequestUri: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + class: "File"; + /** Created From Basename */ + created_from_basename?: string | null; + /** + * Dbkey + * @default ? + */ + dbkey: string; + /** + * Deferred + * @default false + */ + deferred: boolean; + /** Ext */ + ext: string; + /** Hashes */ + hashes?: components["schemas"]["DatasetHash-Input"][] | null; + /** Info */ + info?: string | null; + /** Location */ + location: string; + /** Name */ + name?: string | null; + /** + * Space To Tab + * @default false + */ + space_to_tab: boolean; + /** Src */ + src?: null; + /** Tags */ + tags?: string[] | null; + /** + * To Posix Lines + * @default false + */ + to_posix_lines: boolean; + }; /** FileSourceTemplateSummaries */ FileSourceTemplateSummaries: components["schemas"]["FileSourceTemplateSummary"][]; /** FileSourceTemplateSummary */ @@ -12218,7 +12399,7 @@ export interface components { * Hashes * @description The list of hashes associated with this dataset. */ - hashes?: components["schemas"]["DatasetHash"][] | null; + hashes?: components["schemas"]["DatasetHash-Output"][] | null; /** * HDA or LDDA * @description Whether this dataset belongs to a history (HDA) or a library (LDDA). @@ -12479,7 +12660,7 @@ export interface components { * Hashes * @description The list of hashes associated with this dataset. */ - hashes: components["schemas"]["DatasetHash"][]; + hashes: components["schemas"]["DatasetHash-Output"][]; /** * HDA or LDDA * @description Whether this dataset belongs to a history (HDA) or a library (LDDA). @@ -26641,6 +26822,51 @@ export interface operations { }; }; }; + create_file_landing_api_file_landings_post: { + parameters: { + query?: never; + header?: { + /** @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; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateFileLandingPayload"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ToolLandingRequest"]; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; file_sources__instances_index: { parameters: { query?: never; diff --git a/lib/galaxy/schema/fetch_data.py b/lib/galaxy/schema/fetch_data.py index fbdc4d41cdb4..557963c9a643 100644 --- a/lib/galaxy/schema/fetch_data.py +++ b/lib/galaxy/schema/fetch_data.py @@ -27,6 +27,7 @@ ) from galaxy.schema.terms import HelpTerms from galaxy.schema.types import CoercedStringType +from galaxy.tool_util_models.parameters import FileOrCollectionRequest from galaxy.util.hash_util import HashFunctionNames HELP_TERMS = HelpTerms() @@ -307,6 +308,11 @@ class DataLandingRequestState(Model): targets: Targets +FileOrCollectionRequests = list[FileOrCollectionRequest] + +FileOrCollectionRequestsAdapter = TypeAdapter(FileOrCollectionRequests) + + # Vaguely matches the schema.schema.ToolLandingState but we don't allow data_fetch to be called directly # via the tool API so we have a more specific model here. class CreateDataLandingPayload(Model): @@ -316,3 +322,12 @@ class CreateDataLandingPayload(Model): origin: Optional[HttpUrl] = None model_config = ConfigDict(extra="forbid") + + +class CreateFileLandingPayload(Model): + request_state: FileOrCollectionRequests + client_secret: Optional[str] = None + public: bool = False + origin: Optional[HttpUrl] = None + + model_config = ConfigDict(extra="forbid") diff --git a/lib/galaxy/tool_util/client/landing.py b/lib/galaxy/tool_util/client/landing.py index 27a2cc7c7159..87ea3b5044f3 100644 --- a/lib/galaxy/tool_util/client/landing.py +++ b/lib/galaxy/tool_util/client/landing.py @@ -64,6 +64,8 @@ def generate_claim_url(request: Request) -> Response: template_type = "tool" elif "workflow_id" in template: template_type = "workflow" + elif isinstance(template["request_state"], list): + template_type = "file" else: template_type = "data" if client_secret: @@ -79,7 +81,7 @@ def generate_claim_url(request: Request) -> Response: try: raw_response.raise_for_status() except Exception: - raise Exception("Request failed: %s", raw_response.text) + raise Exception("Request failed: %s", raw_response.json()) response = raw_response.json() response_type = "workflow" if template_type == "workflow" else "tool" url = f"{galaxy_url}/{response_type}_landings/{response['uuid']}" diff --git a/lib/galaxy/tool_util/client/landing_library.catalog.yml b/lib/galaxy/tool_util/client/landing_library.catalog.yml index 1192e73f3df8..e99b55e18e7c 100644 --- a/lib/galaxy/tool_util/client/landing_library.catalog.yml +++ b/lib/galaxy/tool_util/client/landing_library.catalog.yml @@ -19,6 +19,235 @@ int_workflow: workflow_target_type: stored_workflow request_state: int_input: 8 +upload_file: + request_state: + - class: File + name: Reference information + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "txt" + - class: File + name: Reference sequence + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fasta" + - class: File + name: Read 1 + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq.gz" + - class: Collection + collection_type: list + name: "my collection" + elements: + - class: File + name: sample1 + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: sample2 + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: sample3 + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: Collection + collection_type: list:paired + name: "The List of Dataset Pairs" + elements: + - class: Collection + collection_type: paired + name: sample1 + elements: + - class: File + name: forward + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: reverse + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - name: sample2 + class: Collection + collection_type: paired + elements: + - class: File + name: forward + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: reverse + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: Collection + collection_type: list:list:paired + name: "Nested List of Dataset Pairs" + elements: + - class: Collection + collection_type: list:paired + name: treatment1 + elements: + - class: Collection + collection_type: paired + name: replicate1 + elements: + - class: File + name: forward + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: reverse + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: Collection + collection_type: paired + name: replicate2 + elements: + - class: File + name: forward + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: reverse + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: Collection + collection_type: list:paired + name: treatment2 + elements: + - class: Collection + collection_type: paired + name: replicate1 + elements: + - class: File + name: forward + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: reverse + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: Collection + collection_type: paired + name: replicate2 + elements: + - class: File + name: forward + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: reverse + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + - class: File + name: "sample1.fasta" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fasta" + tags: ['name:sample1'] + - class: File + name: "sample2.fasta" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fasta" + tags: ['name:sample2'] + - class: File + name: "sample1.fastq" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq" + tags: ['group:treatment:treatment1', 'group:replicate:replicate1'] + - class: File + name: "sample2.fastq" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq" + tags: ['group:treatment:treatment1', 'group:replicate:replicate2'] + - class: File + name: "sample2.fastq" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq" + tags: ['group:treatment:treatment1', 'group:replicate:replicate2'] + - class: File + name: "sample3.fastq" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq" + tags: ['group:treatment:treatment1', 'group:replicate:replicate2'] + - class: File + name: "sample3.fastq" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq" + tags: ['group:treatment:treatment2', 'group:replicate:replicate1'] + - class: File + name: "sample4.fastq" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq" + tags: ['group:treatment:treatment2', 'group:replicate:replicate2'] + - class: File + name: "Convert spaces and not newlines" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "txt" + space_to_tab: true + to_posix_lines: false + - class: File + name: "Convert newlines and not spaces" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fasta" + space_to_tab: false + to_posix_lines: true + - class: File + name: "Reference genome" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fastq.gz" + - class: File + name: "Reference information" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "txt" + dbkey: hg19 + - class: File + name: "Reference sequence" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "fasta" + dbkey: hg19 + - class: File + name: "Reference annotation" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "gff" + dbkey: hg19 + - class: Collection + collection_type: paired:paired + name: "esoteric collection type" + elements: + - class: Collection + collection_type: paired + name: "forward" + elements: + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + name: "forward" + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + name: "reverse" + - class: Collection + collection_type: paired + name: "reverse" + elements: + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + name: "forward" + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" + ext: "txt" + name: "reverse" + +upload_one_file: + request_state: + - class: Collection + collection_type: list + name: "my collection" + elements: + - class: File + name: "Reference information" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + ext: "txt" + upload: request_state: targets: diff --git a/lib/galaxy/tool_util_models/parameters.py b/lib/galaxy/tool_util_models/parameters.py index b8daa3f3cfbb..3ed3c6f13f52 100644 --- a/lib/galaxy/tool_util_models/parameters.py +++ b/lib/galaxy/tool_util_models/parameters.py @@ -19,6 +19,7 @@ from pydantic import ( AfterValidator, + AliasChoices, AnyUrl, BaseModel, ConfigDict, @@ -403,6 +404,7 @@ class BaseDataRequest(StrictModel): deferred: StrictBool = False created_from_basename: Optional[StrictStr] = None info: Optional[StrictStr] = None + tags: Optional[List[str]] = None hashes: Optional[List[DatasetHash]] = None space_to_tab: bool = False to_posix_lines: bool = False @@ -440,12 +442,20 @@ class FileRequestUri(BaseDataRequest): class CollectionElementDataRequestUri(FileRequestUri): class_: Literal["File"] = Field(..., alias="class") - identifier: StrictStr + identifier: StrictStr = Field( + ..., + description="A unique identifier for this element within the collection.", + validation_alias=AliasChoices("identifier", "name"), + ) class CollectionElementCollectionRequestUri(StrictModel): class_: Literal["Collection"] = Field(..., alias="class") - identifier: StrictStr + identifier: StrictStr = Field( + ..., + description="A unique identifier for this element within the collection.", + validation_alias=AliasChoices("identifier", "name"), + ) collection_type: StrictStr elements: List[ Annotated[ @@ -485,6 +495,7 @@ class DataRequestCollectionUri(StrictModel): DataRequest: Type = cast(Type, _DataRequest) DataOrCollectionRequest = Union[_DataRequest, FileRequestUri, DataRequestCollectionUri, DataRequestHdca] +FileOrCollectionRequest = Annotated[Union[FileRequestUri, DataRequestCollectionUri], Field(discriminator="class_")] DataRequestHda.model_rebuild() DataRequestLd.model_rebuild() diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 01889be2ffe3..7bb1cdbb5e65 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -41,6 +41,7 @@ from galaxy.model.dataset_collections.workbook_util import workbook_to_bytes from galaxy.schema.fetch_data import ( CreateDataLandingPayload, + CreateFileLandingPayload, FetchDataFormPayload, FetchDataPayload, ) @@ -250,6 +251,15 @@ def get_icon(self, request: Request, tool_id: str, trans: ProvidesHistoryContext response.headers["Last-Modified"] = last_modified return response + @router.post("/api/file_landings", public=True, allow_cors=True) + def create_file_landing( + self, + trans: ProvidesUserContext = DependsOnTrans, + file_landing_request: CreateFileLandingPayload = Body(...), + ) -> ToolLandingRequest: + tool_landing_request = self.service.file_landing_to_tool_landing(trans, file_landing_request) + return self.landing_manager.create_tool_landing_request(tool_landing_request) + @router.post("/api/data_landings", public=True, allow_cors=True) def create_data_landing( self, diff --git a/lib/galaxy/webapps/galaxy/services/_fetch_util.py b/lib/galaxy/webapps/galaxy/services/_fetch_util.py index ecc8ad50be9b..badb02f438a0 100644 --- a/lib/galaxy/webapps/galaxy/services/_fetch_util.py +++ b/lib/galaxy/webapps/galaxy/services/_fetch_util.py @@ -34,6 +34,9 @@ def validate_and_normalize_targets(trans, payload, set_internal_fields=True): as needed for each upload. """ targets = payload.get("targets", []) + landing_uuid = payload.get("landing_uuid") + if landing_uuid: + payload["landing_uuid"] = str(landing_uuid) for target in targets: destination = get_required_item(target, "destination", "Each target must specify a 'destination'") diff --git a/lib/galaxy/webapps/galaxy/services/tools.py b/lib/galaxy/webapps/galaxy/services/tools.py index a954832d9846..377cd8f23a61 100644 --- a/lib/galaxy/webapps/galaxy/services/tools.py +++ b/lib/galaxy/webapps/galaxy/services/tools.py @@ -37,13 +37,27 @@ from galaxy.schema.credentials import CredentialsContext from galaxy.schema.fetch_data import ( CreateDataLandingPayload, + CreateFileLandingPayload, + DataElementsTarget, FetchDataFormPayload, FetchDataPayload, FilesPayload, + HdaDestination, + HdcaDataItemsTarget, + HdcaDestination, + NestedElement, TargetsAdapter, + UrlDataElement, ) from galaxy.schema.schema import CreateToolLandingRequestPayload from galaxy.security.idencoding import IdEncodingHelper +from galaxy.tool_util_models.parameters import ( + CollectionElementCollectionRequestUri, + CollectionElementDataRequestUri, + DataRequestCollectionUri, + DataRequestUri, + FileRequestUri, +) from galaxy.tools import Tool from galaxy.tools.search import ToolBoxSearch from galaxy.util.path import safe_contains @@ -88,6 +102,82 @@ def validate_tool_for_running(trans: ProvidesHistoryContext, tool_ref: ToolRunRe return tool +def file_landing_payload_to_fetch_targets(data_landing_payload: CreateFileLandingPayload): + """Convert a CreateDataLandingPayload with DataOrCollectionRequest format to FetchDataPayload with Targets format. + + This function transforms data/collection requests (used in workflow landing and data request payloads) into the fetch API's target format. + """ + targets: list[Union[DataElementsTarget, HdcaDataItemsTarget]] = [] + + for request_item in data_landing_payload.request_state: + if isinstance(request_item, (DataRequestUri, FileRequestUri)): + # Convert single file/URL request to a DataElementsTarget + element = UrlDataElement( + src="url", + url=str(request_item.url), + ext=request_item.ext, + dbkey=request_item.dbkey, + name=request_item.name, + deferred=request_item.deferred, + info=request_item.info, + tags=request_item.tags, + space_to_tab=request_item.space_to_tab, + to_posix_lines=request_item.to_posix_lines, + created_from_basename=request_item.created_from_basename, + ) + + targets.append( + DataElementsTarget( + destination=HdaDestination(type="hdas"), + elements=[element], + ) + ) + + elif isinstance(request_item, DataRequestCollectionUri): + # Convert collection request to HdcaDataItemsTarget + def convert_collection_element(elem): + """Convert a collection element (file or nested collection) recursively.""" + if isinstance(elem, CollectionElementDataRequestUri): + # This is a file element + return UrlDataElement( + src="url", + url=str(elem.url), + ext=elem.ext, + dbkey=elem.dbkey, + name=elem.identifier, + deferred=elem.deferred, + info=elem.info, + tags=elem.tags, + space_to_tab=elem.space_to_tab, + to_posix_lines=elem.to_posix_lines, + created_from_basename=elem.created_from_basename, + ) + elif isinstance(elem, CollectionElementCollectionRequestUri): + # This is a nested collection element + # Recursively convert its elements + nested_elements = [convert_collection_element(nested_elem) for nested_elem in elem.elements] + return NestedElement( + name=elem.identifier, + elements=nested_elements, + collection_type=elem.collection_type, + ) + else: + raise ValueError(f"Unknown collection element type: {type(elem)}") + + elements = [convert_collection_element(elem) for elem in request_item.elements] + + targets.append( + HdcaDataItemsTarget( + destination=HdcaDestination(type="hdca"), + elements=elements, + collection_type=request_item.collection_type, + name=request_item.name, + ) + ) + + return [target.model_dump(mode="json", exclude_unset=True) for target in TargetsAdapter.validate_python(targets)] + + class ToolsService(ServiceBase): def __init__( self, @@ -101,6 +191,29 @@ def __init__( self.toolbox_search = toolbox_search self.history_manager = history_manager + def file_landing_to_tool_landing( + self, + trans: ProvidesUserContext, + file_landing_payload: CreateFileLandingPayload, + ) -> CreateToolLandingRequestPayload: + request_version = "1" + payload = {"targets": file_landing_payload_to_fetch_targets(file_landing_payload)} + validate_and_normalize_targets(trans, payload, set_internal_fields=False) + request_state = { + "request_version": request_version, + "request_json": { + "targets": payload["targets"], + }, + "file_count": "0", + } + return CreateToolLandingRequestPayload( + tool_id="__DATA_FETCH__", + tool_version=None, + request_state=request_state, + client_secret=file_landing_payload.client_secret, + public=file_landing_payload.public, + ) + def data_landing_to_tool_landing( self, trans: ProvidesUserContext, diff --git a/lib/galaxy_test/api/test_landing.py b/lib/galaxy_test/api/test_landing.py index 6efbb0a85fc9..95d783781eb1 100644 --- a/lib/galaxy_test/api/test_landing.py +++ b/lib/galaxy_test/api/test_landing.py @@ -7,7 +7,9 @@ from galaxy.schema.fetch_data import ( CreateDataLandingPayload, + CreateFileLandingPayload, DataLandingRequestState, + FileOrCollectionRequestsAdapter, ) from galaxy.schema.schema import ( CreateToolLandingRequestPayload, @@ -59,7 +61,7 @@ def test_tool_landing_invalid(self): tool_version=None, request_state={"parameter": "foobar"}, ) - response = self.dataset_populator.create_tool_landing_raw(request) + response = self.dataset_populator.create_landing_raw(request, "tool") assert_status_code_is(response, 400) assert_error_code_is(response, 400008) assert "Input should be a valid integer" in response.text @@ -97,6 +99,34 @@ def test_data_landing(self): assert target["elements"] assert len(target["elements"]) == 1 + def test_file_landing(self): + file_landing_request_state = FileOrCollectionRequestsAdapter.validate_python( + [ + { + "class": "File", + "location": "base64://eyJ0ZXN0IjogInRlc3QifQ==", # base64 encoded {"test": "test"} + "filetype": "txt", + "deferred": False, + }, + ], + ) + payload = CreateFileLandingPayload(request_state=file_landing_request_state, public=True) + response = self.dataset_populator.create_file_landing(payload) + assert response.tool_id == "__DATA_FETCH__" + + tool_landing = self.dataset_populator.use_tool_landing(response.uuid) + request_state = tool_landing.request_state + assert request_state + request_json = request_state["request_json"] + assert request_json + targets = request_json["targets"] + assert targets + assert len(targets) == 1 + target = targets[0] + assert "elements" in target + assert target["elements"] + assert len(target["elements"]) == 1 + @skip_without_tool("cat1") def test_create_public_workflow_landing_authenticated_user(self): request = _get_simple_landing_payload(self.workflow_populator, public=True) diff --git a/lib/galaxy_test/base/populators.py b/lib/galaxy_test/base/populators.py index 892a77b64f19..95b875e8bc53 100644 --- a/lib/galaxy_test/base/populators.py +++ b/lib/galaxy_test/base/populators.py @@ -75,7 +75,10 @@ ImporterGalaxyInterface, ) from gxformat2.yaml import ordered_load -from pydantic import UUID4 +from pydantic import ( + BaseModel, + UUID4, +) from requests import Response from rocrate.rocrate import ROCrate from typing_extensions import ( @@ -84,7 +87,10 @@ TypedDict, ) -from galaxy.schema.fetch_data import CreateDataLandingPayload +from galaxy.schema.fetch_data import ( + CreateDataLandingPayload, + CreateFileLandingPayload, +) from galaxy.schema.schema import ( CreateToolLandingRequestPayload, CreateWorkflowLandingRequestPayload, @@ -890,25 +896,25 @@ def rename_collection(self, content_id: str, new_name: Optional[str] = None): self.update_dataset_collection(content_id, {"name": new_name}) def create_tool_landing(self, payload: CreateToolLandingRequestPayload) -> ToolLandingRequest: - create_response = self.create_tool_landing_raw(payload) + create_response = self.create_landing_raw(payload, "tool") api_asserts.assert_status_code_is(create_response, 200) create_response.raise_for_status() return ToolLandingRequest.model_validate(create_response.json()) - def create_tool_landing_raw(self, payload: CreateToolLandingRequestPayload) -> Response: - create_url = "tool_landings" - json = payload.model_dump(mode="json") - create_response = self._post(create_url, json, json=True, anon=True) - return create_response + def create_file_landing(self, payload: CreateFileLandingPayload) -> ToolLandingRequest: + create_response = self.create_landing_raw(payload, "file") + api_asserts.assert_status_code_is(create_response, 200) + create_response.raise_for_status() + return ToolLandingRequest.model_validate(create_response.json()) def create_data_landing(self, payload: CreateDataLandingPayload) -> ToolLandingRequest: - create_response = self.create_data_landing_raw(payload) + create_response = self.create_landing_raw(payload, "data") api_asserts.assert_status_code_is(create_response, 200) create_response.raise_for_status() return ToolLandingRequest.model_validate(create_response.json()) - def create_data_landing_raw(self, payload: CreateDataLandingPayload) -> Response: - create_url = "data_landings" + def create_landing_raw(self, payload: BaseModel, landing_type: Literal["file", "data", "tool"]) -> Response: + create_url = f"{landing_type}_landings" json = payload.model_dump(mode="json") create_response = self._post(create_url, json, json=True, anon=True) return create_response