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.92.1 (2026-06-12)

### Fix:

- Removes app sessions from the listing of interactive sessions

## 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.92.1'
27 changes: 20 additions & 7 deletions cloudos_cli/interactive_session/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
format_stop_success_output,
poll_session_termination,
build_resume_payload,
fetch_interactive_session_page
fetch_interactive_session_page,
APP_SESSION_TYPES
)
from cloudos_cli.configure.configure import with_profile_config, CLOUDOS_URL
from cloudos_cli.utils.cli_helpers import pass_debug_to_subcommands
Expand Down Expand Up @@ -151,7 +152,7 @@ def list_sessions(ctx,
raise ValueError('Please use a positive integer (>= 1) for the --page parameter')
# Validate table columns if specified

valid_columns = {'id', 'name', 'status', 'type', 'instance', 'cost', 'owner', 'project',
valid_columns = {'id', 'name', 'status', 'type', 'instance', 'cost', 'owner', 'project',
'created_at', 'runtime', 'saved_at', 'resources', 'backend', 'version',
'spot', 'cost_limit', 'time_left'}
selected_columns = table_columns
Expand Down Expand Up @@ -191,10 +192,22 @@ def list_sessions(ctx,
sessions = result.get('sessions', [])
pagination_metadata = result.get('pagination_metadata', None)

# Create callback function for fetching additional pages
fetch_page = lambda page_num: fetch_interactive_session_page(
cl, workspace_id, page_num, limit, filter_status, filter_only_mine, archived, verify_ssl
)
# Create callback function for fetching additional pages.
# App sessions must be filtered here too so that navigating to
# next/prev pages via interactive pagination never re-introduces them.
def fetch_page(page_num):
page_result = fetch_interactive_session_page(
cl, workspace_id, page_num, limit, filter_status, filter_only_mine, archived, verify_ssl
)
page_result['sessions'] = [
s for s in page_result.get('sessions', [])
if s.get('interactiveSessionType') not in APP_SESSION_TYPES
]
return page_result

# Filter out app sessions (awsCustomSession / azureCustomSession) — not supported via API key
# Client-side filter; the API has no type-exclusion parameter
sessions = [s for s in sessions if s.get('interactiveSessionType') not in APP_SESSION_TYPES]

# Handle empty results
if len(sessions) == 0:
Expand All @@ -217,7 +230,7 @@ def list_sessions(ctx,
with open(outfile, 'w') as o:
o.write(json.dumps(sessions, indent=2))
print(f'\tInteractive session list collected with a total of {len(sessions)} sessions on this page.')
print(f'\tInteractive session list saved to {outfile}')
print(f'\tInteractive session list saved to {outfile}')
else:
raise ValueError('Unrecognised output format. Please use one of [stdout|csv|json]')

Expand Down
7 changes: 6 additions & 1 deletion cloudos_cli/interactive_session/interactive_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def validate_instance_type(instance_type, execution_platform='aws'):
return True, None


APP_SESSION_TYPES = frozenset({'awsCustomSession', 'azureCustomSession'})


def _map_session_type_to_friendly_name(session_type):
"""Map internal session type names to user-friendly display names.

Expand All @@ -83,7 +86,9 @@ def _map_session_type_to_friendly_name(session_type):
'azureSpark': 'Spark',
'awsRStudio': 'RStudio', # Handle both capitalizations
'azureRStudio': 'RStudio',
'awsWindowsSession': 'Windows'
'awsWindowsSession': 'Windows',
'awsCustomSession': 'App',
'azureCustomSession': 'App'
}
return type_mapping.get(session_type, session_type)

Expand Down
69 changes: 69 additions & 0 deletions tests/test_interactive_session/test_list_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,75 @@ def test_get_interactive_session_list_validation(self):
cl.get_interactive_session_list('test_team', limit=150)


class TestAppSessionFilter:
"""Tests for client-side app-session filtering."""

@patch('cloudos_cli.interactive_session.cli.Cloudos')
@patch('cloudos_cli.configure.configure.ConfigurationProfile.load_profile_and_validate_data')
def test_app_sessions_are_filtered_out(self, mock_config, mock_cloudos):
"""list_sessions CLI must exclude awsCustomSession and azureCustomSession from its output."""
runner = CliRunner()
mock_config.return_value = {}
mock_cloudos_instance = mock.MagicMock()
mock_cloudos.return_value = mock_cloudos_instance
mock_cloudos_instance.get_interactive_session_list.return_value = {
'sessions': [
{'_id': 'aaa', 'name': 'Jupyter', 'status': 'running',
'interactiveSessionType': 'awsJupyterNotebook',
'resources': {'instanceType': 'c5.xlarge'}, 'totalCostInUsd': 0.0},
{'_id': 'bbb', 'name': 'MyApp', 'status': 'running',
'interactiveSessionType': 'awsCustomSession',
'resources': {'instanceType': 'c5.xlarge'}, 'totalCostInUsd': 0.0},
{'_id': 'ccc', 'name': 'AzureApp', 'status': 'running',
'interactiveSessionType': 'azureCustomSession',
'resources': {'instanceType': 'c5.xlarge'}, 'totalCostInUsd': 0.0},
],
'pagination_metadata': {'count': 3, 'page': 1, 'limit': 10, 'totalPages': 1}
}

with runner.isolated_filesystem():
result = runner.invoke(run_cloudos_cli, [
'interactive-session', 'list',
'--apikey', 'test_key',
'--cloudos-url', 'http://test.com',
'--workspace-id', 'test_team',
'--output-format', 'json',
])

assert result.exit_code == 0, result.output
with open('interactive_sessions_list.json') as f:
saved_sessions = json.load(f)

saved_ids = {s['_id'] for s in saved_sessions}
assert 'aaa' in saved_ids, "Regular session must be kept"
assert 'bbb' not in saved_ids, "awsCustomSession must be filtered out"
assert 'ccc' not in saved_ids, "azureCustomSession must be filtered out"
assert len(saved_sessions) == 1

def test_app_session_types_constant_contains_expected_values(self):
"""APP_SESSION_TYPES must contain exactly the two app session type strings."""
from cloudos_cli.interactive_session.interactive_session import APP_SESSION_TYPES

assert len(APP_SESSION_TYPES) == 2, (
f"APP_SESSION_TYPES must have exactly 2 entries, got {len(APP_SESSION_TYPES)}: {APP_SESSION_TYPES}"
)
assert 'awsCustomSession' in APP_SESSION_TYPES
assert 'azureCustomSession' in APP_SESSION_TYPES

def test_regular_sessions_not_filtered(self):
"""Regular session types must pass through the filter unchanged."""
from cloudos_cli.interactive_session.interactive_session import APP_SESSION_TYPES

regular_types = [
'awsJupyterNotebook', 'azureJupyterNotebook',
'awsVSCode', 'azureVSCode',
'awsRstudio', 'azureRstudio',
'awsSpark', 'awsWindowsSession',
]
for t in regular_types:
assert t not in APP_SESSION_TYPES, f"{t} should not be filtered out"


if __name__ == '__main__':
pytest.main([__file__, '-v'])

Loading