cloudos-cli 2.93.0__tar.gz → 2.93.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/PKG-INFO +1 -1
  2. cloudos_cli-2.93.1/cloudos_cli/_version.py +1 -0
  3. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/interactive_session/cli.py +13 -6
  4. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/interactive_session/interactive_session.py +186 -105
  5. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli.egg-info/PKG-INFO +1 -1
  6. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_interactive_session/test_list_sessions.py +149 -0
  7. cloudos_cli-2.93.0/cloudos_cli/_version.py +0 -1
  8. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/LICENSE +0 -0
  9. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/README.md +0 -0
  10. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/__init__.py +0 -0
  11. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/__main__.py +0 -0
  12. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/bash/__init__.py +0 -0
  13. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/bash/cli.py +0 -0
  14. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/clos.py +0 -0
  15. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/configure/__init__.py +0 -0
  16. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/configure/cli.py +0 -0
  17. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/configure/configure.py +0 -0
  18. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/constants.py +0 -0
  19. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/cost/__init__.py +0 -0
  20. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/cost/cost.py +0 -0
  21. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/cromwell/__init__.py +0 -0
  22. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/cromwell/cli.py +0 -0
  23. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/datasets/__init__.py +0 -0
  24. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/datasets/cli.py +0 -0
  25. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/datasets/datasets.py +0 -0
  26. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/import_wf/__init__.py +0 -0
  27. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/import_wf/import_wf.py +0 -0
  28. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/interactive_session/__init__.py +0 -0
  29. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/interactive_session/link.py +0 -0
  30. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/jobs/__init__.py +0 -0
  31. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/jobs/cli.py +0 -0
  32. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/jobs/job.py +0 -0
  33. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/logging/__init__.py +0 -0
  34. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/logging/logger.py +0 -0
  35. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/procurement/__init__.py +0 -0
  36. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/procurement/cli.py +0 -0
  37. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/procurement/images.py +0 -0
  38. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/projects/__init__.py +0 -0
  39. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/projects/cli.py +0 -0
  40. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/queue/__init__.py +0 -0
  41. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/queue/cli.py +0 -0
  42. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/queue/queue.py +0 -0
  43. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/related_analyses/__init__.py +0 -0
  44. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/related_analyses/related_analyses.py +0 -0
  45. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/__init__.py +0 -0
  46. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/array_job.py +0 -0
  47. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/cli_helpers.py +0 -0
  48. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/cloud.py +0 -0
  49. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/details.py +0 -0
  50. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/errors.py +0 -0
  51. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/last_wf.py +0 -0
  52. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/nextflow_version.py +0 -0
  53. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/requests.py +0 -0
  54. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/utils/resources.py +0 -0
  55. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/workflows/__init__.py +0 -0
  56. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli/workflows/cli.py +0 -0
  57. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli.egg-info/SOURCES.txt +0 -0
  58. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli.egg-info/dependency_links.txt +0 -0
  59. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli.egg-info/entry_points.txt +0 -0
  60. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli.egg-info/requires.txt +0 -0
  61. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/cloudos_cli.egg-info/top_level.txt +0 -0
  62. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/setup.cfg +0 -0
  63. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/setup.py +0 -0
  64. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/__init__.py +0 -0
  65. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/functions_for_pytest.py +0 -0
  66. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_cli_project_create.py +0 -0
  67. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_cost/__init__.py +0 -0
  68. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_cost/test_job_cost.py +0 -0
  69. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_details.py +0 -0
  70. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_error_messages.py +0 -0
  71. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_interactive_session/__init__.py +0 -0
  72. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_interactive_session/test_create_session.py +0 -0
  73. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_interactive_session/test_link_error_handling.py +0 -0
  74. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_interactive_session/test_link_session.py +0 -0
  75. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_interactive_session/test_normalize_path.py +0 -0
  76. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_logging/__init__.py +0 -0
  77. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_logging/test_logger.py +0 -0
  78. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_nextflow_version.py +0 -0
  79. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_related_analyses/__init__.py +0 -0
  80. {cloudos_cli-2.93.0 → cloudos_cli-2.93.1}/tests/test_related_analyses/test_related_analyses.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudos_cli
