diff --git a/.github/actions/nf-test-action/action.yml b/.github/actions/nf-test-action/action.yml index 6b755100f3a..0ee622bf000 100644 --- a/.github/actions/nf-test-action/action.yml +++ b/.github/actions/nf-test-action/action.yml @@ -71,6 +71,7 @@ runs: shell: bash env: SENTIEON_LICSRVR_IP: ${{ env.SENTIEON_LICSRVR_IP }} + METASPACE_API_KEY: ${{ env.METASPACE_API_KEY }} SENTIEON_AUTH_MECH: "GitHub Actions - token" run: | NFT_WORKDIR=~ \ diff --git a/.github/workflows/nf-test.yml b/.github/workflows/nf-test.yml index 4357bc84804..9cfdbc260cd 100644 --- a/.github/workflows/nf-test.yml +++ b/.github/workflows/nf-test.yml @@ -166,6 +166,7 @@ jobs: if: ${{steps.filter.outputs.filtered_paths != '[]'}} uses: ./.github/actions/nf-test-action env: + METASPACE_API_KEY: ${{ secrets.METASPACE_API_KEY }} SENTIEON_ENCRYPTION_KEY: ${{ secrets.SENTIEON_ENCRYPTION_KEY }} SENTIEON_LICENSE_MESSAGE: ${{ secrets.SENTIEON_LICENSE_MESSAGE }} SENTIEON_LICSRVR_IP: ${{ secrets.SENTIEON_LICSRVR_IP }} diff --git a/modules/nf-core/metaspace/submit/environment.yml b/modules/nf-core/metaspace/submit/environment.yml new file mode 100644 index 00000000000..2384d3b3e48 --- /dev/null +++ b/modules/nf-core/metaspace/submit/environment.yml @@ -0,0 +1,8 @@ +channels: +- conda-forge +- bioconda +dependencies: +- bioconda::metaspace2020=2.0.9 +- conda-forge::python=3.11.11 +- conda-forge::pip=25.0.1 +- conda-forge::pyyaml=6.0.2 diff --git a/modules/nf-core/metaspace/submit/main.nf b/modules/nf-core/metaspace/submit/main.nf new file mode 100644 index 00000000000..2e97c3cf4e3 --- /dev/null +++ b/modules/nf-core/metaspace/submit/main.nf @@ -0,0 +1,43 @@ +process METASPACE_SUBMIT { + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/c3/c317f9380b8b631acacad83ab362b2badb42e8782f6bfa03e5befe59f2382283/data': + 'community.wave.seqera.io/library/python_pip_metaspace-converter:958b8906de66e072' }" + + input: + path imzml + path ibd + path config + + output: + path("ds_id.txt") , emit: ds_id + path("versions.yml"), emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def metaspaceApi = secrets.METASPACE_API_KEY ? + "export API_KEY=\$(mktemp);echo -n \"${secrets.METASPACE_API_KEY}\" > \$API_KEY; " : + "" + + """ + $metaspaceApi + """ + template 'submit.py' + + stub: + + """ + touch ds_id.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + python: \$(python --version | cut -d ' ' -f 2) + metaspace: \$(python3 -c 'import metaspace; print(metaspace.__version__)') + yaml: \$(python3 -c 'import yaml; print(yaml.__version__)') + END_VERSIONS + """ +} diff --git a/modules/nf-core/metaspace/submit/meta.yml b/modules/nf-core/metaspace/submit/meta.yml new file mode 100644 index 00000000000..f7a22a0c54b --- /dev/null +++ b/modules/nf-core/metaspace/submit/meta.yml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +name: "metaspace_submit" +description: Submit imaging mass spectrometry data to METASPACE for metabolite annotation +keywords: +- imaging +- mass_spectrometry +- metabolomics +- annotation +tools: +- "metaspace2020": + description: "Python package providing programmatic access to the METASPACE platform" + homepage: "https://metaspace2020.readthedocs.io" + documentation: "https://metaspace2020.readthedocs.io" + tool_dev_url: "https://github.com/metaspace2020/metaspace/tree/master/metaspace/python-client" + licence: ["Apache-2.0 license"] + identifier: biotools:metaspace + +input: +- - imzml: + type: file + description: Path to .imzML file +- - ibd: + type: file + description: Path to .ibd file +- - config: + type: file + description: Path to .yml file containing dataset configuration and metadata +output: +- ds_id: + - "ds_id.txt": + type: file + description: File containing METASPACE dataset ID + pattern: "ds_id.txt" + +- versions: + - "versions.yml": + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: +- "@bisho2122" +maintainers: +- "@bisho2122" diff --git a/modules/nf-core/metaspace/submit/templates/submit.py b/modules/nf-core/metaspace/submit/templates/submit.py new file mode 100644 index 00000000000..db5efcd99f2 --- /dev/null +++ b/modules/nf-core/metaspace/submit/templates/submit.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +import yaml +import metaspace +import platform +import os + +with open("${config}") as f: + config = yaml.safe_load(f) + +api_key_path = os.getenv('API_KEY') + +if api_key_path and os.path.isfile(api_key_path): + with open(api_key_path, "r") as f: + API_key = f.read() +else: + API_key = api_key_path + +sm = metaspace.SMInstance(api_key=API_key) + +ds_name = config['Dataset_name'] + +Annot_settings = config['Annotation_settings'] + +metadata = {k: v for k, v in config.items() if k not in ['User_info','Dataset_name','Annotation_settings']} +metadata = {'Data_Type': 'Imaging MS', **metadata} + +dbs = [tuple(item) for item in Annot_settings["Metabolite_database"]] + +if len(Annot_settings['Adducts']) == 0: + adduct_list = None +else: + adduct_list = Annot_settings['Adducts'] + +if len(Annot_settings['Chemical_modifications']) == 0: + chem_modif = None +else: + chem_modif = Annot_settings['Chemical_modifications'] + +scoring_model_id_map = { + 'MSM': 2, + 'METASPACE_ML_Animal': 3, + 'METASPACE_ML_Plant': 4 +} + +scoring_model_id = scoring_model_id_map[Annot_settings['Analysis_version']] + +# Submit dataset + +ds_id = sm.submit_dataset( + imzml_fn = "${imzml}", + ibd_fn = "${ibd}", + name = ds_name, + metadata = metadata, + is_public = False, + databases = dbs, + adducts = adduct_list, + chem_mods = chem_modif, + scoring_model = scoring_model_id, + ppm = float(Annot_settings['ppm']) +) + +with open("ds_id.txt", 'w') as f: + f.write(ds_id) + +# Versions +versions = {"${task.process}": {"python": platform.python_version(), + "metaspace": metaspace.__version__, + "yaml": yaml.__version__}} + +def format_yaml_like(data: dict, indent: int = 0) -> str: + """Formats a dictionary to a YAML-like string. + + Args: + data (dict): The dictionary to format. + indent (int): The current indentation level. + + Returns: + str: A string formatted as YAML. + """ + yaml_str = "" + for key, value in data.items(): + spaces = " " * indent + if isinstance(value, dict): + yaml_str += f"{spaces}{key}:\\n{format_yaml_like(value, indent + 1)}" + else: + yaml_str += f"{spaces}{key}: {value}\\n" + return yaml_str + +with open("versions.yml", "w") as f: + f.write(format_yaml_like(versions)) diff --git a/modules/nf-core/metaspace/submit/tests/main.nf.test b/modules/nf-core/metaspace/submit/tests/main.nf.test new file mode 100644 index 00000000000..579f3177b04 --- /dev/null +++ b/modules/nf-core/metaspace/submit/tests/main.nf.test @@ -0,0 +1,52 @@ +nextflow_process { + + name "Test Process METASPACE_SUBMIT" + script "../main.nf" + process "METASPACE_SUBMIT" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "metaspace" + tag "metaspace/submit" + + test("metaspace_submit_test") { + + when { + process { + """ + input[0] = file(params.test_data_base_path + 'test_metaspace_submit/test.imzML', checkIfExists: true) + input[1] = file(params.test_data_base_path + 'test_metaspace_submit/test.ibd', checkIfExists: true) + input[2] = file(params.test_data_base_path + 'test_metaspace_submit/ds_config.yml') + """ + } + } + + then { + assertAll( + { assert process.success } + ) + } + } + test("metaspace_submit - stub") { + tag "versions_test" + + options '-stub' + when { + process { + """ + input[0] = file(params.test_data_base_path + 'test_metaspace_submit/test.imzML', checkIfExists: true) + input[1] = file(params.test_data_base_path + 'test_metaspace_submit/test.ibd', checkIfExists: true) + input[2] = file(params.test_data_base_path + 'test_metaspace_submit/ds_config.yml') + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + +} diff --git a/modules/nf-core/metaspace/submit/tests/main.nf.test.snap b/modules/nf-core/metaspace/submit/tests/main.nf.test.snap new file mode 100644 index 00000000000..4f3badedf9b --- /dev/null +++ b/modules/nf-core/metaspace/submit/tests/main.nf.test.snap @@ -0,0 +1,25 @@ +{ + "metaspace_submit - stub": { + "content": [ + { + "0": [ + "ds_id.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ], + "1": [ + "versions.yml:md5,db6244919f766cddc2cd20502db9403d" + ], + "ds_id": [ + "ds_id.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ], + "versions": [ + "versions.yml:md5,db6244919f766cddc2cd20502db9403d" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.5" + }, + "timestamp": "2025-03-31T21:55:03.550137605" + } +} \ No newline at end of file diff --git a/modules/nf-core/metaspace/submit/tests/nextflow.config b/modules/nf-core/metaspace/submit/tests/nextflow.config new file mode 100644 index 00000000000..f65d17e4434 --- /dev/null +++ b/modules/nf-core/metaspace/submit/tests/nextflow.config @@ -0,0 +1,11 @@ +env { + // NOTE This is how nf-core/metaspace/submit users will use METASPACE submission in real world use + API_KEY = "$METASPACE_API_KEY" + + // NOTE This is how nf-core/metaspace/submit users will test out METASPACE submission with a user API key + // nextflow secrets set METASPACE_API_KEY "USER_API_KEY" +} + +params { + test_data_base_path = "https://raw.githubusercontent.com/nf-core/test-datasets/spatialmetabo/" +}