cloudos-cli 2.89.0__tar.gz → 2.89.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.89.0 → cloudos_cli-2.89.1}/PKG-INFO +12 -10
  2. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/README.md +11 -9
  3. cloudos_cli-2.89.1/cloudos_cli/_version.py +1 -0
  4. cloudos_cli-2.89.1/cloudos_cli/constants.py +75 -0
  5. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/details.py +189 -146
  6. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli.egg-info/PKG-INFO +12 -10
  7. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli.egg-info/SOURCES.txt +1 -0
  8. cloudos_cli-2.89.1/tests/test_details.py +444 -0
  9. cloudos_cli-2.89.0/cloudos_cli/_version.py +0 -1
  10. cloudos_cli-2.89.0/cloudos_cli/constants.py +0 -28
  11. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/LICENSE +0 -0
  12. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/__init__.py +0 -0
  13. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/__main__.py +0 -0
  14. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/bash/__init__.py +0 -0
  15. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/bash/cli.py +0 -0
  16. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/clos.py +0 -0
  17. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/configure/__init__.py +0 -0
  18. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/configure/cli.py +0 -0
  19. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/configure/configure.py +0 -0
  20. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/cost/__init__.py +0 -0
  21. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/cost/cost.py +0 -0
  22. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/cromwell/__init__.py +0 -0
  23. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/cromwell/cli.py +0 -0
  24. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/datasets/__init__.py +0 -0
  25. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/datasets/cli.py +0 -0
  26. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/datasets/datasets.py +0 -0
  27. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/import_wf/__init__.py +0 -0
  28. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/import_wf/import_wf.py +0 -0
  29. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/interactive_session/__init__.py +0 -0
  30. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/interactive_session/cli.py +0 -0
  31. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/interactive_session/interactive_session.py +0 -0
  32. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/jobs/__init__.py +0 -0
  33. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/jobs/cli.py +0 -0
  34. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/jobs/job.py +0 -0
  35. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/link/__init__.py +0 -0
  36. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/link/cli.py +0 -0
  37. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/link/link.py +0 -0
  38. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/logging/__init__.py +0 -0
  39. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/logging/logger.py +0 -0
  40. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/procurement/__init__.py +0 -0
  41. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/procurement/cli.py +0 -0
  42. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/procurement/images.py +0 -0
  43. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/projects/__init__.py +0 -0
  44. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/projects/cli.py +0 -0
  45. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/queue/__init__.py +0 -0
  46. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/queue/cli.py +0 -0
  47. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/queue/queue.py +0 -0
  48. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/related_analyses/__init__.py +0 -0
  49. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/related_analyses/related_analyses.py +0 -0
  50. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/__init__.py +0 -0
  51. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/array_job.py +0 -0
  52. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/cli_helpers.py +0 -0
  53. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/cloud.py +0 -0
  54. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/errors.py +0 -0
  55. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/last_wf.py +0 -0
  56. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/nextflow_version.py +0 -0
  57. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/requests.py +0 -0
  58. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/utils/resources.py +0 -0
  59. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/workflows/__init__.py +0 -0
  60. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli/workflows/cli.py +0 -0
  61. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli.egg-info/dependency_links.txt +0 -0
  62. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli.egg-info/entry_points.txt +0 -0
  63. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli.egg-info/requires.txt +0 -0
  64. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/cloudos_cli.egg-info/top_level.txt +0 -0
  65. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/setup.cfg +0 -0
  66. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/setup.py +0 -0
  67. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/__init__.py +0 -0
  68. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/functions_for_pytest.py +0 -0
  69. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_cli_project_create.py +0 -0
  70. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_cost/__init__.py +0 -0
  71. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_cost/test_job_cost.py +0 -0
  72. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_error_messages.py +0 -0
  73. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_interactive_session/__init__.py +0 -0
  74. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_interactive_session/test_create_session.py +0 -0
  75. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_interactive_session/test_list_sessions.py +0 -0
  76. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_logging/__init__.py +0 -0
  77. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_logging/test_logger.py +0 -0
  78. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_nextflow_version.py +0 -0
  79. {cloudos_cli-2.89.0 → cloudos_cli-2.89.1}/tests/test_related_analyses/__init__.py +0 -0
  80. {cloudos_cli-2.89.0 → cloudos_cli-2.89.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.89.0
3
+ Version: 2.89.1
4
4
  Summary: Python package for interacting with CloudOS
5
5
  Home-page: https://github.com/lifebit-ai/cloudos-cli
6
6
  Author: David Piñeyro
@@ -799,19 +799,21 @@ The output shows a rich table with job information and pagination details:
799
799
  ```console
800
800
  Executing list...
801
801
 
802
- Job List
803
- ┏━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
804
- ┃ Status ┃ Name ┃ Project ┃ Owner ┃ Pipeline ┃ ID ┃ Submit time ┃
805
- ┡━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
806
- analysis_runtest-proj John rnatoy 692ee71c40e98ed6ed529e432025-12-02
807
- │ │ │ │ Doe │ │ │ 15:30:45 │
808
- │ ◐ │ test_job │ research │ Jane │ VEP │ 692ee81d50f98ed7fe639f54│ 2025-12-02 │
809
- │ │ │ │ Smith │ │ │ 14:20:30 │
810
- └────────┴──────────────┴─────────────┴──────────┴──────────────┴─────────────────────────┴──────────────┘
802
+ ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┓
803
+ ┃ Status ┃ ID ┃ Pipeline ┃ Name ┃ Project ┃ Owner ┃ Runtime ┃
804
+ ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━┩
805
+ │ ✓ │ 692ee71c40e98ed6ed529e43 │ rnatoy │ analysis_run │ test-proj │ John Doe │ 15m 30s │
806
+ 692ee81d50f98ed7fe639f54VEP test_job research Jane Sm… 2m 15s
807
+ └────────┴──────────────────────────┴──────────────┴────────────────┴──────────────┴──────────┴─────────┘
808
+
809
+ Legend: = Completed | ◐ = Running | ✗ = Failed | ■ = Aborted | ○ = Initialising | ? = Unknown
811
810
 
812
811
  Showing 10 of 45 total jobs | Page 1 of 5
813
812
  ```
814
813
 
814
+ > [!NOTE]
815
+ > **Responsive Table Display**: The table automatically adapts to your terminal width, intelligently selecting which columns to display. Narrow terminals show essential columns (Status, ID, Pipeline, Name), while wider terminals progressively add more information (Project, Owner, Runtime, Cost, timestamps, etc.). The table ensures only complete columns are shown and always renders with proper borders.
816
+
815
817
  **Status Indicators**
816
818
 
817
819
  Jobs are displayed with colored visual status indicators:
@@ -764,19 +764,21 @@ The output shows a rich table with job information and pagination details:
764
764
  ```console
765
765
  Executing list...
766
766
 
767
- Job List
768
- ┏━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
769
- ┃ Status ┃ Name ┃ Project ┃ Owner ┃ Pipeline ┃ ID ┃ Submit time ┃
770
- ┡━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
771
- analysis_runtest-proj John rnatoy 692ee71c40e98ed6ed529e432025-12-02
772
- │ │ │ │ Doe │ │ │ 15:30:45 │
773
- │ ◐ │ test_job │ research │ Jane │ VEP │ 692ee81d50f98ed7fe639f54│ 2025-12-02 │
774
- │ │ │ │ Smith │ │ │ 14:20:30 │
775
- └────────┴──────────────┴─────────────┴──────────┴──────────────┴─────────────────────────┴──────────────┘
767
+ ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┓
768
+ ┃ Status ┃ ID ┃ Pipeline ┃ Name ┃ Project ┃ Owner ┃ Runtime ┃
769
+ ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━┩
770
+ │ ✓ │ 692ee71c40e98ed6ed529e43 │ rnatoy │ analysis_run │ test-proj │ John Doe │ 15m 30s │
771
+ 692ee81d50f98ed7fe639f54VEP test_job research Jane Sm… 2m 15s
772
+ └────────┴──────────────────────────┴──────────────┴────────────────┴──────────────┴──────────┴─────────┘
773
+
774
+ Legend: = Completed | ◐ = Running | ✗ = Failed | ■ = Aborted | ○ = Initialising | ? = Unknown
776
775
 
777
776
  Showing 10 of 45 total jobs | Page 1 of 5
778
777
  ```
779
778
 
779
+ > [!NOTE]
780
+ > **Responsive Table Display**: The table automatically adapts to your terminal width, intelligently selecting which columns to display. Narrow terminals show essential columns (Status, ID, Pipeline, Name), while wider terminals progressively add more information (Project, Owner, Runtime, Cost, timestamps, etc.). The table ensures only complete columns are shown and always renders with proper borders.
781
+
780
782
  **Status Indicators**
781
783
 
782
784
  Jobs are displayed with colored visual status indicators:
@@ -0,0 +1 @@
1
+ __version__ = '2.89.1'
@@ -0,0 +1,75 @@
1
+ """Global constants for CloudOS CLI."""
2
+
3
+ # Job status constants
4
+ JOB_COMPLETED = 'completed'
5
+ JOB_FAILED = 'failed'
6
+ JOB_ABORTED = 'aborted'
7
+
8
+ # Nextflow version constants
9
+ AWS_NEXTFLOW_VERSIONS = ['22.10.8', '24.04.4', '25.04.8', '25.10.4']
10
+ AZURE_NEXTFLOW_VERSIONS = ['22.11.1-edge']
11
+ HPC_NEXTFLOW_VERSIONS = ['22.10.8']
12
+ AWS_NEXTFLOW_LATEST = '25.10.4'
13
+ AZURE_NEXTFLOW_LATEST = '22.11.1-edge'
14
+ HPC_NEXTFLOW_LATEST = '22.10.8'
15
+
16
+ # Nextflow version defaults by workflow type
17
+ PLATFORM_WORKFLOW_NEXTFLOW_VERSION = '22.10.8' # For Lifebit Platform workflows (modules)
18
+ USER_WORKFLOW_NEXTFLOW_VERSION = '24.04.4' # For user-imported workflows
19
+
20
+ # Job abort states
21
+ ABORT_JOB_STATES = ['running', 'initializing']
22
+
23
+ # Request interval for Cromwell
24
+ REQUEST_INTERVAL_CROMWELL = 30
25
+
26
+ # Global constants for CloudOS CLI
27
+ CLOUDOS_URL = 'https://cloudos.lifebit.ai'
28
+ INIT_PROFILE = 'initialisingProfile'
29
+
30
+ # Job status symbol mapping
31
+ JOB_STATUS_SYMBOLS = {
32
+ "completed": "[bold green]✓[/bold green]",
33
+ "running": "[bold bright_black]◐[/bold bright_black]",
34
+ "failed": "[bold red]✗[/bold red]",
35
+ "aborted": "[bold orange3]■[/bold orange3]",
36
+ "aborting": "[bold orange3]⊡[/bold orange3]",
37
+ "initialising": "[bold bright_black]○[/bold bright_black]",
38
+ "scheduled": "[bold cyan]◷[/bold cyan]",
39
+ "n/a": "[bold bright_black]?[/bold bright_black]"
40
+ }
41
+
42
+ # Column priority groups for responsive table display
43
+ COLUMN_PRIORITY_GROUPS = {
44
+ 'minimal': ['status', 'id', 'name'],
45
+ 'essential': ['status', 'id', 'name', 'pipeline'],
46
+ 'important': ['project', 'owner', 'run_time', 'cost'],
47
+ 'useful': ['submit_time', 'end_time', 'commit'],
48
+ 'extended': ['resources', 'storage_type']
49
+ }
50
+
51
+ # Essential columns priority order for auto-selection
52
+ ESSENTIAL_COLUMN_PRIORITY = ['status', 'id', 'name', 'pipeline']
53
+
54
+ # Additional columns priority order for auto-selection
55
+ ADDITIONAL_COLUMN_PRIORITY = [
56
+ 'project', 'owner', 'run_time', 'cost',
57
+ 'submit_time', 'end_time', 'commit', 'resources', 'storage_type'
58
+ ]
59
+
60
+ # Column configurations for job list table
61
+ COLUMN_CONFIGS = {
62
+ 'status': {"header": "Status", "style": "cyan", "no_wrap": True, "min_width": 6, "max_width": 6},
63
+ 'name': {"header": "Name", "style": "green", "overflow": "fold", "no_wrap": False, "min_width": 6, "max_width": 14},
64
+ 'project': {"header": "Project", "style": "magenta", "overflow": "fold", "no_wrap": False, "min_width": 6, "max_width": 18},
65
+ 'owner': {"header": "Owner", "style": "blue", "overflow": "fold", "no_wrap": False, "min_width": 4, "max_width": 14},
66
+ 'pipeline': {"header": "Pipeline", "style": "yellow", "overflow": "fold", "no_wrap": False, "min_width": 8, "max_width": 14},
67
+ 'id': {"header": "ID", "style": "white", "overflow": "ellipsis", "no_wrap": True, "min_width": 24, "max_width": 24},
68
+ 'submit_time': {"header": "Submit", "style": "cyan", "no_wrap": True, "min_width": 12, "max_width": 16},
69
+ 'end_time': {"header": "End", "style": "cyan", "no_wrap": True, "min_width": 12, "max_width": 16},
70
+ 'run_time': {"header": "Runtime", "style": "green", "no_wrap": True, "min_width": 8, "max_width": 12},
71
+ 'commit': {"header": "Commit", "style": "magenta", "no_wrap": True, "min_width": 9, "max_width": 10},
72
+ 'cost': {"header": "Cost", "style": "yellow", "no_wrap": True, "min_width": 8, "max_width": 12},
73
+ 'resources': {"header": "Resources", "style": "blue", "overflow": "ellipsis", "no_wrap": True, "min_width": 8, "max_width": 16},
74
+ 'storage_type': {"header": "Storage", "style": "white", "no_wrap": True, "min_width": 8, "max_width": 10}
75
+ }
@@ -6,6 +6,14 @@ import csv
6
6
  import os
7
7
  import sys
8
8
 
9
+ from cloudos_cli.constants import (
10
+ JOB_STATUS_SYMBOLS,
11
+ COLUMN_PRIORITY_GROUPS,
12
+ ESSENTIAL_COLUMN_PRIORITY,
13
+ ADDITIONAL_COLUMN_PRIORITY,
14
+ COLUMN_CONFIGS
15
+ )
16
+
9
17
 
10
18
  def get_path(param, param_kind_map, execution_platform, storage_provider, mode="parameters"):
11
19
  """
@@ -362,15 +370,7 @@ def _build_job_row_values(job, cloudos_url, terminal_width, columns_to_show):
362
370
  """
363
371
  # Status with colored and bold ANSI symbols
364
372
  status_raw = str(job.get("status", "N/A"))
365
- status_symbol_map = {
366
- "completed": "[bold green]✓[/bold green]",
367
- "running": "[bold bright_black]◐[/bold bright_black]",
368
- "failed": "[bold red]✗[/bold red]",
369
- "aborted": "[bold orange3]■[/bold orange3]",
370
- "initialising": "[bold bright_black]○[/bold bright_black]",
371
- "N/A": "[bold bright_black]?[/bold bright_black]"
372
- }
373
- status = status_symbol_map.get(status_raw.lower(), status_raw)
373
+ status = JOB_STATUS_SYMBOLS.get(status_raw.lower(), status_raw)
374
374
 
375
375
  # Name
376
376
  name = str(job.get("name", "N/A"))
@@ -378,24 +378,16 @@ def _build_job_row_values(job, cloudos_url, terminal_width, columns_to_show):
378
378
  # Project
379
379
  project = str(job.get("project", {}).get("name", "N/A"))
380
380
 
381
- # Owner (compact format for small terminals)
381
+ # Owner (single-line format, no wrapping)
382
382
  user_info = job.get("user", {})
383
383
  name_part = user_info.get('name', '')
384
384
  surname_part = user_info.get('surname', '')
385
- if terminal_width < 90:
386
- if name_part and surname_part:
387
- owner = f"{name_part[0]}.{surname_part[0]}."
388
- elif name_part or surname_part:
389
- owner = (name_part or surname_part)[:8]
390
- else:
391
- owner = "N/A"
385
+ if name_part and surname_part:
386
+ owner = f"{name_part} {surname_part}"
387
+ elif name_part or surname_part:
388
+ owner = name_part or surname_part
392
389
  else:
393
- if name_part and surname_part:
394
- owner = f"{name_part}\n{surname_part}"
395
- elif name_part or surname_part:
396
- owner = name_part or surname_part
397
- else:
398
- owner = "N/A"
390
+ owner = "N/A"
399
391
 
400
392
  # Pipeline
401
393
  pipeline = str(job.get("workflow", {}).get("name", "N/A")).split('\n')[0].strip()
@@ -407,23 +399,23 @@ def _build_job_row_values(job, cloudos_url, terminal_width, columns_to_show):
407
399
  job_url = f"{cloudos_url}/app/advanced-analytics/analyses/{job_id}"
408
400
  job_id_with_link = f"[link={job_url}]{job_id}[/link]"
409
401
 
410
- # Submit time (compact format for small terminals)
402
+ # Submit time (single-line format)
411
403
  created_at = job.get("createdAt")
412
404
  if created_at:
413
405
  try:
414
406
  dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
415
- submit_time = dt.strftime('%m-%d\n%H:%M') if terminal_width < 90 else dt.strftime('%Y-%m-%d\n%H:%M:%S')
407
+ submit_time = dt.strftime('%m-%d %H:%M') if terminal_width < 90 else dt.strftime('%Y-%m-%d %H:%M')
416
408
  except (ValueError, TypeError):
417
409
  submit_time = "N/A"
418
410
  else:
419
411
  submit_time = "N/A"
420
412
 
421
- # End time (compact format for small terminals)
413
+ # End time (single-line format)
422
414
  end_time_raw = job.get("endTime")
423
415
  if end_time_raw:
424
416
  try:
425
417
  dt = datetime.fromisoformat(end_time_raw.replace('Z', '+00:00'))
426
- end_time = dt.strftime('%m-%d\n%H:%M') if terminal_width < 90 else dt.strftime('%Y-%m-%d\n%H:%M:%S')
418
+ end_time = dt.strftime('%m-%d %H:%M') if terminal_width < 90 else dt.strftime('%Y-%m-%d %H:%M')
427
419
  except (ValueError, TypeError):
428
420
  end_time = "N/A"
429
421
  else:
@@ -504,7 +496,28 @@ def _build_job_row_values(job, cloudos_url, terminal_width, columns_to_show):
504
496
  return [column_values[col] for col in columns_to_show]
505
497
 
506
498
 
507
- def _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, all_columns):
499
+ def _create_status_legend():
500
+ """Create a formatted legend for job status symbols.
501
+
502
+ Returns
503
+ -------
504
+ str
505
+ Formatted legend string with status symbols and their meanings.
506
+ """
507
+ legend_items = [
508
+ "[bold cyan]◷[/bold cyan] = Scheduled",
509
+ "[bold bright_black]○[/bold bright_black] = Initialising",
510
+ "[bold bright_black]◐[/bold bright_black] = Running",
511
+ "[bold green]✓[/bold green] = Completed",
512
+ "[bold red]✗[/bold red] = Failed",
513
+ "[bold orange3]⊡[/bold orange3] = Aborting",
514
+ "[bold orange3]■[/bold orange3] = Aborted",
515
+ "[bold bright_black]?[/bold bright_black] = Unknown"
516
+ ]
517
+ return "[cyan]Legend:[/cyan] " + " | ".join(legend_items)
518
+
519
+
520
+ def _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, column_configs):
508
521
  """Helper function to build a complete job table.
509
522
 
510
523
  Parameters
@@ -517,7 +530,7 @@ def _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, all_col
517
530
  Current terminal width for responsive formatting
518
531
  columns_to_show : list
519
532
  List of column keys to include
520
- all_columns : dict
533
+ column_configs : dict
521
534
  Dictionary of all column configurations
522
535
 
523
536
  Returns
@@ -525,11 +538,11 @@ def _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, all_col
525
538
  Table
526
539
  Rich Table object populated with job rows
527
540
  """
