Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## lifebit-ai/cloudos-cli: changelog

## v2.80.0 (2026-02-16)

### Feat

- Adds new parameter `--params-file` for `job run` that supports .json/.yaml/.yml extensions

## v2.79.0 (2026-01-28)

### Patch
Expand Down
2 changes: 1 addition & 1 deletion cloudos_cli/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.79.0'
__version__ = '2.80.0'
6 changes: 6 additions & 0 deletions cloudos_cli/jobs/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ def job():
@click.option('--job-config',
help=('A config file similar to a nextflow.config file, ' +
'but only with the parameters to use with your job.'))
@click.option('--params-file',
help=('A file containing the parameters to pass to the job call. ' +
'It should be a .json or .yaml file with a dictionary structure ' +
'where keys are parameter names and values are parameter values.'))
@click.option('-p',
'--parameter',
multiple=True,
Expand Down Expand Up @@ -199,6 +203,7 @@ def run(ctx,
workflow_name,
last,
job_config,
params_file,
parameter,
git_commit,
git_tag,
Expand Down Expand Up @@ -400,6 +405,7 @@ def run(ctx,
if workflow_type == 'nextflow':
print(f'\tNextflow version: {nextflow_version}')
j_id = j.send_job(job_config=job_config,
params_file=params_file,
parameter=parameter,
is_module=is_module,
git_commit=git_commit,
Expand Down
75 changes: 75 additions & 0 deletions cloudos_cli/jobs/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from cloudos_cli.utils.errors import BadRequestException
from cloudos_cli.utils.requests import retry_requests_post, retry_requests_get, retry_requests_delete
from pathlib import Path
from urllib.parse import urlparse
import base64
from cloudos_cli.utils.array_job import classify_pattern, get_file_or_folder_id, extract_project
import os
Expand Down Expand Up @@ -174,8 +175,73 @@ def fetch_cloudos_id(self,
else:
raise ValueError(f'No {name} element in {resource} was found')

def build_parameters_file_payload(self, params_file):
"""Build the parametersFile payload for a params file path."""
if params_file is None:
return None
if isinstance(params_file, (list, tuple)):
if len(params_file) != 1:
raise ValueError('Please, provide a single file for --params-file.')
params_file = params_file[0]

ext = os.path.splitext(params_file)[1].lower()
allowed_ext = {'.json', '.yaml', '.yml'}
if ext not in allowed_ext:
raise ValueError('Please, provide a .json or .yaml file for --params-file.')

if params_file.startswith('s3://'):
parsed = urlparse(params_file)
bucket = parsed.netloc
s3_key = parsed.path.lstrip('/')
if not bucket or not s3_key:
raise ValueError('Invalid S3 URL. Please, provide a full s3://bucket/key path.')
name = s3_key.rstrip('/').split('/')[-1]
return {
"parametersFile": {
"dataItemEmbedded": {
"data": {
"name": name,
"s3BucketName": bucket,
"s3ObjectKey": s3_key
},
"type": "S3File"
}
}
}

if not self.project_name:
raise ValueError('Please, provide --project-name to resolve --params-file paths.')

normalized_path = params_file.lstrip('/')
allowed_prefixes = ('Data', 'Analyses Results', 'Cohorts')
if not normalized_path.startswith(allowed_prefixes):
raise ValueError('Params file path must start with Data, Analyses Results, or Cohorts.')

command_path = Path(normalized_path)
command_dir = str(command_path.parent)
command_name = command_path.name
file_id = get_file_or_folder_id(
self.cloudos_url,
self.apikey,
self.workspace_id,
self.project_name,
self.verify,
command_dir,
command_name,
is_file=True
)
return {
"parametersFile": {
"dataItem": {
"kind": "File",
"item": f"{file_id}"
}
}
}

def convert_nextflow_to_json(self,
job_config,
params_file,
parameter,
array_parameter,
array_file_header,
Expand Down Expand Up @@ -217,6 +283,8 @@ def convert_nextflow_to_json(self,
----------
job_config : string
Path to a nextflow.config file with parameters scope.
params_file : string
S3 or File Explorer path to a JSON/YAML file with Nextflow parameters.
parameter : tuple
Tuple of strings indicating the parameters to pass to the pipeline call.
They are in the following form: ('param1=param1val', 'param2=param2val', ...)
Expand Down Expand Up @@ -424,6 +492,9 @@ def convert_nextflow_to_json(self,
"usesFusionFileSystem": use_mountpoints,
"accelerateSavingResults": accelerate_saving_results
}
params_file_payload = self.build_parameters_file_payload(params_file)
if params_file_payload is not None:
params.update(params_file_payload)
if workflow_type != 'docker':
params["nextflowVersion"] = nextflow_version
if execution_platform != 'hpc':
Expand Down Expand Up @@ -473,6 +544,7 @@ def convert_nextflow_to_json(self,

def send_job(self,
job_config=None,
params_file=None,
parameter=(),
array_parameter=(),
array_file_header=None,
Expand Down Expand Up @@ -513,6 +585,8 @@ def send_job(self,
----------
job_config : string
Path to a nextflow.config file with parameters scope.
params_file : string
S3 or File Explorer path to a JSON/YAML file with Nextflow parameters.
parameter : tuple
Tuple of strings indicating the parameters to pass to the pipeline call.
They are in the following form: ('param1=param1val', 'param2=param2val', ...)
Expand Down Expand Up @@ -609,6 +683,7 @@ def send_job(self,
"apikey": apikey
}
params = self.convert_nextflow_to_json(job_config,
params_file,
parameter,
array_parameter,
array_file_header,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bash/test_get_array_file_cols.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def test_get_array_file_columns():
s3_object_key_b64 = base64.b64encode(s3_object_key.encode()).decode()
break
else:
raise ValueError(f'File "{file_name}" not found in the "Data" folder of the project "{project_name}".')
raise ValueError(f'File "{file_name}" not found in the "Data" folder of the project "{PROJECT_NAME}".')

url = (
f"{CLOUDOS_URL}/api/v1/jobs/array-file/metadata"
Expand Down
87 changes: 83 additions & 4 deletions tests/test_jobs/test_convert_nextflow_to_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
param_dict = {
"config": "cloudos_cli/examples/rnatoy.config",
"parameter": (),
"params_file": None,
"array_parameter": (),
"array_file_header": None,
"is_module": False,
Expand Down Expand Up @@ -46,8 +47,19 @@


def test_convert_nextflow_to_json_output_correct():
job_json = Job.convert_nextflow_to_json(
1, param_dict["config"],
job = Job(
"https://cloudos.example",
"test_api_key",
None,
"workspace_id",
"project",
"workflow",
project_id=param_dict["project_id"],
workflow_id=param_dict["workflow_id"]
)
job_json = job.convert_nextflow_to_json(
param_dict["config"],
param_dict["params_file"],
parameter=param_dict["parameter"],
array_parameter=param_dict["array_parameter"],
array_file_header=param_dict["array_file_header"],
Expand Down Expand Up @@ -92,8 +104,19 @@ def test_convert_nextflow_to_json_output_correct():
def test_convert_nextflow_to_json_badly_formed_config():
no_equals_config = "tests/test_data/wrong_params.config"
with pytest.raises(ValueError) as excinfo:
Job.convert_nextflow_to_json(
1, no_equals_config,
job = Job(
"https://cloudos.example",
"test_api_key",
None,
"workspace_id",
"project",
"workflow",
project_id=param_dict["project_id"],
workflow_id=param_dict["workflow_id"]
)
job.convert_nextflow_to_json(
no_equals_config,
param_dict["params_file"],
parameter=param_dict["parameter"],
array_parameter=param_dict["array_parameter"],
array_file_header=param_dict["array_file_header"],
Expand Down Expand Up @@ -135,3 +158,59 @@ def test_convert_nextflow_to_json_badly_formed_config():
tests/test_data/wrong_params.config\
using the \'=\' as spacer.\
E.g: name = my_name".replace(" ", "") in str(excinfo.value)


def test_params_file_payload_s3():
job = Job(
"https://cloudos.example",
"test_api_key",
None,
"workspace_id",
"project",
"workflow",
project_id=param_dict["project_id"],
workflow_id=param_dict["workflow_id"]
)
payload = job.build_parameters_file_payload("s3://my-bucket/path/to/params.json")
assert payload == {
"parametersFile": {
"dataItemEmbedded": {
"data": {
"name": "params.json",
"s3BucketName": "my-bucket",
"s3ObjectKey": "path/to/params.json"
},
"type": "S3File"
}
}
}


def test_params_file_payload_file_explorer(monkeypatch):
job = Job(
"https://cloudos.example",
"test_api_key",
None,
"workspace_id",
"project",
"workflow",
project_id=param_dict["project_id"],
workflow_id=param_dict["workflow_id"]
)

def fake_get_file_or_folder_id(*args, **kwargs):
return "file-123"

monkeypatch.setattr(
"cloudos_cli.jobs.job.get_file_or_folder_id",
fake_get_file_or_folder_id
)
payload = job.build_parameters_file_payload("Data/params-files/run160226.json")
assert payload == {
"parametersFile": {
"dataItem": {
"kind": "File",
"item": "file-123"
}
}
}