From ca8d80fbe737ca265f12b241705bdaa20602c06a Mon Sep 17 00:00:00 2001 From: Leila Mansouri Date: Fri, 12 Jun 2026 15:24:25 +0200 Subject: [PATCH 1/3] removed apps from list --- cloudos_cli/interactive_session/cli.py | 8 ++++++-- .../interactive_session/interactive_session.py | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/cloudos_cli/interactive_session/cli.py b/cloudos_cli/interactive_session/cli.py index a1c9b9b2..8d555075 100644 --- a/cloudos_cli/interactive_session/cli.py +++ b/cloudos_cli/interactive_session/cli.py @@ -153,7 +153,7 @@ def list_sessions(ctx, valid_columns = {'id', 'name', 'status', 'type', 'instance', 'cost', 'owner', 'project', 'created_at', 'runtime', 'saved_at', 'resources', 'backend', 'version', - 'spot', 'cost_limit', 'time_left'} + 'spot', 'cost_limit', 'time_left', 'container_image'} selected_columns = table_columns if selected_columns: @@ -196,6 +196,10 @@ def list_sessions(ctx, cl, workspace_id, page_num, limit, filter_status, filter_only_mine, archived, verify_ssl ) + # Filter out app sessions (awsCustomSession / azureCustomSession) — not supported via API key + APP_SESSION_TYPES = {'awsCustomSession', 'azureCustomSession'} + sessions = [s for s in sessions if s.get('interactiveSessionType') not in APP_SESSION_TYPES] + # Handle empty results if len(sessions) == 0: if filter_status: @@ -217,7 +221,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]') diff --git a/cloudos_cli/interactive_session/interactive_session.py b/cloudos_cli/interactive_session/interactive_session.py index 3de7042a..d6dc8c4a 100644 --- a/cloudos_cli/interactive_session/interactive_session.py +++ b/cloudos_cli/interactive_session/interactive_session.py @@ -83,12 +83,14 @@ 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) -def create_interactive_session_list_table(sessions, pagination_metadata=None, selected_columns=None, page_size=10, fetch_page_callback=None): +def create_interactive_session_list_table(sessions, pagination_metadata=None, selected_columns=None, page_size=10, fetch_page_callback=None, title='Interactive Sessions'): """Create a rich table displaying interactive sessions with interactive pagination. Parameters @@ -106,6 +108,8 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se fetch_page_callback : callable, optional Callback function to fetch a specific page of results. Should accept page number (1-indexed) and return dict with 'sessions' and 'pagination_metadata' keys. + title : str, optional + Title for the table. Default='Interactive Sessions'. """ console = Console() # Define all available columns with their configuration @@ -201,6 +205,13 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se 'max_width': 15, 'accessor': 'interactiveSessionType' }, + 'container_image': { + 'header': 'Container Image', + 'style': 'cyan', + 'overflow': 'ellipsis', + 'max_width': 30, + 'accessor': 'containerImage.name' + }, 'version': { 'header': 'Version', 'style': 'white', @@ -293,7 +304,7 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se # Clear console first console.clear() # Create table - table = Table(title='Interactive Sessions') + table = Table(title=title) # Add columns to table for col_name in columns_to_show: if col_name not in all_columns: From 580a4cec71c3d29d8b53f79064a194fe2cd195f6 Mon Sep 17 00:00:00 2001 From: Leila Mansouri Date: Fri, 12 Jun 2026 15:54:54 +0200 Subject: [PATCH 2/3] code cleanup --- CHANGELOG.md | 6 +++ cloudos_cli/_version.py | 2 +- cloudos_cli/interactive_session/cli.py | 11 +++-- .../interactive_session.py | 16 ++----- .../test_list_sessions.py | 46 +++++++++++++++++++ 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f2df07..e5175d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/cloudos_cli/_version.py b/cloudos_cli/_version.py index 1271f796..ed6f86f9 100644 --- a/cloudos_cli/_version.py +++ b/cloudos_cli/_version.py @@ -1 +1 @@ -__version__ = '2.91.0' +__version__ = '2.92.1' diff --git a/cloudos_cli/interactive_session/cli.py b/cloudos_cli/interactive_session/cli.py index 8d555075..d363b832 100644 --- a/cloudos_cli/interactive_session/cli.py +++ b/cloudos_cli/interactive_session/cli.py @@ -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 @@ -151,9 +152,9 @@ 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', 'container_image'} + 'spot', 'cost_limit', 'time_left'} selected_columns = table_columns if selected_columns: @@ -197,8 +198,8 @@ def list_sessions(ctx, ) # Filter out app sessions (awsCustomSession / azureCustomSession) — not supported via API key - APP_SESSION_TYPES = {'awsCustomSession', 'azureCustomSession'} - sessions = [s for s in sessions if s.get('interactiveSessionType') not in APP_SESSION_TYPES] + # 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: diff --git a/cloudos_cli/interactive_session/interactive_session.py b/cloudos_cli/interactive_session/interactive_session.py index d6dc8c4a..a9bfc78a 100644 --- a/cloudos_cli/interactive_session/interactive_session.py +++ b/cloudos_cli/interactive_session/interactive_session.py @@ -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. @@ -90,7 +93,7 @@ def _map_session_type_to_friendly_name(session_type): return type_mapping.get(session_type, session_type) -def create_interactive_session_list_table(sessions, pagination_metadata=None, selected_columns=None, page_size=10, fetch_page_callback=None, title='Interactive Sessions'): +def create_interactive_session_list_table(sessions, pagination_metadata=None, selected_columns=None, page_size=10, fetch_page_callback=None): """Create a rich table displaying interactive sessions with interactive pagination. Parameters @@ -108,8 +111,6 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se fetch_page_callback : callable, optional Callback function to fetch a specific page of results. Should accept page number (1-indexed) and return dict with 'sessions' and 'pagination_metadata' keys. - title : str, optional - Title for the table. Default='Interactive Sessions'. """ console = Console() # Define all available columns with their configuration @@ -205,13 +206,6 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se 'max_width': 15, 'accessor': 'interactiveSessionType' }, - 'container_image': { - 'header': 'Container Image', - 'style': 'cyan', - 'overflow': 'ellipsis', - 'max_width': 30, - 'accessor': 'containerImage.name' - }, 'version': { 'header': 'Version', 'style': 'white', @@ -304,7 +298,7 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se # Clear console first console.clear() # Create table - table = Table(title=title) + table = Table(title='Interactive Sessions') # Add columns to table for col_name in columns_to_show: if col_name not in all_columns: diff --git a/tests/test_interactive_session/test_list_sessions.py b/tests/test_interactive_session/test_list_sessions.py index 0fadf377..346485f4 100644 --- a/tests/test_interactive_session/test_list_sessions.py +++ b/tests/test_interactive_session/test_list_sessions.py @@ -245,6 +245,52 @@ 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.""" + + def test_app_sessions_are_filtered_out(self): + """awsCustomSession and azureCustomSession must not appear in table output.""" + from io import StringIO + from rich.console import Console + from cloudos_cli.interactive_session.interactive_session import ( + create_interactive_session_list_table, + _APP_SESSION_TYPES, + ) + + sessions = [ + {'_id': 'aaa', 'name': 'Jupyter', 'status': 'running', + 'interactiveSessionType': 'awsJupyterNotebook'}, + {'_id': 'bbb', 'name': 'MyApp', 'status': 'running', + 'interactiveSessionType': 'awsCustomSession'}, + {'_id': 'ccc', 'name': 'AzureApp', 'status': 'running', + 'interactiveSessionType': 'azureCustomSession'}, + ] + + filtered = [s for s in sessions if s.get('interactiveSessionType') not in _APP_SESSION_TYPES] + assert len(filtered) == 1 + assert filtered[0]['_id'] == 'aaa' + + 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 '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']) From f83be37d8b5bb1d976c65c62c5aba7c3616264cf Mon Sep 17 00:00:00 2001 From: Leila Mansouri Date: Fri, 12 Jun 2026 16:16:43 +0200 Subject: [PATCH 3/3] copilot 1 --- cloudos_cli/interactive_session/cli.py | 20 +++-- .../interactive_session.py | 2 +- .../test_list_sessions.py | 73 ++++++++++++------- 3 files changed, 63 insertions(+), 32 deletions(-) diff --git a/cloudos_cli/interactive_session/cli.py b/cloudos_cli/interactive_session/cli.py index d363b832..963aed1e 100644 --- a/cloudos_cli/interactive_session/cli.py +++ b/cloudos_cli/interactive_session/cli.py @@ -32,7 +32,7 @@ poll_session_termination, build_resume_payload, fetch_interactive_session_page, - _APP_SESSION_TYPES + 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 @@ -192,14 +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] + sessions = [s for s in sessions if s.get('interactiveSessionType') not in APP_SESSION_TYPES] # Handle empty results if len(sessions) == 0: diff --git a/cloudos_cli/interactive_session/interactive_session.py b/cloudos_cli/interactive_session/interactive_session.py index a9bfc78a..53c2c8c9 100644 --- a/cloudos_cli/interactive_session/interactive_session.py +++ b/cloudos_cli/interactive_session/interactive_session.py @@ -57,7 +57,7 @@ def validate_instance_type(instance_type, execution_platform='aws'): return True, None -_APP_SESSION_TYPES = frozenset({'awsCustomSession', 'azureCustomSession'}) +APP_SESSION_TYPES = frozenset({'awsCustomSession', 'azureCustomSession'}) def _map_session_type_to_friendly_name(session_type): diff --git a/tests/test_interactive_session/test_list_sessions.py b/tests/test_interactive_session/test_list_sessions.py index 346485f4..3252e3c1 100644 --- a/tests/test_interactive_session/test_list_sessions.py +++ b/tests/test_interactive_session/test_list_sessions.py @@ -248,38 +248,61 @@ def test_get_interactive_session_list_validation(self): class TestAppSessionFilter: """Tests for client-side app-session filtering.""" - def test_app_sessions_are_filtered_out(self): - """awsCustomSession and azureCustomSession must not appear in table output.""" - from io import StringIO - from rich.console import Console - from cloudos_cli.interactive_session.interactive_session import ( - create_interactive_session_list_table, - _APP_SESSION_TYPES, - ) + @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} + } - sessions = [ - {'_id': 'aaa', 'name': 'Jupyter', 'status': 'running', - 'interactiveSessionType': 'awsJupyterNotebook'}, - {'_id': 'bbb', 'name': 'MyApp', 'status': 'running', - 'interactiveSessionType': 'awsCustomSession'}, - {'_id': 'ccc', 'name': 'AzureApp', 'status': 'running', - 'interactiveSessionType': 'azureCustomSession'}, - ] + 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) - filtered = [s for s in sessions if s.get('interactiveSessionType') not in _APP_SESSION_TYPES] - assert len(filtered) == 1 - assert filtered[0]['_id'] == 'aaa' + 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 + """APP_SESSION_TYPES must contain exactly the two app session type strings.""" + from cloudos_cli.interactive_session.interactive_session import APP_SESSION_TYPES - assert 'awsCustomSession' in _APP_SESSION_TYPES - assert 'azureCustomSession' in _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 + from cloudos_cli.interactive_session.interactive_session import APP_SESSION_TYPES regular_types = [ 'awsJupyterNotebook', 'azureJupyterNotebook', @@ -288,7 +311,7 @@ def test_regular_sessions_not_filtered(self): 'awsSpark', 'awsWindowsSession', ] for t in regular_types: - assert t not in _APP_SESSION_TYPES, f"{t} should not be filtered out" + assert t not in APP_SESSION_TYPES, f"{t} should not be filtered out" if __name__ == '__main__':