scientiflow-cli 0.4.5__tar.gz → 0.4.7__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.5 → scientiflow_cli-0.4.7}/PKG-INFO +1 -1
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/pyproject.toml +1 -1
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/config.py +1 -2
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/decode_and_execute.py +102 -58
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/executor.py +13 -1
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/LICENSE.md +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/README.md +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/__init__.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/__main__.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/__init__.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/auth_utils.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/login.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/logout.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/main.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/__init__.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/container_manager.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/get_jobs.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/__init__.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/auth_service.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/base_directory.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/request_handler.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/rich_printer.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/status_updater.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/__init__.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/encryption.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/file_manager.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/logger.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/mock.py +0 -0
- {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/singularity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "scientiflow-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.7"
|
|
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"
|
|
@@ -5,5 +5,4 @@ class Config:
|
|
|
5
5
|
if mode=="prod":
|
|
6
6
|
APP_BASE_URL = os.getenv("APP_BASE_URL", "https://www.backend.scientiflow.com/api")
|
|
7
7
|
elif mode=="dev":
|
|
8
|
-
APP_BASE_URL = "
|
|
9
|
-
# APP_BASE_URL = os.getenv("APP_BASE_URL", "https://www.scientiflow-backend-dev.scientiflow.com/api")
|
|
8
|
+
APP_BASE_URL = os.getenv("APP_BASE_URL", "https://www.scientiflow-backend-dev.scientiflow.com/api")
|
{scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/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, shlex
|
|
5
5
|
import multiprocessing
|
|
6
6
|
import threading
|
|
7
7
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
@@ -12,6 +12,85 @@ from scientiflow_cli.services.rich_printer import RichPrinter
|
|
|
12
12
|
|
|
13
13
|
printer = RichPrinter()
|
|
14
14
|
|
|
15
|
+
# Global background job tracker
|
|
16
|
+
class GlobalBackgroundJobTracker:
|
|
17
|
+
_instance = None
|
|
18
|
+
_lock = threading.Lock()
|
|
19
|
+
|
|
20
|
+
def __new__(cls):
|
|
21
|
+
if cls._instance is None:
|
|
22
|
+
with cls._lock:
|
|
23
|
+
if cls._instance is None:
|
|
24
|
+
cls._instance = super().__new__(cls)
|
|
25
|
+
cls._instance._initialize()
|
|
26
|
+
return cls._instance
|
|
27
|
+
|
|
28
|
+
def _initialize(self):
|
|
29
|
+
self.background_executors = []
|
|
30
|
+
self.background_jobs_count = 0
|
|
31
|
+
self.background_jobs_completed = 0
|
|
32
|
+
self.background_jobs_lock = threading.Lock()
|
|
33
|
+
|
|
34
|
+
def register_background_job(self, executor, futures, node_label, log_file_path):
|
|
35
|
+
"""Register a background job for global tracking."""
|
|
36
|
+
with self.background_jobs_lock:
|
|
37
|
+
self.background_jobs_count += 1
|
|
38
|
+
self.background_executors.append(executor)
|
|
39
|
+
|
|
40
|
+
# Start monitoring in a separate thread
|
|
41
|
+
monitor_thread = threading.Thread(
|
|
42
|
+
target=self._monitor_job,
|
|
43
|
+
args=(futures, node_label, executor, log_file_path),
|
|
44
|
+
daemon=True
|
|
45
|
+
)
|
|
46
|
+
monitor_thread.start()
|
|
47
|
+
|
|
48
|
+
def _monitor_job(self, futures, node_label, executor, log_file_path):
|
|
49
|
+
"""Monitor background job completion."""
|
|
50
|
+
all_successful = True
|
|
51
|
+
for future in as_completed(futures):
|
|
52
|
+
success = future.result()
|
|
53
|
+
if not success:
|
|
54
|
+
all_successful = False
|
|
55
|
+
|
|
56
|
+
if not all_successful:
|
|
57
|
+
with open(log_file_path, 'a') as f:
|
|
58
|
+
f.write(f"[ERROR] Background job {node_label} failed\n")
|
|
59
|
+
printer.print_message(f"[BACKGROUND JOB] {node_label} Failed - some commands in background job failed", style="bold red")
|
|
60
|
+
else:
|
|
61
|
+
printer.print_message(f"[BACKGROUND JOB] {node_label} Execution completed in the background", style="bold green")
|
|
62
|
+
|
|
63
|
+
# Clean up executor
|
|
64
|
+
executor.shutdown(wait=False)
|
|
65
|
+
with self.background_jobs_lock:
|
|
66
|
+
if executor in self.background_executors:
|
|
67
|
+
self.background_executors.remove(executor)
|
|
68
|
+
self.background_jobs_completed += 1
|
|
69
|
+
|
|
70
|
+
def wait_for_all_jobs(self):
|
|
71
|
+
"""Wait for all background jobs to complete."""
|
|
72
|
+
import time
|
|
73
|
+
if self.background_jobs_count > 0:
|
|
74
|
+
printer.print_message(f"[INFO] Waiting for {self.background_jobs_count} background job(s) to complete...", style="bold yellow")
|
|
75
|
+
|
|
76
|
+
while True:
|
|
77
|
+
with self.background_jobs_lock:
|
|
78
|
+
if self.background_jobs_completed >= self.background_jobs_count:
|
|
79
|
+
break
|
|
80
|
+
time.sleep(0.5) # Check every 500ms
|
|
81
|
+
|
|
82
|
+
printer.print_message("[INFO] All background jobs completed.", style="bold green")
|
|
83
|
+
|
|
84
|
+
def reset(self):
|
|
85
|
+
"""Reset the tracker for a new execution cycle."""
|
|
86
|
+
with self.background_jobs_lock:
|
|
87
|
+
self.background_executors = []
|
|
88
|
+
self.background_jobs_count = 0
|
|
89
|
+
self.background_jobs_completed = 0
|
|
90
|
+
|
|
91
|
+
# Global tracker instance
|
|
92
|
+
global_bg_tracker = GlobalBackgroundJobTracker()
|
|
93
|
+
|
|
15
94
|
def execute_background_command_standalone(command: str, log_file_path: str):
|
|
16
95
|
"""Execute a command in background without real-time output display - standalone function for multiprocessing."""
|
|
17
96
|
try:
|
|
@@ -51,10 +130,6 @@ class PipelineExecutor:
|
|
|
51
130
|
self.current_node = None
|
|
52
131
|
self.job_status = job_status
|
|
53
132
|
self.current_node_from_config = current_node_from_config
|
|
54
|
-
self.background_executors = [] # Keep track of background executors
|
|
55
|
-
self.background_jobs_count = 0 # Track number of active background jobs
|
|
56
|
-
self.background_jobs_completed = 0 # Track completed background jobs
|
|
57
|
-
self.background_jobs_lock = threading.Lock() # Thread-safe counter updates
|
|
58
133
|
|
|
59
134
|
# For resuming: flag to track if we've reached the resume point
|
|
60
135
|
self.resume_mode = (job_status == "running" and current_node_from_config is not None)
|
|
@@ -114,9 +189,23 @@ class PipelineExecutor:
|
|
|
114
189
|
print(f"[ERROR] Failed to update terminal output: {e}")
|
|
115
190
|
|
|
116
191
|
def replace_variables(self, command: str) -> str:
|
|
117
|
-
"""Replace placeholders like ${VAR} with environment values."""
|
|
118
|
-
|
|
119
|
-
|
|
192
|
+
# """Replace placeholders like ${VAR} with environment values."""
|
|
193
|
+
# print(self.environment_variables)
|
|
194
|
+
# return re.sub(r'\$\{(\w+)\}', lambda m: self.environment_variables.get(m.group(1), m.group(0)), command)
|
|
195
|
+
"""Replace placeholders like ${VAR} with environment values.
|
|
196
|
+
- If value is a list: safely join into space-separated arguments (quoted).
|
|
197
|
+
- Otherwise: use the old behavior (direct substitution).
|
|
198
|
+
"""
|
|
199
|
+
def replacer(match):
|
|
200
|
+
key = match.group(1)
|
|
201
|
+
value = self.environment_variables.get(key, match.group(0))
|
|
202
|
+
# ✅ Special handling for lists
|
|
203
|
+
if isinstance(value, list):
|
|
204
|
+
return " ".join(shlex.quote(str(v)) for v in value)
|
|
205
|
+
# ✅ Default: keep original behavior for strings, numbers, None
|
|
206
|
+
return self.environment_variables.get(key, match.group(0))
|
|
207
|
+
return re.sub(r'\$\{(\w+)\}', replacer, command)
|
|
208
|
+
|
|
120
209
|
def execute_command(self, command: str):
|
|
121
210
|
"""Run the command in the terminal, display output in real-time, and log the captured output."""
|
|
122
211
|
import sys
|
|
@@ -151,47 +240,7 @@ class PipelineExecutor:
|
|
|
151
240
|
self.update_terminal_output()
|
|
152
241
|
raise SystemExit("[ERROR] Pipeline execution terminated due to an unexpected error.")
|
|
153
242
|
|
|
154
|
-
def monitor_background_job(self, futures, node_label, executor):
|
|
155
|
-
"""Monitor background job completion in a separate thread."""
|
|
156
|
-
def monitor():
|
|
157
|
-
all_successful = True
|
|
158
|
-
for future in as_completed(futures):
|
|
159
|
-
success = future.result()
|
|
160
|
-
if not success:
|
|
161
|
-
all_successful = False
|
|
162
|
-
|
|
163
|
-
if not all_successful:
|
|
164
|
-
self.log_output(f"[ERROR] Background job {node_label} failed")
|
|
165
|
-
printer.print_message(f"[BACKGROUND JOB] {node_label} Failed - some commands in background job failed", style="bold red")
|
|
166
|
-
else:
|
|
167
|
-
printer.print_message(f"[BACKGROUND JOB] {node_label} Execution completed in the background", style="bold green")
|
|
168
|
-
|
|
169
|
-
# Clean up executor
|
|
170
|
-
executor.shutdown(wait=False)
|
|
171
|
-
if executor in self.background_executors:
|
|
172
|
-
self.background_executors.remove(executor)
|
|
173
|
-
|
|
174
|
-
# Update background job completion count
|
|
175
|
-
with self.background_jobs_lock:
|
|
176
|
-
self.background_jobs_completed += 1
|
|
177
|
-
|
|
178
|
-
# Start monitoring thread
|
|
179
|
-
monitor_thread = threading.Thread(target=monitor, daemon=True)
|
|
180
|
-
monitor_thread.start()
|
|
181
243
|
|
|
182
|
-
def wait_for_background_jobs(self):
|
|
183
|
-
"""Wait for all background jobs to complete."""
|
|
184
|
-
import time
|
|
185
|
-
if self.background_jobs_count > 0:
|
|
186
|
-
printer.print_message(f"[INFO] Waiting for {self.background_jobs_count} background job(s) to complete...", style="bold yellow")
|
|
187
|
-
|
|
188
|
-
while True:
|
|
189
|
-
with self.background_jobs_lock:
|
|
190
|
-
if self.background_jobs_completed >= self.background_jobs_count:
|
|
191
|
-
break
|
|
192
|
-
time.sleep(0.5) # Check every 500ms
|
|
193
|
-
|
|
194
|
-
printer.print_message("[INFO] All background jobs completed.", style="bold green")
|
|
195
244
|
|
|
196
245
|
def dfs(self, node: str):
|
|
197
246
|
"""Perform Depth-First Search (DFS) for executing pipeline nodes."""
|
|
@@ -270,19 +319,14 @@ class PipelineExecutor:
|
|
|
270
319
|
|
|
271
320
|
# Execute commands in background using ProcessPoolExecutor (non-blocking)
|
|
272
321
|
if command_list:
|
|
273
|
-
# Increment background jobs counter
|
|
274
|
-
with self.background_jobs_lock:
|
|
275
|
-
self.background_jobs_count += 1
|
|
276
|
-
|
|
277
322
|
executor = ProcessPoolExecutor(max_workers=numberOfThreads)
|
|
278
|
-
self.background_executors.append(executor) # Keep reference to prevent garbage collection
|
|
279
323
|
futures = []
|
|
280
324
|
for cmd in command_list:
|
|
281
325
|
future = executor.submit(execute_background_command_standalone, cmd, self.log_file_path)
|
|
282
326
|
futures.append(future)
|
|
283
327
|
|
|
284
|
-
#
|
|
285
|
-
|
|
328
|
+
# Register with global tracker (non-blocking)
|
|
329
|
+
global_bg_tracker.register_background_job(executor, futures, node_label, self.log_file_path)
|
|
286
330
|
|
|
287
331
|
# Don't wait for completion, immediately continue to next node
|
|
288
332
|
else:
|
|
@@ -330,9 +374,9 @@ class PipelineExecutor:
|
|
|
330
374
|
if starting_node:
|
|
331
375
|
self.dfs(starting_node)
|
|
332
376
|
|
|
333
|
-
#
|
|
334
|
-
|
|
335
|
-
|
|
377
|
+
# Don't wait for background jobs here - let them continue across multiple jobs
|
|
378
|
+
# Background jobs will be waited for at the end of all job executions
|
|
379
|
+
|
|
336
380
|
update_job_status(self.project_job_id, "completed")
|
|
337
381
|
update_stopped_at_node(self.project_id, self.project_job_id, self.current_node)
|
|
338
382
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
2
|
import asyncio
|
|
3
3
|
from scientiflow_cli.pipeline.get_jobs import get_jobs
|
|
4
|
-
from scientiflow_cli.pipeline.decode_and_execute import decode_and_execute_pipeline
|
|
4
|
+
from scientiflow_cli.pipeline.decode_and_execute import decode_and_execute_pipeline, global_bg_tracker
|
|
5
5
|
from scientiflow_cli.pipeline.container_manager import get_job_containers
|
|
6
6
|
from scientiflow_cli.utils.file_manager import create_job_dirs, get_job_files
|
|
7
7
|
from scientiflow_cli.services.rich_printer import RichPrinter
|
|
@@ -51,6 +51,9 @@ def execute_jobs(job_ids: list[int] = None, parallel: bool = False) -> None:
|
|
|
51
51
|
# Execute jobs synchronously
|
|
52
52
|
for job in jobs_to_execute:
|
|
53
53
|
execute_single_job(job)
|
|
54
|
+
|
|
55
|
+
# Wait for all background jobs from all executed jobs to complete
|
|
56
|
+
global_bg_tracker.wait_for_all_jobs()
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
def execute_jobs_sync(job_ids: list[int] = None) -> None:
|
|
@@ -69,6 +72,9 @@ def execute_jobs_sync(job_ids: list[int] = None) -> None:
|
|
|
69
72
|
printer.print_error(f"No job found with ID: {job_id}")
|
|
70
73
|
continue
|
|
71
74
|
execute_single_job(job_dict[job_id])
|
|
75
|
+
|
|
76
|
+
# Wait for all background jobs from all executed jobs to complete
|
|
77
|
+
global_bg_tracker.wait_for_all_jobs()
|
|
72
78
|
|
|
73
79
|
|
|
74
80
|
def sort_jobs_by_id(all_pending_jobs: list[dict]) -> list[dict]:
|
|
@@ -117,6 +123,9 @@ def execute_job_id(job_id: int) -> None:
|
|
|
117
123
|
|
|
118
124
|
execute_single_job(job_dict[job_id])
|
|
119
125
|
|
|
126
|
+
# Wait for all background jobs to complete
|
|
127
|
+
global_bg_tracker.wait_for_all_jobs()
|
|
128
|
+
|
|
120
129
|
|
|
121
130
|
|
|
122
131
|
async def execute_async(jobs: list[dict]) -> None:
|
|
@@ -150,6 +159,9 @@ async def execute_async(jobs: list[dict]) -> None:
|
|
|
150
159
|
|
|
151
160
|
await asyncio.gather(*running_jobs) # Wait for all jobs to complete
|
|
152
161
|
printer.print_success("[ASYNC COMPLETE] All jobs finished!")
|
|
162
|
+
|
|
163
|
+
# Wait for all background jobs from all executed jobs to complete
|
|
164
|
+
global_bg_tracker.wait_for_all_jobs()
|
|
153
165
|
|
|
154
166
|
|
|
155
167
|
def execute_single_job(job: dict) -> None:
|
|
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
|
{scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/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
|