diff --git a/client-api/src/api-types.ts b/client-api/src/api-types.ts index 07544c43510..e22000febf0 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 4f2ea81c568..3bf51909557 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 829cdef6a74..3d0df19ec2e 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -7400,6 +7400,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: { /** @@ -7976,7 +8045,11 @@ export interface components { * @default false */ public: boolean; - request_state: components["schemas"]["DataLandingRequestState"]; + /** Request State */ + request_state: ( + | components["schemas"]["FileRequestUri"] + | components["schemas"]["DataRequestCollectionUri"] + )[]; }; /** CreateEntryPayload */ CreateEntryPayload: { @@ -9510,17 +9583,6 @@ export interface components { * @enum {string} */ DataItemSourceType: "hda" | "ldda" | "hdca" | "dce" | "dc"; - /** DataLandingRequestState */ - DataLandingRequestState: { - /** Targets */ - targets: ( - | components["schemas"]["DataElementsTarget"] - | components["schemas"]["HdcaDataItemsTarget"] - | components["schemas"]["DataElementsFromTarget"] - | components["schemas"]["HdcaDataItemsFromTarget"] - | components["schemas"]["FtpImportTarget"] - )[]; - }; /** DataParameterModel */ DataParameterModel: { /** @@ -9589,6 +9651,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 +9752,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 +11474,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 +12358,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 +12619,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). diff --git a/lib/galaxy/schema/fetch_data.py b/lib/galaxy/schema/fetch_data.py index fbdc4d41cdb..b1c4b8e07cc 100644 --- a/lib/galaxy/schema/fetch_data.py +++ b/lib/galaxy/schema/fetch_data.py @@ -27,6 +27,7 @@ from galaxy.schema.schema import ( ) 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() @@ -303,14 +304,15 @@ class FetchDataFormPayload(BaseDataPayload): targets: Union[Json[Targets], Targets] -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): - request_state: DataLandingRequestState + request_state: FileOrCollectionRequests client_secret: Optional[str] = None public: bool = False origin: Optional[HttpUrl] = None diff --git a/lib/galaxy/tool_util/client/landing.py b/lib/galaxy/tool_util/client/landing.py index 27a2cc7c715..a6b0b9a4af0 100644 --- a/lib/galaxy/tool_util/client/landing.py +++ b/lib/galaxy/tool_util/client/landing.py @@ -79,7 +79,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 1192e73f3df..c9acb180a4f 100644 --- a/lib/galaxy/tool_util/client/landing_library.catalog.yml +++ b/lib/galaxy/tool_util/client/landing_library.catalog.yml @@ -21,207 +21,229 @@ int_workflow: int_input: 8 upload: request_state: - targets: - - destination: {type: "hdas"} - items: - - src: url - name: "Reference information" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "txt" - - src: url - name: "Reference sequence" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fasta" - - src: url - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fastq.gz" - - destination: {type: "hdca"} + - 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" - items: - - src: url - name: "sample1" - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + elements: + - class: File + name: sample1 + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: sample2 - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: sample3 - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - destination: {type: "hdca"} + - class: Collection collection_type: list:paired name: "The List of Dataset Pairs" - items: - - name: sample1 - items: - - src: url + elements: + - class: Collection + collection_type: paired + name: sample1 + elements: + - class: File name: forward - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: reverse - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - name: sample2 - items: - - src: url + class: Collection + collection_type: paired + elements: + - class: File name: forward - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: reverse - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - destination: {type: "hdca"} + - class: Collection collection_type: list:list:paired name: "Nested List of Dataset Pairs" - items: - - name: treatment1 - items: - - name: replicate1 - items: - - src: url + elements: + - class: Collection + collection_type: list:paired + name: treatment1 + elements: + - class: Collection + collection_type: paired + name: replicate1 + elements: + - class: File name: forward - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: reverse - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - name: replicate2 - items: - - src: url + - class: Collection + collection_type: paired + name: replicate2 + elements: + - class: File name: forward - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: reverse - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - name: treatment2 - items: - - name: replicate1 - items: - - src: url + - class: Collection + collection_type: list:paired + name: treatment2 + elements: + - class: Collection + collection_type: paired + name: replicate1 + elements: + - class: File name: forward - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: reverse - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - name: replicate2 - items: - - src: url + - class: Collection + collection_type: paired + name: replicate2 + elements: + - class: File name: forward - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - src: url + - class: File name: reverse - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" - - destination: {type: "hdas"} - items: - - src: url - name: "sample1.fasta" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fasta" - tags: ['name:sample1'] - - src: url - name: "sample2.fasta" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fasta" - tags: ['name:sample2'] - - destination: {type: "hdas"} - items: - - src: url - name: "sample1.fastq" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fastq" - tags: ['group:treatment:treatment1', 'group:replicate:replicate1'] - - src: url - name: "sample2.fastq" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fastq" - tags: ['group:treatment:treatment1', 'group:replicate:replicate2'] - - src: url - name: "sample3.fastq" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fastq" - tags: ['group:treatment:treatment2', 'group:replicate:replicate1'] - - src: url - name: "sample4.fastq" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fastq" - tags: ['group:treatment:treatment2', 'group:replicate:replicate2'] - - destination: {type: "hdas"} - items: - - src: url - name: "Convert spaces and not newlines" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "txt" - space_to_tab: true - to_posix_lines: false - - src: url - name: "Convert newlines and not spaces" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fasta" - space_to_tab: false - to_posix_lines: true - - src: url - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fastq.gz" - - destination: {type: "hdas"} - items: - - src: url - name: "Reference information" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "txt" - dbkey: hg19 - - src: url - name: "Reference sequence" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fasta" - dbkey: hg19 - - src: url - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" - ext: "fastq.gz" - - destination: {type: "hdca"} + - 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" - items: - - name: "forward" - items: - - src: url - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + elements: + - class: Collection + collection_type: paired + name: "forward" + elements: + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" name: "forward" - items: - - src: url - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" name: "reverse" - - name: "reverse" - items: - - src: url - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + - class: Collection + collection_type: paired + name: "reverse" + elements: + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" name: "forward" - items: - - src: url - url: "base64://eyJ0c3JjIjogInRlc3QifQ==" + - class: File + location: "base64://eyJ0c3JjIjogInRlc3QifQ==" ext: "txt" name: "reverse" upload_one: request_state: - targets: - - destination: {type: "hdas"} - items: - - src: url + - class: Collection + collection_type: list + name: "my collection" + elements: + - class: File name: "Reference information" - url: "base64://eyJ0ZXN0IjogInRlc3QifQ==" + location: "base64://eyJ0ZXN0IjogInRlc3QifQ==" ext: "txt" diff --git a/lib/galaxy/tool_util_models/parameters.py b/lib/galaxy/tool_util_models/parameters.py index b8daa3f3cfb..3ed3c6f13f5 100644 --- a/lib/galaxy/tool_util_models/parameters.py +++ b/lib/galaxy/tool_util_models/parameters.py @@ -19,6 +19,7 @@ from typing import ( 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 @@ _DataRequest = Annotated[ 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/services/_fetch_util.py b/lib/galaxy/webapps/galaxy/services/_fetch_util.py index ecc8ad50be9..badb02f438a 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 a954832d984..da476834990 100644 --- a/lib/galaxy/webapps/galaxy/services/tools.py +++ b/lib/galaxy/webapps/galaxy/services/tools.py @@ -37,13 +37,26 @@ from galaxy.model import ( from galaxy.schema.credentials import CredentialsContext from galaxy.schema.fetch_data import ( CreateDataLandingPayload, + 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 +101,82 @@ def validate_tool_for_running(trans: ProvidesHistoryContext, tool_ref: ToolRunRe return tool +def data_landing_payload_to_fetch_targets(data_landing_payload: CreateDataLandingPayload): + """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, @@ -107,12 +196,13 @@ class ToolsService(ServiceBase): data_landing_payload: CreateDataLandingPayload, ) -> CreateToolLandingRequestPayload: request_version = "1" - payload = data_landing_payload.model_dump(exclude_unset=True)["request_state"] + payload = {"targets": data_landing_payload_to_fetch_targets(data_landing_payload)} validate_and_normalize_targets(trans, payload, set_internal_fields=False) - validated_back_to_model = TargetsAdapter.validate_python(payload["targets"]) request_state = { "request_version": request_version, - "request_json": {"targets": TargetsAdapter.dump_python(validated_back_to_model, exclude_unset=True)}, + "request_json": { + "targets": payload["targets"], + }, "file_count": "0", } return CreateToolLandingRequestPayload( diff --git a/lib/galaxy_test/api/test_landing.py b/lib/galaxy_test/api/test_landing.py index 6efbb0a85fc..eb42d7f970c 100644 --- a/lib/galaxy_test/api/test_landing.py +++ b/lib/galaxy_test/api/test_landing.py @@ -7,7 +7,7 @@ from pydantic import HttpUrl from galaxy.schema.fetch_data import ( CreateDataLandingPayload, - DataLandingRequestState, + FileOrCollectionRequestsAdapter, ) from galaxy.schema.schema import ( CreateToolLandingRequestPayload, @@ -65,19 +65,14 @@ class TestLandingApi(ApiTestCase): assert "Input should be a valid integer" in response.text def test_data_landing(self): - data_landing_request_state = DataLandingRequestState( - targets=[ + data_landing_request_state = FileOrCollectionRequestsAdapter.validate_python( + [ { - "destination": {"type": "hdas"}, - "items": [ - { - "src": "url", - "url": "base64://eyJ0ZXN0IjogInRlc3QifQ==", # base64 encoded {"test": "test"} - "ext": "txt", - "deferred": False, - } - ], - } + "class": "File", + "location": "base64://eyJ0ZXN0IjogInRlc3QifQ==", # base64 encoded {"test": "test"} + "filetype": "txt", + "deferred": False, + }, ], ) payload = CreateDataLandingPayload(request_state=data_landing_request_state, public=True)