scientiflow-cli 0.4.3__tar.gz → 0.4.5__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 (29) hide show
  1. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/PKG-INFO +1 -1
  2. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/pyproject.toml +1 -1
  3. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/config.py +1 -1
  4. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/main.py +34 -0
  5. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/decode_and_execute.py +40 -25
  6. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/singularity.py +12 -7
  7. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/LICENSE.md +0 -0
  8. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/README.md +0 -0
  9. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/__init__.py +0 -0
  10. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/__main__.py +0 -0
  11. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/__init__.py +0 -0
  12. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/auth_utils.py +0 -0
  13. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/login.py +0 -0
  14. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/logout.py +0 -0
  15. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/__init__.py +0 -0
  16. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/container_manager.py +0 -0
  17. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/get_jobs.py +0 -0
  18. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/__init__.py +0 -0
  19. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/auth_service.py +0 -0
  20. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/base_directory.py +0 -0
  21. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/executor.py +0 -0
  22. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/request_handler.py +0 -0
  23. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/rich_printer.py +0 -0
  24. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/status_updater.py +0 -0
  25. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/__init__.py +0 -0
  26. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/encryption.py +0 -0
  27. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/file_manager.py +0 -0
  28. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/logger.py +0 -0
  29. {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/mock.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scientiflow-cli
3
- Version: 0.4.3
3
+ Version: 0.4.5
4
4
  Summary: CLI tool for scientiflow. This application runs on the client side, decodes pipelines, and executes them in the configured order!
5
5
  License: Proprietary
6
6
  Author: ScientiFlow
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "scientiflow-cli"
3
- version = "0.4.3"
3
+ version = "0.4.5"
4
4
  description = "CLI tool for scientiflow. This application runs on the client side, decodes pipelines, and executes them in the configured order!"
5
5
  authors = ["ScientiFlow <scientiflow@gmail.com>"]
6
6
  license = "Proprietary"
@@ -6,4 +6,4 @@ class Config:
6
6
  APP_BASE_URL = os.getenv("APP_BASE_URL", "https://www.backend.scientiflow.com/api")
7
7
  elif mode=="dev":
8
8
  APP_BASE_URL = "http://127.0.0.1:8000/api"
9
- # APP_BASE_URL = os.getenv("APP_BASE_URL", "https://www.scientiflow-backend-dev.scientiflow.com/api")
9
+ # APP_BASE_URL = os.getenv("APP_BASE_URL", "https://www.scientiflow-backend-dev.scientiflow.com/api")
@@ -1,5 +1,9 @@
1
1
  import argparse
2
2
  import sys
3
+ import pathlib
4
+ import re
5
+ from importlib import metadata as importlib_metadata
6
+ from importlib.metadata import PackageNotFoundError
3
7
  from scientiflow_cli.cli.login import login_user
4
8
  from scientiflow_cli.cli.logout import logout_user
5
9
  from scientiflow_cli.pipeline.get_jobs import get_jobs
@@ -37,6 +41,30 @@ def display_help(parser):
37
41
 
38
42
  printer.print_table("Scientiflow Agent CLI", columns, rows)
39
43
 
44
+
45
+ def get_package_version() -> str:
46
+ """Return the package version.
47
+
48
+ Try to get the installed package version via importlib.metadata. If the
49
+ package isn't installed (PackageNotFoundError), fall back to reading the
50
+ `pyproject.toml` file in the project root.
51
+ """
52
+ try:
53
+ return importlib_metadata.version("scientiflow-cli")
54
+ except PackageNotFoundError:
55
+ try:
56
+ # Project root is two levels up from this file: .../scientiflow_cli/main.py
57
+ project_root = pathlib.Path(__file__).resolve().parents[1]
58
+ pyproject = project_root / "pyproject.toml"
59
+ if pyproject.exists():
60
+ text = pyproject.read_text(encoding="utf-8")
61
+ m = re.search(r"^version\s*=\s*[\"']([^\"']+)[\"']", text, re.MULTILINE)
62
+ if m:
63
+ return m.group(1)
64
+ except Exception:
65
+ pass
66
+ return "unknown"
67
+
40
68
  def main():
41
69
  parser = RichArgumentParser(description="Scientiflow Agent CLI", add_help=False)
42
70
  parser.formatter_class = lambda prog: argparse.HelpFormatter(prog, max_help_position=50)
@@ -44,6 +72,7 @@ def main():
44
72
  parser.add_argument('-h', '--help', action='store_true', help="Show this help message and exit")
45
73
  parser.add_argument('--login', action='store_true', help="Login using your scientiflow credentials")
46
74
  parser.add_argument('--logout', action='store_true', help="Logout from scientiflow")
75
+ parser.add_argument('-v', '--version', action='store_true', help="Show package version and exit")
47
76
  parser.add_argument('--list-jobs', action='store_true', help="Get all the pending jobs to execute")
48
77
  parser.add_argument('--set-base-directory', action='store_true', help="Set the base directory to the current working directory \nOptionally, use --hostname to specify the hostname for this server")
49
78
  parser.add_argument('-p', '--parallel', action='store_true', help=argparse.SUPPRESS) # Hide -p or --parallel from --help
@@ -74,6 +103,11 @@ def main():
74
103
  display_help(parser)
75
104
  sys.exit()
76
105
 
106
+ if args.version:
107
+ version = get_package_version()
108
+ printer.print_message(f"Version: {version}", style="bold green")
109
+ sys.exit()
110
+
77
111
  try:
78
112
  if args.login:
79
113
  login_user(token=args.token)
@@ -1,7 +1,7 @@
1
1
  import subprocess
2
2
  import tempfile
3
3
  import os
4
- import re, shlex
4
+ import re
5
5
  import multiprocessing
6
6
  import threading
7
7
  from concurrent.futures import ProcessPoolExecutor, as_completed
@@ -55,6 +55,10 @@ class PipelineExecutor:
55
55
  self.background_jobs_count = 0 # Track number of active background jobs
56
56
  self.background_jobs_completed = 0 # Track completed background jobs
57
57
  self.background_jobs_lock = threading.Lock() # Thread-safe counter updates
58
+
59
+ # For resuming: flag to track if we've reached the resume point
60
+ self.resume_mode = (job_status == "running" and current_node_from_config is not None)
61
+ self.reached_resume_point = False
58
62
 
59
63
  # Set up job-specific log file
60
64
  self.log_file_path = os.path.join(self.base_dir, self.project_title, self.job_dir_name, "logs", "output.log")
@@ -109,27 +113,9 @@ class PipelineExecutor:
109
113
  except Exception as e:
110
114
  print(f"[ERROR] Failed to update terminal output: {e}")
111
115
 
112
-
113
- # def replace_variables(self, command: str) -> str:
114
- # """Replace placeholders like ${VAR} with environment values."""
115
- # print(self.environment_variables)
116
- # return re.sub(r'\$\{(\w+)\}', lambda m: self.environment_variables.get(m.group(1), m.group(0)), command)
117
-
118
116
  def replace_variables(self, command: str) -> str:
119
- """Replace placeholders like ${VAR} with environment values.
120
- - If value is a list: safely join into space-separated arguments (quoted).
121
- - Otherwise: use the old behavior (direct substitution).
122
- """
123
- def replacer(match):
124
- key = match.group(1)
125
- value = self.environment_variables.get(key, match.group(0))
126
- # ✅ Special handling for lists
127
- if isinstance(value, list):
128
- return " ".join(shlex.quote(str(v)) for v in value)
129
- # ✅ Default: keep original behavior for strings, numbers, None
130
- return self.environment_variables.get(key, match.group(0))
131
- return re.sub(r'\$\{(\w+)\}', replacer, command)
132
-
117
+ """Replace placeholders like ${VAR} with environment values."""
118
+ return re.sub(r'\$\{(\w+)\}', lambda m: self.environment_variables.get(m.group(1), m.group(0)), command)
133
119
 
134
120
  def execute_command(self, command: str):
135
121
  """Run the command in the terminal, display output in real-time, and log the captured output."""
@@ -214,7 +200,35 @@ class PipelineExecutor:
214
200
 
215
201
  self.current_node = node
216
202
  current_node = self.nodes_map[node]
217
-
203
+
204
+ # Check if we've reached the resume point
205
+ if self.resume_mode and not self.reached_resume_point:
206
+ if node == self.current_node_from_config:
207
+ self.reached_resume_point = True
208
+ node_label = current_node['data'].get('label', node)
209
+ printer.print_message(f"[INFO] Reached resume point: {node_label} - continuing execution", style="bold green")
210
+ else:
211
+ # Skip execution for this node, just traverse
212
+ if current_node['type'] == "splitterParent":
213
+ collector = None
214
+ for child in self.adj_list[node]:
215
+ if self.nodes_map[child]['data']['active']:
216
+ collector = self.dfs(child)
217
+ if collector and self.adj_list[collector]:
218
+ return self.dfs(self.adj_list[collector][0])
219
+ return
220
+ elif current_node['type'] == "splitter-child":
221
+ if current_node['data']['active'] and self.adj_list[node]:
222
+ return self.dfs(self.adj_list[node][0])
223
+ return
224
+ elif current_node['type'] == "terminal":
225
+ if self.adj_list[node]:
226
+ return self.dfs(self.adj_list[node][0])
227
+ return
228
+ elif current_node['type'] == "collector":
229
+ return node if self.adj_list[node] else None
230
+
231
+ # Normal execution (either not in resume mode or already reached resume point)
218
232
  if current_node['type'] == "splitterParent":
219
233
  collector = None
220
234
  for child in self.adj_list[node]:
@@ -297,13 +311,14 @@ class PipelineExecutor:
297
311
  current_status = self.job_status
298
312
 
299
313
  if current_status == "running":
300
- # Job is already running, resume from current node
314
+ # Job is already running, resume from start but skip until current node
301
315
  current_node_id = self.current_node_from_config
302
316
  if current_node_id and current_node_id in self.nodes_map:
303
317
  # Get the label from the current node
304
318
  current_node_label = self.nodes_map[current_node_id]['data'].get('label', current_node_id)
305
- printer.print_message(f"[INFO] Resuming execution from current node: {current_node_label}", style="bold blue")
306
- starting_node = current_node_id
319
+ printer.print_message(f"[INFO] Resuming job - will skip to node: {current_node_label}", style="bold blue")
320
+ # Start from the beginning (start_node or root)
321
+ starting_node = self.start_node or next(iter(self.root_nodes), None)
307
322
  else:
308
323
  printer.print_message("[WARNING] Current node not found, starting from beginning", style="bold yellow")
309
324
  starting_node = self.start_node or next(iter(self.root_nodes), None)
@@ -39,18 +39,23 @@ def install_singularity():
39
39
 
40
40
  printer.print_message("[bold yellow][+] Installing Singularity[/bold yellow]")
41
41
  os_release_path = Path("/etc/os-release")
42
- ubuntu_codename = None
42
+ os_codename = None
43
+
43
44
  if os_release_path.exists():
44
45
  for line in os_release_path.read_text().splitlines():
45
- if line.startswith("UBUNTU_CODENAME="):
46
- ubuntu_codename = line.split("=")[1]
46
+ if line.startswith("VERSION_CODENAME="):
47
+ os_codename = line.split("=")[1]
47
48
  break
48
49
 
49
- if not ubuntu_codename:
50
- raise ValueError("[bold red]Could not determine Ubuntu codename from /etc/os-release.[/bold red]")
50
+ if not os_codename:
51
+ raise ValueError("[bold red]Could not determine Ubuntu / Debian codename from /etc/os-release.[/bold red]")
52
+
53
+ if os_codename == "bookworm":
54
+ printer.print_message("[bold yellow][+] WARNING: Debian distros are not officially supported. Compatibility issues may arise[/bold yellow]")
55
+ os_codename = "jammy"
51
56
 
52
- singularity_url = f"https://github.com/sylabs/singularity/releases/download/v{SINGULARITY_VERSION}/singularity-ce_{SINGULARITY_VERSION}-{ubuntu_codename}_amd64.deb"
53
- temp_file = Path(f"/tmp/singularity-ce_{SINGULARITY_VERSION}-{ubuntu_codename}_amd64.deb")
57
+ singularity_url = f"https://github.com/sylabs/singularity/releases/download/v{SINGULARITY_VERSION}/singularity-ce_{SINGULARITY_VERSION}-{os_codename}_amd64.deb"
58
+ temp_file = Path(f"/tmp/singularity-ce_{SINGULARITY_VERSION}-{os_codename}_amd64.deb")
54
59
 
55
60
  printer.print_message("[bold cyan]Downloading Singularity package...[/bold cyan]")
56
61
  progress, task = printer.create_progress_bar("[cyan]Downloading...", total=100)