From a4e870cacd11c2eaa22db7a0f23f0fd1840aae9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:17:25 +0000 Subject: [PATCH 1/2] Initial plan From 68a27019fe5e472b7fec558e9f464578bcdf34dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:23:00 +0000 Subject: [PATCH 2/2] feat: add project members command and endpoint support --- CHANGELOG.md | 6 +++ cloudos_cli/_version.py | 2 +- cloudos_cli/clos.py | 29 +++++++++- cloudos_cli/projects/cli.py | 43 +++++++++++++++ supported-endpoints.json | 26 +++++++++ tests/test_cli_project_members.py | 28 ++++++++++ tests/test_clos/test_get_project_members.py | 60 +++++++++++++++++++++ 7 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 supported-endpoints.json create mode 100644 tests/test_cli_project_members.py create mode 100644 tests/test_clos/test_get_project_members.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f2df07..f3ea1f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## lifebit-ai/cloudos-cli: changelog +## v2.91.1 (2026-06-09) + +### Patch + +- Adds `cloudos project members` command to collect project members from `/api/v1/projects/{id}/members`. + ## v2.91.0 (2026-05-28) ### Feat: diff --git a/cloudos_cli/_version.py b/cloudos_cli/_version.py index 1271f796..4d94094d 100644 --- a/cloudos_cli/_version.py +++ b/cloudos_cli/_version.py @@ -1 +1 @@ -__version__ = '2.91.0' +__version__ = '2.91.1' diff --git a/cloudos_cli/clos.py b/cloudos_cli/clos.py index 73f6f5c2..a60b08c2 100644 --- a/cloudos_cli/clos.py +++ b/cloudos_cli/clos.py @@ -1711,6 +1711,34 @@ def get_project_list(self, workspace_id, verify=True, get_all=True, else: return content['projects'] + def get_project_members(self, project_id, verify=True): + """Get members from a Lifebit Platform project. + + Parameters + ---------- + project_id : string + The Lifebit Platform project id to collect members from. + verify: [bool|string] + Whether to use SSL verification or not. Alternatively, if + a string is passed, it will be interpreted as the path to + the SSL certificate file. + + Returns + ------- + list | dict + The parsed server response with project members. + """ + headers = { + "Content-type": "application/json", + "apikey": self.apikey + } + r = retry_requests_get( + "{}/api/v1/projects/{}/members".format(self.cloudos_url, project_id), + headers=headers, verify=verify) + if r.status_code >= 400: + raise BadRequestException(r) + return json.loads(r.content) + @staticmethod def process_project_list(r, all_fields=False): """Process a server response from a self.get_project_list call. @@ -2615,4 +2643,3 @@ def mount_fuse_filesystem_v2(self, session_id, team_id, payload, verify=True): # Return the status code (204 No Content is success) return r.status_code - diff --git a/cloudos_cli/projects/cli.py b/cloudos_cli/projects/cli.py index 97ac4752..9a153913 100644 --- a/cloudos_cli/projects/cli.py +++ b/cloudos_cli/projects/cli.py @@ -186,3 +186,46 @@ def create_project(ctx, except Exception as e: print(f'\tError creating project: {str(e)}') sys.exit(1) + + +@project.command('members') +@click.option('-k', + '--apikey', + help='Your Lifebit Platform API key', + required=True) +@click.option('-c', + '--cloudos-url', + help=(f'The Lifebit Platform url you are trying to access to. Default={CLOUDOS_URL}.'), + default=CLOUDOS_URL, + required=True) +@click.option('--project-id', + help='The specific Lifebit Platform project id.', + required=True) +@click.option('--verbose', + help='Whether to print information messages or not.', + is_flag=True) +@click.option('--disable-ssl-verification', + help=('Disable SSL certificate verification. Please, remember that this option is ' + + 'not generally recommended for security reasons.'), + is_flag=True) +@click.option('--ssl-cert', + help='Path to your SSL certificate file.') +@click.option('--profile', help='Profile to use from the config file', default=None) +@click.pass_context +@with_profile_config(required_params=['apikey']) +def list_project_members(ctx, + apikey, + cloudos_url, + project_id, + verbose, + disable_ssl_verification, + ssl_cert, + profile): + """Collect and display all members from a Lifebit Platform project.""" + # apikey and cloudos_url are now automatically resolved by the decorator + verify_ssl = ssl_selector(disable_ssl_verification, ssl_cert) + if verbose: + print('\t...Preparing objects') + cl = Cloudos(cloudos_url, apikey, None) + response = cl.get_project_members(project_id, verify=verify_ssl) + print(json.dumps(response)) diff --git a/supported-endpoints.json b/supported-endpoints.json new file mode 100644 index 00000000..509263fd --- /dev/null +++ b/supported-endpoints.json @@ -0,0 +1,26 @@ +{ + "endpoints": [ + { + "id": "projects-create", + "method": "POST", + "path": "/api/v1/projects", + "query_params": [ + "teamId" + ], + "cli_command": "cloudos project create", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "projects-members-list", + "method": "GET", + "path": "/api/v1/projects/{id}/members", + "query_params": [], + "cli_command": "cloudos project members", + "source_file": "cloudos_cli/projects/cli.py", + "added_in_version": "2.91.1", + "notes": null + } + ] +} diff --git a/tests/test_cli_project_members.py b/tests/test_cli_project_members.py new file mode 100644 index 00000000..8d77386b --- /dev/null +++ b/tests/test_cli_project_members.py @@ -0,0 +1,28 @@ +from click.testing import CliRunner +from cloudos_cli.__main__ import run_cloudos_cli + + +def test_project_members_command_exists(): + """ + Test that the 'project members' command exists and shows proper help + """ + runner = CliRunner() + result = runner.invoke(run_cloudos_cli, ['project', 'members', '--help']) + + assert result.exit_code == 0 + assert 'Collect and display all members from a Lifebit Platform project' in result.output + assert '--project-id' in result.output + assert '--apikey' in result.output + assert '--cloudos-url' in result.output + + +def test_project_group_contains_members_command(): + """ + Test that the 'project' group contains the 'members' command + """ + runner = CliRunner() + result = runner.invoke(run_cloudos_cli, ['project', '--help']) + + assert result.exit_code == 0 + assert 'members' in result.output + assert 'Collect and display all members from a Lifebit Platform project' in result.output diff --git a/tests/test_clos/test_get_project_members.py b/tests/test_clos/test_get_project_members.py new file mode 100644 index 00000000..49a1c240 --- /dev/null +++ b/tests/test_clos/test_get_project_members.py @@ -0,0 +1,60 @@ +import mock +import json +import pytest +import responses +from cloudos_cli.clos import Cloudos +from cloudos_cli.utils.errors import BadRequestException + +APIKEY = 'vnoiweur89u2ongs' +CLOUDOS_URL = 'http://cloudos.lifebit.ai' +PROJECT_ID = '64a7b1c2f8d9e1a2b3c4d5e6' + + +@mock.patch('cloudos_cli.clos', mock.MagicMock()) +@responses.activate +def test_get_project_members_correct_response(): + """ + Test 'get_project_members' to work as intended + """ + members_payload = [ + {"id": "user-1", "email": "user-1@example.com", "role": "owner"}, + {"id": "user-2", "email": "user-2@example.com", "role": "member"} + ] + responses.add( + responses.GET, + url=f"{CLOUDOS_URL}/api/v1/projects/{PROJECT_ID}/members", + body=json.dumps(members_payload), + status=200 + ) + + clos = Cloudos(apikey=APIKEY, cromwell_token=None, cloudos_url=CLOUDOS_URL) + response = clos.get_project_members(PROJECT_ID) + + assert isinstance(response, list) + assert len(response) == 2 + assert response[0]['role'] == 'owner' + + +@mock.patch('cloudos_cli.clos', mock.MagicMock()) +@responses.activate +def test_get_project_members_incorrect_response(): + """ + Test 'get_project_members' to fail with '400' response + """ + error_message = { + "statusCode": 400, + "code": "BadRequest", + "message": "Invalid project id", + "time": "2026-06-09_17:18:00" + } + responses.add( + responses.GET, + url=f"{CLOUDOS_URL}/api/v1/projects/{PROJECT_ID}/members", + body=json.dumps(error_message), + status=400 + ) + + with pytest.raises(BadRequestException) as error: + clos = Cloudos(apikey=APIKEY, cromwell_token=None, cloudos_url=CLOUDOS_URL) + clos.get_project_members(PROJECT_ID) + assert "Server returned status 400." in str(error)