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.
Files changed (29) hide show
  1. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/PKG-INFO +1 -1
  2. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/pyproject.toml +1 -1
  3. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/config.py +1 -2
  4. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/decode_and_execute.py +102 -58
  5. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/executor.py +13 -1
  6. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/LICENSE.md +0 -0
  7. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/README.md +0 -0
  8. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/__init__.py +0 -0
  9. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/__main__.py +0 -0
  10. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/__init__.py +0 -0
  11. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/auth_utils.py +0 -0
  12. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/login.py +0 -0
  13. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/cli/logout.py +0 -0
  14. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/main.py +0 -0
  15. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/__init__.py +0 -0
  16. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/container_manager.py +0 -0
  17. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/pipeline/get_jobs.py +0 -0
  18. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/__init__.py +0 -0
  19. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/auth_service.py +0 -0
  20. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/base_directory.py +0 -0
  21. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/request_handler.py +0 -0
  22. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/rich_printer.py +0 -0
  23. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/services/status_updater.py +0 -0
  24. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/__init__.py +0 -0
  25. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/encryption.py +0 -0
  26. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/file_manager.py +0 -0
  27. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/logger.py +0 -0
  28. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/mock.py +0 -0
  29. {scientiflow_cli-0.4.5 → scientiflow_cli-0.4.7}/scientiflow_cli/utils/singularity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scientiflow-cli
3
- Version: 0.4.5
3
+ Version: 0.4.7
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.5"
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 = "http://127.0.0.1:8000/api"
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")
@@ -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
- return re.sub(r'\$\{(\w+)\}', lambda m: self.environment_variables.get(m.group(1), m.group(0)), command)
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
- # Start monitoring in a separate thread (non-blocking)
285
- self.monitor_background_job(futures, node_label, executor)
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
- # Wait for all background jobs to complete before marking pipeline as completed
334
- self.wait_for_background_jobs()
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: