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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## lifebit-ai/cloudos-cli: changelog

## v2.91.1 (2026-06-19)

### Feat:

- Adds `cloudos analytics team-summary` command for `GET /api/v1/analytics/team/summary`
- Returns aggregated team usage analytics (compute hours, job counts, spend) over a date range
- Supports optional query parameters: `--start-date`, `--end-date`, `--granularity`

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

### Feat:
Expand Down
2 changes: 2 additions & 0 deletions cloudos_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from cloudos_cli.configure.cli import configure
from cloudos_cli.link.cli import link
from cloudos_cli.interactive_session.cli import interactive_session
from cloudos_cli.analytics.cli import analytics


# Install the custom exception handler
Expand Down Expand Up @@ -65,6 +66,7 @@ def run_cloudos_cli(ctx):
run_cloudos_cli.add_command(configure)
run_cloudos_cli.add_command(link)
run_cloudos_cli.add_command(interactive_session)
run_cloudos_cli.add_command(analytics)

if __name__ == '__main__':
run_cloudos_cli()
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'
8 changes: 8 additions & 0 deletions cloudos_cli/analytics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
Functions and classes related to analytics.
"""

from .analytics import Analytics


__all__ = ['Analytics']
72 changes: 72 additions & 0 deletions cloudos_cli/analytics/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
This is the main class for analytics operations.
"""

import requests
import json
from dataclasses import dataclass
from typing import Union, Optional
from cloudos_cli.clos import Cloudos
from cloudos_cli.utils.errors import BadRequestException


@dataclass
class Analytics(Cloudos):
"""Class to store and operate analytics data.

Parameters
----------
cloudos_url : string
The Lifebit Platform service url.
apikey : string
Your Lifebit Platform API key.
cromwell_token : string
Cromwell server token.
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.
"""
verify: Union[bool, str] = True

def get_team_summary(self,
team_id: str,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
granularity: Optional[str] = None):
"""Get aggregated team usage analytics over a date range.

Parameters
----------
team_id : str
The Lifebit Platform team (workspace) id.
start_date : str, optional
The start date for the analytics range (e.g. '2024-01-01').
end_date : str, optional
The end date for the analytics range (e.g. '2024-12-31').
granularity : str, optional
The time granularity for the analytics (e.g. 'daily', 'weekly', 'monthly').

Returns
-------
r : dict
A dict containing aggregated team usage analytics (compute hours,
job counts, spend) over the requested date range.
"""
headers = {"apikey": self.apikey}
params = {"teamId": team_id}
if start_date is not None:
params["startDate"] = start_date
if end_date is not None:
params["endDate"] = end_date
if granularity is not None:
params["granularity"] = granularity
r = requests.get(
"{}/api/v1/analytics/team/summary".format(self.cloudos_url),
headers=headers,
params=params,
verify=self.verify
)
if r.status_code >= 400:
raise BadRequestException(r)
return json.loads(r.content)
90 changes: 90 additions & 0 deletions cloudos_cli/analytics/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""CLI commands for Lifebit Platform analytics."""

import rich_click as click
import json
from cloudos_cli.analytics.analytics import Analytics
from cloudos_cli.utils.resources import ssl_selector
from cloudos_cli.configure.configure import with_profile_config, CLOUDOS_URL
from cloudos_cli.utils.cli_helpers import pass_debug_to_subcommands


# Create the analytics group
@click.group(cls=pass_debug_to_subcommands())
def analytics():
"""Lifebit Platform analytics functionality."""
print(analytics.__doc__ + '\n')


@analytics.command('team-summary')
@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('--team-id',
help='The Lifebit Platform team (workspace) id.',
required=True)
@click.option('--start-date',
help='The start date for the analytics range (e.g. 2024-01-01).',
required=False,
default=None)
@click.option('--end-date',
help='The end date for the analytics range (e.g. 2024-12-31).',
required=False,
default=None)
@click.option('--granularity',
help='The time granularity for the analytics (e.g. daily, weekly, monthly).',
required=False,
default=None)
@click.option('--output-format',
help=('The desired display for the output, either directly in standard output or saved as file. '
'Default=stdout.'),
type=click.Choice(['stdout', 'json'], case_sensitive=False),
default='stdout')
@click.option('--output-basename',
help=('Output file base name to save team summary. '
'Default=team_summary'),
default='team_summary',
required=False)
@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', 'team_id'])
def team_summary(ctx,
apikey,
cloudos_url,
team_id,
start_date,
end_date,
granularity,
output_format,
output_basename,
disable_ssl_verification,
ssl_cert,
profile):
"""Retrieve aggregated team usage analytics (compute hours, job counts, spend) over a date range."""
verify_ssl = ssl_selector(disable_ssl_verification, ssl_cert)
print('Executing team-summary...')
a = Analytics(cloudos_url, apikey, None, verify=verify_ssl)
result = a.get_team_summary(
team_id=team_id,
start_date=start_date,
end_date=end_date,
granularity=granularity
)
if output_format == 'stdout':
print(json.dumps(result, indent=2))
elif output_format == 'json':
outfile = output_basename + '.json'
with open(outfile, 'w') as o:
o.write(json.dumps(result))
print(f'\tTeam summary saved to {outfile}')
15 changes: 15 additions & 0 deletions supported-endpoints.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"cli_version": "2.91.1",
"endpoints": [
{
"id": "analytics-team-summary",
"method": "GET",
"path": "/api/v1/analytics/team/summary",
"cli_command": "cloudos analytics team-summary",
"cli_source_file": "cloudos_cli/analytics/analytics.py",
"query_params": ["teamId", "startDate", "endDate", "granularity"],
"auth": [],
"summary": "Returns aggregated team usage analytics (compute hours, job counts, spend) over a date range."
}
]
}
Empty file.
106 changes: 106 additions & 0 deletions tests/test_analytics/test_cli_analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Test the CLI analytics team-summary command functionality."""

import pytest
import json
import requests_mock
from click.testing import CliRunner
from cloudos_cli.__main__ import run_cloudos_cli

# Test data
APIKEY = 'test_api_key_12345'
CLOUDOS_URL = 'https://cloudos.lifebit.ai'
TEAM_ID = 'test_team_id_123'

# Load test analytics data
with open("tests/test_data/analytics/team_summary.json") as f:
TEAM_SUMMARY_JSON_STR = f.read()
TEAM_SUMMARY_JSON_DICT = json.loads(TEAM_SUMMARY_JSON_STR)


def test_analytics_group_exists():
"""Test that the analytics group exists in the CLI."""
runner = CliRunner()
result = runner.invoke(run_cloudos_cli, ['analytics', '--help'])
assert result.exit_code == 0
assert 'analytics' in result.output.lower() or 'Lifebit Platform' in result.output


def test_analytics_team_summary_help():
"""Test that the analytics team-summary command help works."""
runner = CliRunner()
result = runner.invoke(run_cloudos_cli, ['analytics', 'team-summary', '--help'])
assert result.exit_code == 0
assert '--team-id' in result.output
assert '--apikey' in result.output
assert '--start-date' in result.output
assert '--end-date' in result.output
assert '--granularity' in result.output
assert '--output-format' in result.output


def test_analytics_team_summary_stdout():
"""Test analytics team-summary with stdout output format."""
runner = CliRunner()

with requests_mock.Mocker() as m:
m.get(
f"{CLOUDOS_URL}/api/v1/analytics/team/summary",
text=TEAM_SUMMARY_JSON_STR,
status_code=200
)
result = runner.invoke(run_cloudos_cli, [
'analytics', 'team-summary',
'--apikey', APIKEY,
'--cloudos-url', CLOUDOS_URL,
'--team-id', TEAM_ID,
])
assert result.exit_code == 0
assert 'computeHours' in result.output or 'jobCount' in result.output or 'spend' in result.output


def test_analytics_team_summary_with_optional_params():
"""Test analytics team-summary with all optional parameters."""
runner = CliRunner()

with requests_mock.Mocker() as m:
m.get(
f"{CLOUDOS_URL}/api/v1/analytics/team/summary",
text=TEAM_SUMMARY_JSON_STR,
status_code=200
)
result = runner.invoke(run_cloudos_cli, [
'analytics', 'team-summary',
'--apikey', APIKEY,
'--cloudos-url', CLOUDOS_URL,
'--team-id', TEAM_ID,
'--start-date', '2024-01-01',
'--end-date', '2024-12-31',
'--granularity', 'monthly',
])
assert result.exit_code == 0
# Verify optional params were passed in the request
assert len(m.request_history) == 1
request_url = m.request_history[0].url
assert 'startDate=2024-01-01' in request_url
assert 'endDate=2024-12-31' in request_url
assert 'granularity=monthly' in request_url


def test_analytics_team_summary_api_error():
"""Test analytics team-summary raises an error on API failure."""
runner = CliRunner()
error_body = json.dumps({"statusCode": 401, "message": "Unauthorized"})

with requests_mock.Mocker() as m:
m.get(
f"{CLOUDOS_URL}/api/v1/analytics/team/summary",
text=error_body,
status_code=401
)
result = runner.invoke(run_cloudos_cli, [
'analytics', 'team-summary',
'--apikey', 'bad_key',
'--cloudos-url', CLOUDOS_URL,
'--team-id', TEAM_ID,
])
assert result.exit_code != 0
Loading
Loading