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.
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/PKG-INFO +1 -1
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/pyproject.toml +1 -1
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/config.py +1 -1
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/main.py +34 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/decode_and_execute.py +40 -25
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/singularity.py +12 -7
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/LICENSE.md +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/README.md +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/__init__.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/__main__.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/__init__.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/auth_utils.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/login.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/cli/logout.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/__init__.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/container_manager.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/get_jobs.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/__init__.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/auth_service.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/base_directory.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/executor.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/request_handler.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/rich_printer.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/services/status_updater.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/__init__.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/encryption.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/file_manager.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/logger.py +0 -0
- {scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/utils/mock.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "scientiflow-cli"
|
|
3
|
-
version = "0.4.
|
|
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)
|
{scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/decode_and_execute.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import tempfile
|
|
3
3
|
import os
|
|
4
|
-
import re
|
|
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
|
-
|
|
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
|
|
306
|
-
|
|
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
|
-
|
|
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("
|
|
46
|
-
|
|
46
|
+
if line.startswith("VERSION_CODENAME="):
|
|
47
|
+
os_codename = line.split("=")[1]
|
|
47
48
|
break
|
|
48
49
|
|
|
49
|
-
if not
|
|
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}-{
|
|
53
|
-
temp_file = Path(f"/tmp/singularity-ce_{SINGULARITY_VERSION}-{
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scientiflow_cli-0.4.3 → scientiflow_cli-0.4.5}/scientiflow_cli/pipeline/container_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|