Skip to content
Draft
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.91.1 (2026-06-18)

### Patch

- Syncs with CloudOS API endpoint changes: `POST /api/v2/jobs`, `GET /api/v2/jobs`, and `PUT /api/v2/jobs/{job_id}/abort` are now served at `/api/v2/jobsNew`, `/api/v2/jobsNew`, and `/api/v2/jobsNew/{id}/abort` respectively. Updates all affected CLI commands (`cloudos job run`, `cloudos job list`, `cloudos job abort`, `cloudos job clone`, `cloudos job resume`, `cloudos bash job`, `cloudos bash array-job`) to use the new paths.

## v2.91.0 (2026-05-28)

### Feat:
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.91.0'
__version__ = '2.91.1'
4 changes: 2 additions & 2 deletions cloudos_cli/clos.py
Original file line number Diff line number Diff line change
Expand Up @@ -1205,7 +1205,7 @@ def get_job_list(self, workspace_id, last_n_jobs=None, page=None, page_size=None
while True:
params["page"] = current_page

r = retry_requests_get(f"{self.cloudos_url}/api/v2/jobs", params=params, headers=headers, verify=verify)
r = retry_requests_get(f"{self.cloudos_url}/api/v2/jobsNew", params=params, headers=headers, verify=verify)
if r.status_code >= 400:
raise BadRequestException(r)

Expand Down Expand Up @@ -1883,7 +1883,7 @@ def abort_job(self, job, workspace_id, verify=True, force=False):
"apikey": apikey
}
force_abort = "true" if force else "false"
r = retry_requests_put("{}/api/v2/jobs/{}/abort?forceAbort={}&teamId={}".format(cloudos_url, job, force_abort, workspace_id),
r = retry_requests_put("{}/api/v2/jobsNew/{}/abort?forceAbort={}&teamId={}".format(cloudos_url, job, force_abort, workspace_id),
headers=headers, verify=verify)
if r.status_code >= 400:
raise BadRequestException(r)
Expand Down
6 changes: 3 additions & 3 deletions cloudos_cli/jobs/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ def send_job(self,
command=command,
cpus=cpus,
memory=memory)
r = retry_requests_post("{}/api/v2/jobs?teamId={}".format(cloudos_url,
r = retry_requests_post("{}/api/v2/jobsNew?teamId={}".format(cloudos_url,
workspace_id),
data=json.dumps(params), headers=headers, verify=verify)
if r.status_code >= 400:
Expand Down Expand Up @@ -1364,7 +1364,7 @@ def clone_or_resume_job(self,
}
clean_payload = self.fix_boolean_strings(cloned_payload)

r = retry_requests_post(f"{self.cloudos_url}/api/v2/jobs?teamId={self.workspace_id}",
r = retry_requests_post(f"{self.cloudos_url}/api/v2/jobsNew?teamId={self.workspace_id}",
data=json.dumps(clean_payload),
headers=headers,
verify=verify)
Expand Down Expand Up @@ -1449,7 +1449,7 @@ def get_job_relatedness(self, workspace_id, workdir_folder_id, limit=100, verify
"teamId": workspace_id
}

url = f"{self.cloudos_url}/api/v2/jobs"
url = f"{self.cloudos_url}/api/v2/jobsNew"
response = retry_requests_get(url, params=params, headers=headers, verify=verify)

if response.status_code >= 400:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bash/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_send_bash_job():
# mock GET method with the .json
responses.add(
responses.POST,
url=f"{CLOUDOS_URL}/api/v2/jobs?{search_str}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew?{search_str}",
body=create_json,
headers=header,
match=[matchers.query_param_matcher(params_job)],
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bash/test_send_bash_array_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def test_send_bash_array_job():
# mock GET method with the .json
responses.add(
responses.POST,
url=f"{CLOUDOS_URL}/api/v2/jobs?{search_str}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew?{search_str}",
body=create_json,
headers=header,
match=[matchers.query_param_matcher(params_job)],
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bash/test_send_bash_array_job_multi_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def test_send_bash_array_job():
# mock GET method with the .json
responses.add(
responses.POST,
url=f"{CLOUDOS_URL}/api/v2/jobs?{search_str}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew?{search_str}",
body=create_json,
headers=header,
match=[matchers.query_param_matcher(params_job)],
Expand Down
8 changes: 4 additions & 4 deletions tests/test_clos/test_abort_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_abort_job_correct_response():
# mock GET method with the .json
responses.add(
responses.PUT,
url=f"{CLOUDOS_URL}/api/v2/jobs/{JOB_ID}/abort?forceAbort=false&teamId={WORKSPACE_ID}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew/{JOB_ID}/abort?forceAbort=false&teamId={WORKSPACE_ID}",
headers=header,
status=200)
# start cloudOS service
Expand Down Expand Up @@ -56,7 +56,7 @@ def test_abort_job_incorrect_response():
# mock GET method with the .json
responses.add(
responses.PUT,
url=f"{CLOUDOS_URL}/api/v2/jobs/{JOB_ID}/abort?forceAbort=false&teamId={WORKSPACE_ID}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew/{JOB_ID}/abort?forceAbort=false&teamId={WORKSPACE_ID}",
body=error_json,
headers=header,
status=400)
Expand All @@ -82,7 +82,7 @@ def test_abort_job_with_force_correct_response():
# mock PUT method with the .json
responses.add(
responses.PUT,
url=f"{CLOUDOS_URL}/api/v2/jobs/{JOB_ID}/abort?forceAbort=true&teamId={WORKSPACE_ID}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew/{JOB_ID}/abort?forceAbort=true&teamId={WORKSPACE_ID}",
headers=header,
status=200)
# start cloudOS service
Expand Down Expand Up @@ -110,7 +110,7 @@ def test_abort_job_with_force_incorrect_response():
# mock PUT method with the .json
responses.add(
responses.PUT,
url=f"{CLOUDOS_URL}/api/v2/jobs/{JOB_ID}/abort?forceAbort=true&teamId={WORKSPACE_ID}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew/{JOB_ID}/abort?forceAbort=true&teamId={WORKSPACE_ID}",
body=error_json,
headers=header,
status=400)
Expand Down
16 changes: 8 additions & 8 deletions tests/test_clos/test_archive_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ def test_job_archive_successful_flow():
with requests_mock.Mocker() as m:
# Mock checking if job is archived (should return empty for unarchived job)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=valid_job_123",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=valid_job_123",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)

# Mock checking if job exists in unarchived list (should return the job)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=false&page=1&limit=1&id=valid_job_123",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=false&page=1&limit=1&id=valid_job_123",
status_code=200,
json={"jobs": [{"_id": "valid_job_123", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)
Expand Down Expand Up @@ -54,14 +54,14 @@ def test_job_archive_multiple_jobs_successful_flow():
for job_id in ['job1', 'job2', 'job3']:
# Mock checking if job is archived (should return empty for unarchived jobs)
m.get(
f"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id={job_id}",
f"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id={job_id}",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)

# Mock checking if job exists in unarchived list (should return the job)
m.get(
f"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=false&page=1&limit=1&id={job_id}",
f"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=false&page=1&limit=1&id={job_id}",
status_code=200,
json={"jobs": [{"_id": job_id, "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)
Expand Down Expand Up @@ -93,24 +93,24 @@ def test_job_archive_mixed_valid_invalid_jobs():
with requests_mock.Mocker() as m:
# Mock job status check - valid job (not archived)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=valid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=valid_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=false&page=1&limit=1&id=valid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=false&page=1&limit=1&id=valid_job",
status_code=200,
json={"jobs": [{"_id": "valid_job", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)

# Mock job status check - invalid job (not in either list)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=invalid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=invalid_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=false&page=1&limit=1&id=invalid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=false&page=1&limit=1&id=invalid_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
Expand Down
20 changes: 10 additions & 10 deletions tests/test_clos/test_archive_status_checking.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_archive_already_archived_job():
with requests_mock.Mocker() as m:
# Mock checking if job is archived (should return the job since it's archived)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=already_archived_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=already_archived_job",
status_code=200,
json={"jobs": [{"_id": "already_archived_job", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)
Expand All @@ -40,14 +40,14 @@ def test_unarchive_already_unarchived_job():
with requests_mock.Mocker() as m:
# Mock checking if job is archived (should return empty for unarchived job)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=not_archived_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=not_archived_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)

# Mock checking if job exists in unarchived list (should return the job)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=false&page=1&limit=1&id=not_archived_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=false&page=1&limit=1&id=not_archived_job",
status_code=200,
json={"jobs": [{"_id": "not_archived_job", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)
Expand All @@ -74,18 +74,18 @@ def test_archive_mixed_status_jobs():
with requests_mock.Mocker() as m:
# Mock job status - one archived, one not archived
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=already_archived",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=already_archived",
status_code=200,
json={"jobs": [{"_id": "already_archived", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)

m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=not_archived",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=not_archived",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=false&page=1&limit=1&id=not_archived",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=false&page=1&limit=1&id=not_archived",
status_code=200,
json={"jobs": [{"_id": "not_archived", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)
Expand Down Expand Up @@ -117,18 +117,18 @@ def test_unarchive_mixed_status_jobs():
with requests_mock.Mocker() as m:
# Mock job status - one archived, one not archived
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=archived_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=archived_job",
status_code=200,
json={"jobs": [{"_id": "archived_job", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)

m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=not_archived",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=not_archived",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=false&page=1&limit=1&id=not_archived",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=false&page=1&limit=1&id=not_archived",
status_code=200,
json={"jobs": [{"_id": "not_archived", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)
Expand Down Expand Up @@ -160,7 +160,7 @@ def test_archive_verbose_already_archived():
with requests_mock.Mocker() as m:
# Mock checking if job is archived (should return the job since it's archived)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=workspace_123&archived.status=true&page=1&limit=1&id=already_archived",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=workspace_123&archived.status=true&page=1&limit=1&id=already_archived",
status_code=200,
json={"jobs": [{"_id": "already_archived", "status": "completed"}], "pagination_metadata": {"Pagination-Count": 1}}
)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_clos/test_cli_archive_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ def test_job_archive_invalid_job_ids():
with requests_mock.Mocker() as m:
# Mock the job status check to fail (job doesn't exist in either list)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=test_workspace&archived.status=true&page=1&limit=1&id=invalid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=test_workspace&archived.status=true&page=1&limit=1&id=invalid_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=test_workspace&archived.status=false&page=1&limit=1&id=invalid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=test_workspace&archived.status=false&page=1&limit=1&id=invalid_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_clos/test_cli_unarchive_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ def test_job_unarchive_invalid_job_ids():
with requests_mock.Mocker() as m:
# Mock the job status check to fail (job doesn't exist in either list)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=test_workspace&archived.status=true&page=1&limit=1&id=invalid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=test_workspace&archived.status=true&page=1&limit=1&id=invalid_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
m.get(
"https://cloudos.lifebit.ai/api/v2/jobs?teamId=test_workspace&archived.status=false&page=1&limit=1&id=invalid_job",
"https://cloudos.lifebit.ai/api/v2/jobsNew?teamId=test_workspace&archived.status=false&page=1&limit=1&id=invalid_job",
status_code=200,
json={"jobs": [], "pagination_metadata": {"Pagination-Count": 0}}
)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_clos/test_get_job_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_get_job_list_correct_response():
# mock GET method with the .json
responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs?{search_str}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew?{search_str}",
body=create_json,
headers=header,
match=[matchers.query_param_matcher(params)],
Expand Down Expand Up @@ -67,7 +67,7 @@ def test_get_job_list_incorrect_response():
# mock GET method with the .json
responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs?{search_str}",
url=f"{CLOUDOS_URL}/api/v2/jobsNew?{search_str}",
body=error_json,
headers=header,
match=[matchers.query_param_matcher(params)],
Expand Down
12 changes: 6 additions & 6 deletions tests/test_clos/test_get_job_list_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_filter_by_status():
}
responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs",
url=f"{CLOUDOS_URL}/api/v2/jobsNew",
json=MOCK_JOB_LIST,
match=[matchers.query_param_matcher(expected_params)],
status=200
Expand All @@ -86,7 +86,7 @@ def test_filter_by_job_name():
}
responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs",
url=f"{CLOUDOS_URL}/api/v2/jobsNew",
json=MOCK_JOB_LIST,
match=[matchers.query_param_matcher(expected_params)],
status=200
Expand All @@ -107,7 +107,7 @@ def test_filter_by_job_id():
}
responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs",
url=f"{CLOUDOS_URL}/api/v2/jobsNew",
json=MOCK_JOB_LIST,
match=[matchers.query_param_matcher(expected_params)],
status=200
Expand All @@ -134,7 +134,7 @@ def test_filter_only_mine():
}
responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs",
url=f"{CLOUDOS_URL}/api/v2/jobsNew",
json=MOCK_JOB_LIST,
match=[matchers.query_param_matcher(expected_params)],
status=200
Expand All @@ -150,7 +150,7 @@ def test_filter_by_queue(mock_get_queues):
mock_get_queues.return_value = MOCK_QUEUE_LIST
responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs",
url=f"{CLOUDOS_URL}/api/v2/jobsNew",
json=MOCK_JOB_LIST,
status=200
)
Expand Down Expand Up @@ -195,7 +195,7 @@ def test_filter_by_system_queue(mock_get_queues):

responses.add(
responses.GET,
url=f"{CLOUDOS_URL}/api/v2/jobs",
url=f"{CLOUDOS_URL}/api/v2/jobsNew",
json=mock_job_with_system_queue,
status=200
)
Expand Down
Loading
Loading