528
- table = Table(title="Job List")
541
+ table = Table()
529
542
 
530
543
  # Add columns to table
531
544
  for col_key in columns_to_show:
532
- col_config = all_columns[col_key]
545
+ col_config = column_configs[col_key]
533
546
  table.add_column(
534
547
  col_config["header"],
535
548
  style=col_config.get("style"),
@@ -547,129 +560,169 @@ def _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, all_col
547
560
  return table
548
561
 
549
562
 
550
- def create_job_list_table(jobs, cloudos_url, pagination_metadata=None, selected_columns=None, fetch_page_callback=None):
551
- """
552
- Creates a formatted job list table for stdout output with responsive design and interactive pagination.
553
-
554
- The table automatically adapts to terminal width by showing different column sets:
555
- - Very narrow (<60 chars): Essential columns only (status, name, pipeline, id)
556
- - Narrow (<90 chars): + Important columns (project, owner, run_time, cost)
557
- - Medium (<120 chars): + Useful columns (submit_time, end_time, commit)
558
- - Wide (≥120 chars): + Extended columns (resources, storage_type)
559
-
560
- Status symbols are displayed with colors:
561
- - Green ✓ for completed jobs
562
- - Grey ◐ for running jobs
563
- - Red ✗ for failed jobs
564
- - Orange ■ for aborted jobs
565
- - Grey ○ for initialising jobs
566
- - Grey ? for unknown status
563
+ def _calculate_table_width(column_list, col_configs):
564
+ """Calculate total table width including all overhead."""
565
+ borders_and_separators = 2 + (len(column_list) - 1)
566
+ column_widths = sum(
567
+ col_configs[col].get('max_width', col_configs[col].get('min_width', 10)) + 2
568
+ for col in column_list
569
+ )
570
+ buffer = 2
567
571
 
572
+ return borders_and_separators + column_widths + buffer
573
+
574
+
575
+ def _fit_columns_to_terminal(cols, terminal_w, col_configs, preserve_order=False):
576
+ """Build column list progressively, only adding columns that fit completely.
577
+
568
578
  Parameters
569
579
  ----------
570
- jobs : list
571
- List of job dictionaries from the CloudOS API.
572
- cloudos_url : str
573
- The CloudOS service URL for generating job links.
574
- pagination_metadata : dict, optional
575
- Pagination metadata from the API response containing:
576
- - 'Pagination-Count': Total number of jobs matching the filter
577
- - 'Pagination-Page': Current page number
578
- - 'Pagination-Limit': Page size
579
- selected_columns : str or list, optional
580
- Column names to display. Can be:
581
- - None: Auto-responsive based on terminal width
582
- - String: Comma-separated column names (e.g., "status,name,cost")
583
- - List: List of column names
584
- Valid columns: 'status', 'name', 'project', 'owner', 'pipeline', 'id',
585
- 'submit_time', 'end_time', 'run_time', 'commit', 'cost', 'resources', 'storage_type'
586
- fetch_page_callback : callable, optional
587
- Callback function to fetch a specific page of results for interactive pagination.
588
- Should accept page number (1-indexed) and return dict with 'jobs' and 'pagination_metadata' keys.
589
- If provided, enables interactive navigation (n=next, p=previous, q=quit).
590
-
580
+ cols : list
581
+ List of column keys to fit
582
+ terminal_w : int
583
+ Terminal width to fit columns into
584
+ col_configs : dict
585
+ Column configuration dictionary
586
+ preserve_order : bool
587
+ If True, preserve the order of cols. If False, reorder by priority.
588
+
591
589
  Returns
592
590
  -------
593
- None
594
- Prints the formatted table to console with pagination information.
595
-
596
- Raises
597
- ------
598
- ValueError
599
- If invalid column names are provided in selected_columns.
591
+ list
592
+ Columns that fit in the terminal, in appropriate order
600
593
  """
601
- console = Console()
594
+ if len(cols) == 0:
595
+ return cols
596
+
597
+ if preserve_order:
598
+ # User explicitly specified column order - preserve it
599
+ result = []
600
+ for col in cols:
601
+ test_list = result + [col]
602
+ width = _calculate_table_width(test_list, col_configs)
603
+ if width <= terminal_w:
604
+ result.append(col)
605
+ # Continue evaluating remaining columns even if this one doesn't fit
606
+
607
+ # Ensure at least one column is shown, even if terminal is very narrow
608
+ if len(result) == 0 and len(cols) > 0:
609
+ # Find the narrowest column that was requested
610
+ narrowest_col = min(cols, key=lambda c: col_configs[c].get('max_width', col_configs[c].get('min_width', 10)))
611
+ result.append(narrowest_col)
612
+
613
+ return result
614
+
615
+ # Auto-selection mode: reorder by priority for better UX
616
+ essential_requested = [col for col in ESSENTIAL_COLUMN_PRIORITY if col in cols]
617
+ additional_requested = [col for col in cols if col not in ESSENTIAL_COLUMN_PRIORITY]
618
+
619
+ additional_ordered = [col for col in ADDITIONAL_COLUMN_PRIORITY if col in additional_requested]
620
+ additional_ordered.extend([col for col in additional_requested if col not in ADDITIONAL_COLUMN_PRIORITY])
621
+
622
+ result = []
623
+ for col in essential_requested:
624
+ test_list = result + [col]
625
+ width = _calculate_table_width(test_list, col_configs)
626
+ if width <= terminal_w:
627
+ result.append(col)
628
+ else:
629
+ # Column doesn't fit - continue trying remaining columns
630
+ # Special case: always show at least status on very narrow terminals
631
+ if len(result) == 0 and col == 'status':
632
+ result.append(col)
633
+
634
+ # Try to add additional columns one by one
635
+ for col in additional_ordered:
636
+ test_list = result + [col]
637
+ width = _calculate_table_width(test_list, col_configs)
638
+ if width <= terminal_w:
639
+ result.append(col)
640
+ # Continue trying remaining columns even if this one doesn't fit
641
+
642
+ return result
643
+
602
644
 
645
+ def create_job_list_table(jobs, cloudos_url, pagination_metadata=None, selected_columns=None, fetch_page_callback=None):
646
+ """Creates a formatted job list table with responsive design and pagination."""
603
647
  # Get terminal width for responsive design
604
648
  try:
605
649
  terminal_width = os.get_terminal_size().columns
606
650
  except OSError:
607
651
  terminal_width = 80 # Default fallback
608
652
 
609
- # Define column priority groups for small terminals
610
- priority_columns = {
611
- 'essential': ['status', 'name', 'pipeline', 'id'], # ~40 chars minimum
612
- 'important': ['project', 'owner', 'run_time', 'cost'], # +30 chars
613
- 'useful': [ 'submit_time', 'end_time', 'commit'], # +50 chars
614
- 'extended': [ 'resources', 'storage_type'] # +30 chars
615
- }
616
-
617
- # Define all available columns with their configurations
618
- all_columns = {
619
- 'status': {"header": "Status", "style": "cyan", "no_wrap": True, "min_width": 6, "max_width": 6},
620
- 'name': {"header": "Name", "style": "green", "overflow": "ellipsis", "min_width": 6, "max_width": 20},
621
- 'project': {"header": "Project", "style": "magenta", "overflow": "ellipsis", "min_width": 6, "max_width": 15},
622
- 'owner': {"header": "Owner", "style": "blue", "overflow": "ellipsis", "min_width": 6, "max_width": 12},
623
- 'pipeline': {"header": "Pipeline", "style": "yellow", "overflow": "ellipsis", "min_width": 6, "max_width": 15},
624
- 'id': {"header": "ID", "style": "white", "overflow": "ellipsis", "min_width": 6, "max_width": 12},
625
- 'submit_time': {"header": "Submit", "style": "cyan", "no_wrap": True, "min_width": 10, "max_width": 16},
626
- 'end_time': {"header": "End", "style": "cyan", "no_wrap": True, "min_width": 10, "max_width": 16},
627
- 'run_time': {"header": "Runtime", "style": "green", "no_wrap": True, "min_width": 5, "max_width": 10},
628
- 'commit': {"header": "Commit", "style": "magenta", "no_wrap": True, "min_width": 7, "max_width": 8},
629
- 'cost': {"header": "Cost", "style": "yellow", "no_wrap": True, "min_width": 6, "max_width": 10},
630
- 'resources': {"header": "Resources", "style": "blue", "overflow": "ellipsis", "min_width": 3, "max_width": 15},
631
- 'storage_type': {"header": "Storage", "style": "white", "no_wrap": True, "min_width": 3, "max_width": 8}
632
- }
633
-
634
- # Validate and process selected_columns
635
653
  if selected_columns is None:
636
- # Auto-select columns based on terminal width if none specified
637
- if terminal_width < 60:
638
- columns_to_show = priority_columns['essential']
639
- elif terminal_width < 90:
640
- columns_to_show = priority_columns['essential'] + priority_columns['important']
641
- elif terminal_width < 130:
642
- columns_to_show = (priority_columns['essential'] +
643
- priority_columns['important'] +
644
- priority_columns['useful'])
645
- else: # terminal_width >= 130
646
- columns_to_show = (priority_columns['essential'] +
647
- priority_columns['important'] +
648
- priority_columns['useful'] +
649
- priority_columns['extended'])
654
+ if terminal_width < 80:
655
+ columns_to_show = COLUMN_PRIORITY_GROUPS['minimal']
656
+ elif terminal_width <= 100:
657
+ columns_to_show = COLUMN_PRIORITY_GROUPS['essential']
658
+ elif terminal_width < 150:
659
+ columns_to_show = COLUMN_PRIORITY_GROUPS['essential'] + COLUMN_PRIORITY_GROUPS['important']
660
+ elif terminal_width < 180:
661
+ columns_to_show = (COLUMN_PRIORITY_GROUPS['essential'] +
662
+ COLUMN_PRIORITY_GROUPS['important'] +
663
+ COLUMN_PRIORITY_GROUPS['useful'])
664
+ else:
665
+ columns_to_show = (COLUMN_PRIORITY_GROUPS['essential'] +
666
+ COLUMN_PRIORITY_GROUPS['important'] +
667
+ COLUMN_PRIORITY_GROUPS['useful'] +
668
+ COLUMN_PRIORITY_GROUPS['extended'])
650
669
  else:
651
- # Accept either a comma-separated string or a list
652
670
  if isinstance(selected_columns, str):
653
671
  selected_columns = [col.strip().lower() for col in selected_columns.split(',')]
654
- valid_columns = list(all_columns.keys())
672
+
673
+ # Store original count before deduplication (for accurate warning message)
674
+ original_column_count = len(selected_columns)
675
+
676
+ # Check for duplicates
677
+ duplicates = [col for col in selected_columns if selected_columns.count(col) > 1]
678
+ if duplicates:
679
+ # Deduplicate while preserving order
680
+ seen = set()
681
+ deduplicated = []
682
+ for col in selected_columns:
683
+ if col not in seen:
684
+ seen.add(col)
685
+ deduplicated.append(col)
686
+ selected_columns = deduplicated
687
+
688
+ # Warn user about deduplication
689
+ unique_duplicates = list(dict.fromkeys(duplicates)) # Remove duplicates from duplicates list
690
+ console = Console()
691
+ console.print(f"[yellow]Warning: Duplicate columns removed: {', '.join(unique_duplicates)}[/yellow]")
692
+
693
+ valid_columns = list(COLUMN_CONFIGS.keys())
655
694
  invalid_cols = [col for col in selected_columns if col not in valid_columns]
656
695
  if invalid_cols:
657
696
  raise ValueError(f"Invalid column names: {', '.join(invalid_cols)}. "
658
697
  f"Valid columns are: {', '.join(valid_columns)}")
659
- columns_to_show = selected_columns # Preserve user-specified order
698
+ columns_to_show = selected_columns
699
+
700
+ effective_width = terminal_width - 5
701
+ console = Console(width=terminal_width)
702
+ # Preserve user-specified column order; auto-selected columns are reordered by priority
703
+ preserve_order = selected_columns is not None
704
+ columns_to_show = _fit_columns_to_terminal(columns_to_show, effective_width, COLUMN_CONFIGS, preserve_order)
705
+
706
+ # Warn if user-requested columns were truncated due to narrow terminal
707
+ if preserve_order and selected_columns:
708
+ # Use original count before deduplication for accurate message
709
+ original_count = original_column_count
710
+ if len(columns_to_show) < original_count:
711
+ console.print(f"[yellow]Warning: Terminal too narrow. Showing {len(columns_to_show)} of {original_count} requested columns.[/yellow]")
712
+ console.print(f"[yellow]Increase terminal width to see all columns.[/yellow]\n")
660
713
 
661
714
  if not jobs:
662
715
  console.print("\n[yellow]No jobs found matching the criteria.[/yellow]")
663
716
  return
664
717
 
665
- # Create table using helper function
666
- table = _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, all_columns)
718
+ # Use actual terminal_width (not effective_width) for date formatting logic
719
+ table = _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, COLUMN_CONFIGS)
667
720
 
668
- # If no fetch_page_callback, display static table
669
721
  if not fetch_page_callback or not pagination_metadata:
670
722
  console.print(table)
671
-
672
- # Display pagination info at the bottom
723
+ legend = _create_status_legend()
724
+ console.print(f"\n{legend}\n")
725
+
673
726
  if pagination_metadata:
674
727
  total_jobs = pagination_metadata.get('Pagination-Count', 0)
675
728
  current_page = pagination_metadata.get('Pagination-Page', 1)
@@ -679,20 +732,17 @@ def create_job_list_table(jobs, cloudos_url, pagination_metadata=None, selected_
679
732
  console.print(f"\n[cyan]Showing {len(jobs)} of {total_jobs} total jobs | Page {current_page} of {total_pages}[/cyan]")
680
733
  return
681
734
 
682
- # Interactive pagination mode
683
- current_page = pagination_metadata.get('Pagination-Page', 1) or 1 # Ensure never None
735
+ current_page = pagination_metadata.get('Pagination-Page', 1) or 1
684
736
  total_jobs = pagination_metadata.get('Pagination-Count', 0)
685
737
  page_size_value = pagination_metadata.get('Pagination-Limit', 10)
686
738
  total_pages = (total_jobs + page_size_value - 1) // page_size_value if total_jobs > 0 else 1
687
-
688
739
  show_error = None
689
-
740
+
690
741
  while True:
691
- # Clear console and display table
692
742
  console.clear()
693
743
  console.print(table)
694
-
695
- # Display pagination info
744
+ legend = _create_status_legend()
745
+ console.print(f"{legend}\n")
696
746
  console.print(f"\n[cyan]Total jobs:[/cyan] {total_jobs}")
697
747
  if total_pages > 1:
698
748
  console.print(f"[cyan]Page:[/cyan] {current_page} of {total_pages}")
@@ -705,21 +755,19 @@ def create_job_list_table(jobs, cloudos_url, pagination_metadata=None, selected_
705
755
 
706
756
  # Show pagination controls only if there are multiple pages
707
757
  if total_pages > 1:
708
- # Check if we're in an interactive environment
709
758
  if not sys.stdin.isatty():
710
759
  console.print("\n[yellow]Note: Pagination not available in non-interactive mode. Showing page 1 of {0}.[/yellow]".format(total_pages))
711
760
  console.print("[yellow]Run in an interactive terminal to navigate through all pages.[/yellow]")
712
761
  break
713
-
762
+
714
763
  console.print(f"\n[bold cyan]n[/] = next, [bold cyan]p[/] = prev, [bold cyan]q[/] = quit")
715
-
716
- # Get user input for navigation
764
+
717
765
  try:
718
766
  choice = input(">>> ").strip().lower()
719
767
  except (EOFError, KeyboardInterrupt):
720
768
  console.print("\n[yellow]Pagination interrupted.[/yellow]")
721
769
  break
722
-
770
+
723
771
  if choice in ("q", "quit"):
724
772
  break
725
773
  elif choice in ("n", "next"):
@@ -732,10 +780,8 @@ def create_job_list_table(jobs, cloudos_url, pagination_metadata=None, selected_
732
780
  total_pages = pagination_metadata.get('totalPages',
733
781
  (pagination_metadata.get('Pagination-Count', 0) + page_size_value - 1) // page_size_value
734
782
  if pagination_metadata.get('Pagination-Count', 0) > 0 else 1)
735
-
736
- # Rebuild table with new jobs using helper function
737
- table = _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, all_columns)
738
-
783
+ # Use terminal_width (not effective_width) for consistent date formatting
784
+ table = _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, COLUMN_CONFIGS)
739
785
  except Exception as e:
740
786
  show_error = f"[red]Error fetching page: {str(e)}[/red]"
741
787
  else:
@@ -750,10 +796,8 @@ def create_job_list_table(jobs, cloudos_url, pagination_metadata=None, selected_
750
796
  total_pages = pagination_metadata.get('totalPages',
751
797
  (pagination_metadata.get('Pagination-Count', 0) + page_size_value - 1) // page_size_value
752
798
  if pagination_metadata.get('Pagination-Count', 0) > 0 else 1)
753
-
754
- # Rebuild table with new jobs using helper function
755
- table = _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, all_columns)
756
-
799
+ # Use terminal_width (not effective_width) for consistent date formatting
800
+ table = _build_job_table(jobs, cloudos_url, terminal_width, columns_to_show, COLUMN_CONFIGS)
757
801
  except Exception as e:
758
802
  show_error = f"[red]Error fetching page: {str(e)}[/red]"
759
803
  else:
@@ -761,7 +805,6 @@ def create_job_list_table(jobs, cloudos_url, pagination_metadata=None, selected_
761
805
  else:
762
806
  show_error = "[yellow]Invalid choice. Use 'n' (next), 'p' (previous), or 'q' (quit)[/yellow]"
763
807
  else:
764
- # Only one page, exit after displaying
765
808
  break
766
809
 
767
810