3
- Version: 2.93.0
3
+ Version: 2.93.1
4
4
  Summary: Python package for interacting with the Lifebit Platform
5
5
  Home-page: https://github.com/lifebit-ai/cloudos-cli
6
6
  Author: David Piñeyro
@@ -0,0 +1 @@
1
+ __version__ = '2.93.1'
@@ -29,7 +29,9 @@ from cloudos_cli.interactive_session.interactive_session import (
29
29
  format_stop_success_output,
30
30
  poll_session_termination,
31
31
  build_resume_payload,
32
- fetch_interactive_session_page
32
+ fetch_interactive_session_page,
33
+ APP_SESSION_TYPES,
34
+ build_app_filtered_page_fetcher
33
35
  )
34
36
  from cloudos_cli.configure.configure import with_profile_config, CLOUDOS_URL
35
37
  from cloudos_cli.utils.cli_helpers import pass_debug_to_subcommands
@@ -223,11 +225,16 @@ def list_sessions(ctx,
223
225
  sessions = result.get('sessions', [])
224
226
  pagination_metadata = result.get('pagination_metadata', None)
225
227
 
226
- # Create callback function for fetching additional pages
227
- def fetch_page(page_num):
228
- return fetch_interactive_session_page(
229
- cl, workspace_id, page_num, limit, filter_status, filter_only_mine, archived, verify_ssl
230
- )
228
+ # Create callback function for fetching additional pages.
229
+ # App sessions must be filtered here too so that navigating to
230
+ # next/prev pages via interactive pagination never re-introduces them.
231
+ fetch_page = build_app_filtered_page_fetcher(
232
+ cl, workspace_id, limit, filter_status, filter_only_mine, archived, verify_ssl
233
+ )
234
+
235
+ # Filter out app sessions (awsCustomSession / azureCustomSession) — not supported via API key
236
+ # Client-side filter; the API has no type-exclusion parameter
237
+ sessions = [s for s in sessions if s.get('interactiveSessionType') not in APP_SESSION_TYPES]
231
238
 
232
239
  # Handle empty results
233
240
  if len(sessions) == 0:
@@ -57,6 +57,9 @@ def validate_instance_type(instance_type, execution_platform='aws'):
57
57
  return True, None
58
58
 
59
59
 
60
+ APP_SESSION_TYPES = frozenset({'awsCustomSession', 'azureCustomSession'})
61
+
62
+
60
63
  def _map_session_type_to_friendly_name(session_type):
61
64
  """Map internal session type names to user-friendly display names.
62
65
 
@@ -83,7 +86,9 @@ def _map_session_type_to_friendly_name(session_type):
83
86
  'azureSpark': 'Spark',
84
87
  'awsRStudio': 'RStudio', # Handle both capitalizations
85
88
  'azureRStudio': 'RStudio',
86
- 'awsWindowsSession': 'Windows'
89
+ 'awsWindowsSession': 'Windows',
90
+ 'awsCustomSession': 'App',
91
+ 'azureCustomSession': 'App'
87
92
  }
88
93
  return type_mapping.get(session_type, session_type)
89
94
 
@@ -251,50 +256,95 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se
251
256
  if len(sessions) == 0:
252
257
  console.print('[yellow]No interactive sessions found.[/yellow]')
253
258
  return
254
- # Prepare rows data
255
- rows = []
256
- for session in sessions:
257
- row_data = []
258
- for col_name in columns_to_show:
259
- if col_name not in all_columns:
260
- continue
261
- col_config = all_columns[col_name]
262
- accessor = col_config['accessor']
263
- # Extract value from session object
264
- value = _get_nested_value(session, accessor)
265
- # Format the value
266
- formatted_value = _format_session_field(col_name, value)
267
- row_data.append(formatted_value)
268
- rows.append(row_data)
269
-
270
- # Interactive pagination - use API pagination metadata if available
259
+ # ------------------------------------------------------------------
260
+ # Row-builder helper (keeps column-mapping in one place)
261
+ # ------------------------------------------------------------------
262
+ def _build_rows(sess_list):
263
+ result = []
264
+ for sess in sess_list:
265
+ row_data = []
266
+ for col_name in columns_to_show:
267
+ if col_name not in all_columns:
268
+ continue
269
+ col_config = all_columns[col_name]
270
+ value = _get_nested_value(sess, col_config['accessor'])
271
+ row_data.append(_format_session_field(col_name, value))
272
+ result.append(row_data)
273
+ return result
274
+
275
+ # ------------------------------------------------------------------
276
+ # Pagination state
277
+ # ------------------------------------------------------------------
271
278
  if pagination_metadata:
272
- # Server-side pagination
279
+ # Server-side pagination with fill-buffer.
280
+ # The server page may contain app-session types that are filtered
281
+ # client-side, leaving fewer than page_size rows. We keep a
282
+ # carry_forward buffer and silently fetch ahead so that every
283
+ # virtual page is exactly page_size rows (or fewer only on the
284
+ # very last page).
273
285
  current_api_page = pagination_metadata.get('page', 1)
274
- total_sessions = pagination_metadata.get('count', len(sessions))
275
- total_pages = pagination_metadata.get('totalPages', 1)
286
+ total_sessions = pagination_metadata.get('count', len(sessions))
287
+ total_pages = pagination_metadata.get('totalPages', 1)
288
+
289
+ carry_forward = list(sessions) # fetched but not yet shown
290
+ last_fetched_server_page = current_api_page
291
+ virtual_page_history = [] # for "prev" navigation
292
+
293
+ def _top_up():
294
+ """Silently fetch ahead until carry_forward >= page_size or exhausted."""
295
+ nonlocal last_fetched_server_page, total_pages
296
+ if not fetch_page_callback:
297
+ return
298
+ while (len(carry_forward) < page_size
299
+ and last_fetched_server_page < total_pages):
300
+ try:
301
+ extra = fetch_page_callback(last_fetched_server_page + 1)
302
+ carry_forward.extend(extra.get('sessions', []))
303
+ meta = extra.get('pagination_metadata', {})
304
+ total_pages = meta.get('totalPages', total_pages)
305
+ last_fetched_server_page += 1
306
+ except Exception:
307
+ break
308
+
309
+ # Fill the first virtual page
310
+ _top_up()
311
+ current_page_sessions = carry_forward[:page_size]
312
+ del carry_forward[:page_size]
313
+
276
314
  else:
277
- # Client-side pagination (fallback)
278
- current_api_page = 0
279
- total_sessions = len(sessions)
280
- total_pages = (len(sessions) + page_size - 1) // page_size if len(sessions) > 0 else 1
281
- show_error = None # Track error messages to display
315
+ # Client-side pagination (all sessions already in memory)
316
+ current_api_page = 0
317
+ total_sessions = len(sessions)
318
+ total_pages = (len(sessions) + page_size - 1) // page_size if sessions else 1
319
+ current_page_sessions = None # unused in client-side path
320
+ carry_forward = []
321
+ last_fetched_server_page = 0
322
+ virtual_page_history = []
323
+
324
+ virtual_page_num = 1
325
+ show_error = None # Track error messages to display
326
+
282
327
  while True:
283
- # For client-side pagination, start/end are indices into the local rows array
284
- # For server-side pagination, we use the API page directly
328
+ # ---- Build display rows for the current virtual page ----------------
329
+ if fetch_page_callback and pagination_metadata:
330
+ page_rows = _build_rows(current_page_sessions)
331
+ else:
332
+ all_rows = _build_rows(sessions)
333
+ start = current_api_page * page_size
334
+ page_rows = all_rows[start:start + page_size]
335
+
336
+ # ---- Determine navigation availability ------------------------------
285
337
  if fetch_page_callback and pagination_metadata:
286
- # Server-side pagination - sessions list contains current page data
287
- page_rows = rows[:] # All rows are from current page
338
+ has_next = bool(carry_forward) or last_fetched_server_page < total_pages
339
+ has_prev = bool(virtual_page_history)
288
340
  else:
289
- # Client-side pagination
290
- start = current_api_page * page_size
291
- end = start + page_size
292
- page_rows = rows[start:end]
293
- # Clear console first
341
+ has_next = (current_api_page + 1) * page_size < len(sessions)
342
+ has_prev = current_api_page > 0
343
+ show_nav = has_next or has_prev
344
+
345
+ # ---- Render table --------------------------------------------------
294
346
  console.clear()
295
- # Create table
296
347
  table = Table(title='Interactive Sessions')
297
- # Add columns to table
298
348
  for col_name in columns_to_show:
299
349
  if col_name not in all_columns:
300
350
  continue
@@ -304,100 +354,86 @@ def create_interactive_session_list_table(sessions, pagination_metadata=None, se
304
354
  style=col_config.get('style', 'white'),
305
355
  no_wrap=col_config.get('no_wrap', False)
306
356
  )
307
- # Add rows to table
308
357
  for row in page_rows:
309
358
  table.add_row(*row)
310
- # Print table
311
359
  console.print(table)
312
- # Display pagination info
313
- console.print(f"\n[cyan]Total sessions:[/cyan] {total_sessions}")
314
- if total_pages > 1:
315
- console.print(f"[cyan]Page:[/cyan] {current_api_page} of {total_pages}")
316
- console.print(f"[cyan]Sessions on this page:[/cyan] {len(page_rows)}")
317
360
 
318
- # Show error message if any
361
+ # ---- Pagination info ------------------------------------------------
362
+ # Sessions on this page is always the exact post-filter count.
363
+ # The virtual page number counts only pages with at least one session.
364
+ if show_nav:
365
+ page_display = virtual_page_num if (fetch_page_callback and pagination_metadata) else current_api_page + 1
366
+ console.print(f"\n[cyan]Page:[/cyan] {page_display}")
367
+ elif not pagination_metadata:
368
+ # Client-side, single page: total is exact
369
+ console.print(f"\n[cyan]Total sessions:[/cyan] {total_sessions}")
370
+ console.print(f"[cyan]Sessions on this page:[/cyan] {len(page_rows)}")
371
+ if fetch_page_callback and pagination_metadata:
372
+ console.print("[yellow]Note: total page count is not available from the CLI for interactive sessions. This feature will be ready soon.[/yellow]")
373
+
374
+ # ---- Error message --------------------------------------------------
319
375
  if show_error:
320
376
  console.print(show_error)
321
- show_error = None # Reset error after displaying
322
- # Show pagination controls
323
- if total_pages > 1:
324
- # Check if we're in an interactive environment
377
+ show_error = None
378
+
379
+ # ---- Navigation controls -------------------------------------------
380
+ if show_nav:
325
381
  if not sys.stdin.isatty():
326
- console.print("\n[yellow]Note: Pagination not available in non-interactive mode. Showing page 1 of {0}.[/yellow]".format(total_pages))
382
+ page_display = virtual_page_num if (fetch_page_callback and pagination_metadata) else current_api_page + 1
383
+ console.print(f"\n[yellow]Note: Pagination not available in non-interactive mode. Showing page {page_display}.[/yellow]")
327
384
  console.print("[yellow]Run in an interactive terminal to navigate through all pages.[/yellow]")
328
385
  break
329
386
  console.print(f"\n[bold cyan]n[/] = next, [bold cyan]p[/] = prev, [bold cyan]q[/] = quit")
330
- # Get user input for navigation
331
387
  try:
332
388
  choice = input(">>> ").strip().lower()
333
389
  except (EOFError, KeyboardInterrupt):
334
- # Handle non-interactive environments or user interrupt
335
390
  console.print("\n[yellow]Pagination interrupted.[/yellow]")
336
391
  break
392
+
337
393
  if choice in ("q", "quit"):
338
394
  break
395
+
339
396
  elif choice in ("n", "next"):
340
- if current_api_page < total_pages:
341
- # Try to fetch the next page
342
- if fetch_page_callback:
343
- try:
344
- next_page_data = fetch_page_callback(current_api_page + 1)
345
- sessions = next_page_data.get('sessions', [])
346
- pagination_metadata = next_page_data.get('pagination_metadata', {})
347
- current_api_page = pagination_metadata.get('page', current_api_page + 1)
348
- total_pages = pagination_metadata.get('totalPages', total_pages)
349
- # Rebuild rows for the new page
350
- rows = []
351
- for session in sessions:
352
- row_data = []
353
- for col_name in columns_to_show:
354
- if col_name not in all_columns:
355
- continue
356
- col_config = all_columns[col_name]
357
- accessor = col_config['accessor']
358
- value = _get_nested_value(session, accessor)
359
- formatted_value = _format_session_field(col_name, value)
360
- row_data.append(formatted_value)
361
- rows.append(row_data)
362
- except Exception as e:
363
- show_error = f"[red]Error fetching next page: {str(e)}[/red]"
397
+ if fetch_page_callback and pagination_metadata:
398
+ if carry_forward or last_fetched_server_page < total_pages:
399
+ virtual_page_history.append(current_page_sessions[:])
400
+ _top_up()
401
+ if carry_forward:
402
+ current_page_sessions = carry_forward[:page_size]
403
+ del carry_forward[:page_size]
404
+ virtual_page_num += 1
405
+ else:
406
+ virtual_page_history.pop()
407
+ show_error = "[yellow]No more sessions found.[/yellow]"
364
408
  else:
365
- current_api_page += 1
409
+ show_error = "[red]Already on the last page.[/red]"
366
410
  else:
367
- show_error = "[red]Invalid choice. Already on the last page.[/red]"
411
+ if has_next:
412
+ current_api_page += 1
413
+ else:
414
+ show_error = "[red]Already on the last page.[/red]"
415
+
368
416
  elif choice in ("p", "prev"):
369
- if current_api_page > 1:
370
- # Try to fetch the previous page
371
- if fetch_page_callback:
372
- try:
373
- prev_page_data = fetch_page_callback(current_api_page - 1)
374
- sessions = prev_page_data.get('sessions', [])
375
- pagination_metadata = prev_page_data.get('pagination_metadata', {})
376
- current_api_page = pagination_metadata.get('page', current_api_page - 1)
377
- total_pages = pagination_metadata.get('totalPages', total_pages)
378
- # Rebuild rows for the new page
379
- rows = []
380
- for session in sessions:
381
- row_data = []
382
- for col_name in columns_to_show:
383
- if col_name not in all_columns:
384
- continue
385
- col_config = all_columns[col_name]
386
- accessor = col_config['accessor']
387
- value = _get_nested_value(session, accessor)
388
- formatted_value = _format_session_field(col_name, value)
389
- row_data.append(formatted_value)
390
- rows.append(row_data)
391
- except Exception as e:
392
- show_error = f"[red]Error fetching previous page: {str(e)}[/red]"
417
+ if fetch_page_callback and pagination_metadata:
418
+ if virtual_page_history:
419
+ # Return current sessions to the front of carry_forward,
420
+ # then restore the previous virtual page from history.
421
+ carry_forward[:0] = current_page_sessions
422
+ current_page_sessions = virtual_page_history.pop()
423
+ virtual_page_num -= 1
393
424
  else:
394
- current_api_page -= 1
425
+ show_error = "[red]Already on the first page.[/red]"
395
426
  else:
396
- show_error = "[red]Invalid choice. Already on the first page.[/red]"
427
+ if has_prev:
428
+ current_api_page -= 1
429
+ else:
430
+ show_error = "[red]Already on the first page.[/red]"
431
+
397
432
  else:
398
433
  show_error = "[red]Invalid choice. Please enter 'n' (next), 'p' (prev), or 'q' (quit).[/red]"
434
+
399
435
  else:
400
- # Only one page, no need for input, just exit
436
+ # Single page or no sessions left nothing to navigate
401
437
  break
402
438
 
403
439
 
@@ -1851,3 +1887,48 @@ def fetch_interactive_session_page(cl, workspace_id, page_num, limit, filter_sta
1851
1887
  include_archived=archived,
1852
1888
  verify=verify_ssl
1853
1889
  )
1890
+
1891
+
1892
+ def build_app_filtered_page_fetcher(cl, workspace_id, limit, filter_status, filter_only_mine, archived, verify_ssl):
1893
+ """Return a page-fetch callback that strips app-type sessions from each page.
1894
+
1895
+ The returned callable is intended for use as the ``fetch_page_callback``
1896
+ argument of :func:`create_interactive_session_list_table`. It fetches a
1897
+ page via :func:`fetch_interactive_session_page` and removes any session
1898
+ whose ``interactiveSessionType`` belongs to :data:`APP_SESSION_TYPES`
1899
+ before returning, ensuring app sessions never appear in the interactive
1900
+ table even when the user navigates to subsequent pages.
1901
+
1902
+ Parameters
1903
+ ----------
1904
+ cl : Cloudos
1905
+ API client instance.
1906
+ workspace_id : str
1907
+ Team/workspace identifier.
1908
+ limit : int
1909
+ Page size.
1910
+ filter_status : tuple or None
1911
+ Status values to pass to the API filter.
1912
+ filter_only_mine : bool
1913
+ Restrict results to the authenticated user's sessions.
1914
+ archived : bool
1915
+ Include archived sessions.
1916
+ verify_ssl : bool or str
1917
+ SSL verification setting.
1918
+
1919
+ Returns
1920
+ -------
1921
+ callable
1922
+ A function ``fetch_page(page_num: int) -> dict`` that returns the API
1923
+ response dict with app sessions already removed from ``"sessions"``.
1924
+ """
1925
+ def fetch_page(page_num):
1926
+ page_result = fetch_interactive_session_page(
1927
+ cl, workspace_id, page_num, limit, filter_status, filter_only_mine, archived, verify_ssl
1928
+ )
1929
+ page_result['sessions'] = [
1930
+ s for s in page_result.get('sessions', [])
1931
+ if s.get('interactiveSessionType') not in APP_SESSION_TYPES
1932
+ ]
1933
+ return page_result
1934
+ return fetch_page
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudos_cli
3
- Version: 2.93.0
3
+ Version: 2.93.1
4
4
  Summary: Python package for interacting with the Lifebit Platform
5
5
  Home-page: https://github.com/lifebit-ai/cloudos-cli
6
6
  Author: David Piñeyro
@@ -245,6 +245,155 @@ class TestInteractiveSessionAPIMethod:
245
245
  cl.get_interactive_session_list('test_team', limit=150)
246
246
 
247
247
 
248
+ class TestAppSessionFilter:
249
+ """Tests for client-side app-session filtering."""
250
+
251
+ @patch('cloudos_cli.interactive_session.cli.Cloudos')
252
+ @patch('cloudos_cli.configure.configure.ConfigurationProfile.load_profile_and_validate_data')
253
+ def test_app_sessions_are_filtered_out(self, mock_config, mock_cloudos):
254
+ """list_sessions CLI must exclude awsCustomSession and azureCustomSession from its output."""
255
+ runner = CliRunner()
256
+ mock_config.return_value = {}
257
+ mock_cloudos_instance = mock.MagicMock()
258
+ mock_cloudos.return_value = mock_cloudos_instance
259
+ mock_cloudos_instance.get_interactive_session_list.return_value = {
260
+ 'sessions': [
261
+ {'_id': 'aaa', 'name': 'Jupyter', 'status': 'running',
262
+ 'interactiveSessionType': 'awsJupyterNotebook',
263
+ 'resources': {'instanceType': 'c5.xlarge'}, 'totalCostInUsd': 0.0},
264
+ {'_id': 'bbb', 'name': 'MyApp', 'status': 'running',
265
+ 'interactiveSessionType': 'awsCustomSession',
266
+ 'resources': {'instanceType': 'c5.xlarge'}, 'totalCostInUsd': 0.0},
267
+ {'_id': 'ccc', 'name': 'AzureApp', 'status': 'running',
268
+ 'interactiveSessionType': 'azureCustomSession',
269
+ 'resources': {'instanceType': 'c5.xlarge'}, 'totalCostInUsd': 0.0},
270
+ ],
271
+ 'pagination_metadata': {'count': 3, 'page': 1, 'limit': 10, 'totalPages': 1}
272
+ }
273
+
274
+ with runner.isolated_filesystem():
275
+ result = runner.invoke(run_cloudos_cli, [
276
+ 'interactive-session', 'list',
277
+ '--apikey', 'test_key',
278
+ '--cloudos-url', 'http://test.com',
279
+ '--workspace-id', 'test_team',
280
+ '--output-format', 'json',
281
+ ])
282
+
283
+ assert result.exit_code == 0, result.output
284
+ with open('interactive_sessions_list.json') as f:
285
+ saved_sessions = json.load(f)
286
+
287
+ saved_ids = {s['_id'] for s in saved_sessions}
288
+ assert 'aaa' in saved_ids, "Regular session must be kept"
289
+ assert 'bbb' not in saved_ids, "awsCustomSession must be filtered out"
290
+ assert 'ccc' not in saved_ids, "azureCustomSession must be filtered out"
291
+ assert len(saved_sessions) == 1
292
+
293
+ def test_app_session_types_constant_contains_expected_values(self):
294
+ """APP_SESSION_TYPES must contain exactly the two app session type strings."""
295
+ from cloudos_cli.interactive_session.interactive_session import APP_SESSION_TYPES
296
+
297
+ assert len(APP_SESSION_TYPES) == 2, (
298
+ f"APP_SESSION_TYPES must have exactly 2 entries, got {len(APP_SESSION_TYPES)}: {APP_SESSION_TYPES}"
299
+ )
300
+ assert 'awsCustomSession' in APP_SESSION_TYPES
301
+ assert 'azureCustomSession' in APP_SESSION_TYPES
302
+
303
+ def test_regular_sessions_not_filtered(self):
304
+ """Regular session types must pass through the filter unchanged."""
305
+ from cloudos_cli.interactive_session.interactive_session import APP_SESSION_TYPES
306
+
307
+ regular_types = [
308
+ 'awsJupyterNotebook', 'azureJupyterNotebook',
309
+ 'awsVSCode', 'azureVSCode',
310
+ 'awsRstudio', 'azureRstudio',
311
+ 'awsSpark', 'awsWindowsSession',
312
+ ]
313
+ for t in regular_types:
314
+ assert t not in APP_SESSION_TYPES, f"{t} should not be filtered out"
315
+
316
+
317
+ class TestBuildAppFilteredPageFetcher:
318
+ """Tests for build_app_filtered_page_fetcher, the factory used as fetch_page_callback."""
319
+
320
+ @patch('cloudos_cli.interactive_session.interactive_session.fetch_interactive_session_page')
321
+ def test_filters_app_sessions_from_page(self, mock_fetch):
322
+ """App sessions must be removed from the returned sessions list."""
323
+ from cloudos_cli.interactive_session.interactive_session import build_app_filtered_page_fetcher
324
+ from unittest.mock import MagicMock
325
+
326
+ mock_fetch.return_value = {
327
+ 'sessions': [
328
+ {'_id': 'aaa', 'interactiveSessionType': 'awsJupyterNotebook'},
329
+ {'_id': 'bbb', 'interactiveSessionType': 'awsCustomSession'},
330
+ {'_id': 'ccc', 'interactiveSessionType': 'azureCustomSession'},
331
+ ],
332
+ 'pagination_metadata': {'count': 3, 'page': 2, 'totalPages': 3}
333
+ }
334
+
335
+ cl = MagicMock()
336
+ fetcher = build_app_filtered_page_fetcher(cl, 'ws1', 10, None, False, False, True)
337
+ result = fetcher(2)
338
+
339
+ mock_fetch.assert_called_once_with(cl, 'ws1', 2, 10, None, False, False, True)
340
+ returned_ids = {s['_id'] for s in result['sessions']}
341
+ assert 'aaa' in returned_ids, "Regular session must be kept"
342
+ assert 'bbb' not in returned_ids, "awsCustomSession must be filtered"
343
+ assert 'ccc' not in returned_ids, "azureCustomSession must be filtered"
344
+
345
+ @patch('cloudos_cli.interactive_session.interactive_session.fetch_interactive_session_page')
346
+ def test_passes_all_params_to_api(self, mock_fetch):
347
+ """Factory closure must forward every captured parameter to fetch_interactive_session_page."""
348
+ from cloudos_cli.interactive_session.interactive_session import build_app_filtered_page_fetcher
349
+ from unittest.mock import MagicMock
350
+
351
+ mock_fetch.return_value = {'sessions': [], 'pagination_metadata': {}}
352
+
353
+ cl = MagicMock()
354
+ fetcher = build_app_filtered_page_fetcher(cl, 'ws1', 20, ('running',), True, False, True)
355
+ fetcher(5)
356
+
357
+ mock_fetch.assert_called_once_with(cl, 'ws1', 5, 20, ('running',), True, False, True)
358
+
359
+ @patch('cloudos_cli.interactive_session.interactive_session.fetch_interactive_session_page')
360
+ def test_keeps_all_regular_sessions(self, mock_fetch):
361
+ """Non-app session types must be returned unchanged."""
362
+ from cloudos_cli.interactive_session.interactive_session import build_app_filtered_page_fetcher
363
+ from unittest.mock import MagicMock
364
+
365
+ regular_sessions = [
366
+ {'_id': 'j1', 'interactiveSessionType': 'awsJupyterNotebook'},
367
+ {'_id': 'v1', 'interactiveSessionType': 'awsVSCode'},
368
+ {'_id': 'r1', 'interactiveSessionType': 'awsRstudio'},
369
+ ]
370
+ mock_fetch.return_value = {
371
+ 'sessions': regular_sessions,
372
+ 'pagination_metadata': {'count': 3, 'page': 1, 'totalPages': 1}
373
+ }
374
+
375
+ cl = MagicMock()
376
+ fetcher = build_app_filtered_page_fetcher(cl, 'ws1', 10, None, False, False, True)
377
+ result = fetcher(1)
378
+
379
+ assert len(result['sessions']) == 3
380
+
381
+ @patch('cloudos_cli.interactive_session.interactive_session.fetch_interactive_session_page')
382
+ def test_pagination_metadata_preserved(self, mock_fetch):
383
+ """pagination_metadata from the API response must be returned intact."""
384
+ from cloudos_cli.interactive_session.interactive_session import build_app_filtered_page_fetcher
385
+ from unittest.mock import MagicMock
386
+
387
+ expected_meta = {'count': 10, 'page': 3, 'totalPages': 5}
388
+ mock_fetch.return_value = {'sessions': [], 'pagination_metadata': expected_meta}
389
+
390
+ cl = MagicMock()
391
+ fetcher = build_app_filtered_page_fetcher(cl, 'ws1', 10, None, False, False, True)
392
+ result = fetcher(3)
393
+
394
+ assert result['pagination_metadata'] == expected_meta
395
+
396
+
248
397
  if __name__ == '__main__':
249
398
  pytest.main([__file__, '-v'])
250
399
 
@@ -1 +0,0 @@
1
- __version__ = '2.93.0'
File without changes
File without changes
File without changes
File without changes