scitex 2.16.2__py3-none-any.whl → 2.17.3__py3-none-any.whl

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 (70) hide show
  1. scitex/_dev/__init__.py +122 -0
  2. scitex/_dev/_config.py +391 -0
  3. scitex/_dev/_dashboard/__init__.py +11 -0
  4. scitex/_dev/_dashboard/_app.py +89 -0
  5. scitex/_dev/_dashboard/_routes.py +169 -0
  6. scitex/_dev/_dashboard/_scripts.py +301 -0
  7. scitex/_dev/_dashboard/_styles.py +205 -0
  8. scitex/_dev/_dashboard/_templates.py +117 -0
  9. scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
  10. scitex/_dev/_ecosystem.py +109 -0
  11. scitex/_dev/_github.py +360 -0
  12. scitex/_dev/_mcp/__init__.py +11 -0
  13. scitex/_dev/_mcp/handlers.py +182 -0
  14. scitex/_dev/_ssh.py +332 -0
  15. scitex/_dev/_versions.py +272 -0
  16. scitex/_mcp_resources/_cheatsheet.py +1 -1
  17. scitex/_mcp_resources/_modules.py +1 -1
  18. scitex/_mcp_tools/__init__.py +4 -0
  19. scitex/_mcp_tools/dev.py +186 -0
  20. scitex/_mcp_tools/verify.py +256 -0
  21. scitex/audio/_audio_check.py +84 -41
  22. scitex/cli/capture.py +45 -22
  23. scitex/cli/dev.py +494 -0
  24. scitex/cli/main.py +4 -0
  25. scitex/cli/stats.py +48 -20
  26. scitex/cli/verify.py +473 -0
  27. scitex/dev/plt/__init__.py +1 -1
  28. scitex/dev/plt/mpl/get_dir_ax.py +1 -1
  29. scitex/dev/plt/mpl/get_signatures.py +1 -1
  30. scitex/dev/plt/mpl/get_signatures_details.py +1 -1
  31. scitex/io/_load.py +8 -1
  32. scitex/io/_save.py +12 -0
  33. scitex/plt/__init__.py +16 -6
  34. scitex/session/README.md +2 -2
  35. scitex/session/__init__.py +1 -0
  36. scitex/session/_decorator.py +57 -33
  37. scitex/session/_lifecycle/__init__.py +23 -0
  38. scitex/session/_lifecycle/_close.py +225 -0
  39. scitex/session/_lifecycle/_config.py +112 -0
  40. scitex/session/_lifecycle/_matplotlib.py +83 -0
  41. scitex/session/_lifecycle/_start.py +246 -0
  42. scitex/session/_lifecycle/_utils.py +186 -0
  43. scitex/session/_manager.py +40 -3
  44. scitex/session/template.py +1 -1
  45. scitex/template/__init__.py +18 -1
  46. scitex/template/_templates/plt.py +1 -1
  47. scitex/template/_templates/session.py +1 -1
  48. scitex/template/clone_research_minimal.py +111 -0
  49. scitex/verify/README.md +300 -0
  50. scitex/verify/__init__.py +208 -0
  51. scitex/verify/_chain.py +369 -0
  52. scitex/verify/_db.py +600 -0
  53. scitex/verify/_hash.py +187 -0
  54. scitex/verify/_integration.py +127 -0
  55. scitex/verify/_rerun.py +253 -0
  56. scitex/verify/_tracker.py +330 -0
  57. scitex/verify/_visualize.py +44 -0
  58. scitex/verify/_viz/__init__.py +38 -0
  59. scitex/verify/_viz/_colors.py +84 -0
  60. scitex/verify/_viz/_format.py +302 -0
  61. scitex/verify/_viz/_json.py +192 -0
  62. scitex/verify/_viz/_mermaid.py +440 -0
  63. scitex/verify/_viz/_templates.py +246 -0
  64. scitex/verify/_viz/_utils.py +56 -0
  65. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/METADATA +2 -1
  66. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/RECORD +69 -28
  67. scitex/session/_lifecycle.py +0 -827
  68. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/WHEEL +0 -0
  69. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/entry_points.txt +0 -0
  70. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # Timestamp: "2026-02-01 08:38:19 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_decorator.py
5
+
3
6
  # Timestamp: "2025-11-05"
4
- # File: /home/ywatanabe/proj/scitex-code/src/scitex/session/_decorator.py
5
- # ----------------------------------------
6
7
  """Session decorator for scitex.
7
8
 
8
9
  Provides @stx.session decorator that automatically:
@@ -16,10 +17,12 @@ import functools
16
17
  import inspect
17
18
  import argparse
18
19
  from pathlib import Path
19
- from typing import Callable, Any, get_type_hints
20
+ from typing import Callable
21
+ from typing import Any, get_type_hints
20
22
  import sys as sys_module
21
23
 
22
- from ._lifecycle import start, close
24
+ from ._lifecycle import start
25
+ from ._lifecycle import close
23
26
  from scitex.logging import getLogger
24
27
  from . import INJECTED # Use local INJECTED from session module
25
28
 
@@ -79,7 +82,7 @@ def session(
79
82
  # - CONFIG: Session configuration dict
80
83
  # - plt: Matplotlib pyplot (configured for session)
81
84
  # - COLORS: Custom Colors
82
- # - rng: RandomStateManager (fixes seeds, creates named generators)
85
+ # - rngg: RandomStateManager (fixes seeds, creates named generators)
83
86
  logger.info(f"Session ID: {CONFIG['ID']}")
84
87
  logger.info(f"Output directory: {CONFIG['SDIR_RUN']}")
85
88
  # ... training code ...
@@ -100,8 +103,8 @@ def session(
100
103
  - CONFIG (dict): Session configuration with ID, SDIR, paths, etc.
101
104
  - plt (module): matplotlib.pyplot configured with session settings
102
105
  - COLORS (CustomColors): Custom Colors for consistent plotting
103
- - rng (RandomStateManager): Manages reproducibility by fixing global seeds
104
- and creating named generators via rng("name")
106
+ - rngg (RandomStateManager): Manages reproducibility by fixing global seeds
107
+ and creating named generators via rngg("name")
105
108
  """
106
109
 
107
110
  def decorator(func: Callable) -> Callable:
@@ -157,13 +160,17 @@ def _run_with_session(
157
160
 
158
161
  # Clean up INJECTED sentinels from args before passing to session
159
162
  cleaned_args = argparse.Namespace(
160
- **{k: v for k, v in vars(args).items() if not isinstance(v, type(INJECTED))}
163
+ **{
164
+ k: v
165
+ for k, v in vars(args).items()
166
+ if not isinstance(v, type(INJECTED))
167
+ }
161
168
  )
162
169
 
163
170
  # Start session
164
171
  import matplotlib.pyplot as plt
165
172
 
166
- CONFIG, stdout, stderr, plt, COLORS, rng = start(
173
+ CONFIG, stdout, stderr, plt, COLORS, rngg = start(
167
174
  sys=sys_module,
168
175
  plt=plt,
169
176
  args=cleaned_args,
@@ -182,21 +189,33 @@ def _run_with_session(
182
189
  func_globals["CONFIG"] = CONFIG
183
190
  func_globals["plt"] = plt
184
191
  func_globals["COLORS"] = COLORS
185
- func_globals["rng"] = rng
192
+ func_globals["rngg"] = rngg
186
193
  func_globals["logger"] = script_logger
187
194
 
188
195
  # Log injected globals for user awareness (only in verbose mode)
189
196
  if verbose:
190
197
  _decorator_logger.info("=" * 60)
191
- _decorator_logger.info("Injected Global Variables (available in your function):")
198
+ _decorator_logger.info(
199
+ "Injected Global Variables (available in your function):"
200
+ )
192
201
  _decorator_logger.info(" • CONFIG - Session configuration dict")
193
202
  _decorator_logger.info(f" - CONFIG['ID']: {CONFIG['ID']}")
194
- _decorator_logger.info(f" - CONFIG['SDIR_RUN']: {CONFIG['SDIR_RUN']}")
203
+ _decorator_logger.info(
204
+ f" - CONFIG['SDIR_RUN']: {CONFIG['SDIR_RUN']}"
205
+ )
195
206
  _decorator_logger.info(f" - CONFIG['PID']: {CONFIG['PID']}")
196
- _decorator_logger.info(" • plt - matplotlib.pyplot (configured for session)")
197
- _decorator_logger.info(" • COLORS - CustomColors (for consistent plotting)")
198
- _decorator_logger.info(" • rng - RandomStateManager (for reproducibility)")
199
- _decorator_logger.info(" • logger - SciTeX logger (configured for your script)")
207
+ _decorator_logger.info(
208
+ " • plt - matplotlib.pyplot (configured for session)"
209
+ )
210
+ _decorator_logger.info(
211
+ " • COLORS - CustomColors (for consistent plotting)"
212
+ )
213
+ _decorator_logger.info(
214
+ " • rngg - RandomStateManager (for reproducibility)"
215
+ )
216
+ _decorator_logger.info(
217
+ " • logger - SciTeX logger (configured for your script)"
218
+ )
200
219
  _decorator_logger.info("=" * 60)
201
220
 
202
221
  # Run function
@@ -216,7 +235,7 @@ def _run_with_session(
216
235
  "CONFIG": CONFIG,
217
236
  "plt": plt,
218
237
  "COLORS": COLORS,
219
- "rng": rng,
238
+ "rngg": rngg,
220
239
  "logger": script_logger,
221
240
  }
222
241
 
@@ -238,8 +257,12 @@ def _run_with_session(
238
257
 
239
258
  # Log injected arguments summary (only in verbose mode)
240
259
  if verbose:
241
- args_summary = {k: type(v).__name__ for k, v in filtered_kwargs.items()}
242
- _decorator_logger.info(f"Running {func.__name__} with injected parameters:")
260
+ args_summary = {
261
+ k: type(v).__name__ for k, v in filtered_kwargs.items()
262
+ }
263
+ _decorator_logger.info(
264
+ f"Running {func.__name__} with injected parameters:"
265
+ )
243
266
  _decorator_logger.info(args_summary, pprint=True, indent=2)
244
267
 
245
268
  # Execute function
@@ -252,7 +275,9 @@ def _run_with_session(
252
275
  exit_status = 0
253
276
 
254
277
  except Exception as e:
255
- _decorator_logger.error(f"Error in {func.__name__}: {e}", exc_info=True)
278
+ _decorator_logger.error(
279
+ f"Error in {func.__name__}: {e}", exc_info=True
280
+ )
256
281
  exit_status = 1
257
282
  raise
258
283
 
@@ -359,15 +384,11 @@ def _create_parser(func: Callable) -> argparse.ArgumentParser:
359
384
  # If we can't load the YAML files, just show error
360
385
  config_status = " CONFIG from YAML files:\n (unable to load at help-time, will be available at runtime)"
361
386
  else:
362
- config_status = (
363
- " CONFIG from YAML files:\n (no .yaml files found)"
364
- )
387
+ config_status = " CONFIG from YAML files:\n (no .yaml files found)"
365
388
  else:
366
389
  config_status = " CONFIG from YAML files:\n (./config/ directory not found)"
367
390
  except:
368
- config_status = (
369
- " CONFIG from YAML files:\n (unable to check at help-time)"
370
- )
391
+ config_status = " CONFIG from YAML files:\n (unable to check at help-time)"
371
392
 
372
393
  # Get available color keys
373
394
  try:
@@ -380,7 +401,9 @@ def _create_parser(func: Callable) -> argparse.ArgumentParser:
380
401
  color_keys = ", ".join(f"'{k}'" for k in sorted_keys)
381
402
  except Exception as e:
382
403
  # Fallback if configure_mpl fails
383
- color_keys = "'blue', 'red', 'green', 'yellow', 'purple', 'orange', ..."
404
+ color_keys = (
405
+ "'blue', 'red', 'green', 'yellow', 'purple', 'orange', ..."
406
+ )
384
407
 
385
408
  # Create parser with epilog documenting injected globals with actual values
386
409
  epilog = f"""
@@ -419,7 +442,7 @@ Global Variables Injected by @session Decorator:
419
442
  plt.plot(x, y, color=COLORS.blue)
420
443
  plt.plot(x, y, color=COLORS['blue'])
421
444
 
422
- rng (RandomStateManager)
445
+ rngg (RandomStateManager)
423
446
  Manages reproducible randomness
424
447
 
425
448
  logger (SciTeXLogger)
@@ -545,9 +568,11 @@ def _add_argument(
545
568
  choices_str = f", choices: {choices}" if choices else ""
546
569
  kwargs = {
547
570
  "type": param_type,
548
- "help": f"(default: {default}{choices_str})"
549
- if has_default
550
- else f"(required{choices_str})",
571
+ "help": (
572
+ f"(default: {default}{choices_str})"
573
+ if has_default
574
+ else f"(required{choices_str})"
575
+ ),
551
576
  }
552
577
 
553
578
  if choices:
@@ -596,7 +621,7 @@ def run(func: Callable, parse_args: Callable = None, **session_kwargs) -> Any:
596
621
  # Start session
597
622
  import matplotlib.pyplot as plt
598
623
 
599
- CONFIG, stdout, stderr, plt, COLORS, rng = start(
624
+ CONFIG, stdout, stderr, plt, COLORS, rngg = start(
600
625
  sys=sys_module,
601
626
  plt=plt,
602
627
  args=args,
@@ -627,5 +652,4 @@ def run(func: Callable, parse_args: Callable = None, **session_kwargs) -> Any:
627
652
 
628
653
  return exit_status
629
654
 
630
-
631
655
  # EOF
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/__init__.py
4
+ """Session lifecycle management - refactored subpackage.
5
+
6
+ This package contains the split modules from the original _lifecycle.py:
7
+ - _start.py: Session start function
8
+ - _close.py: Session close and running2finished functions
9
+ - _config.py: Configuration setup
10
+ - _matplotlib.py: Matplotlib configuration
11
+ - _utils.py: Utility functions
12
+ """
13
+
14
+ from ._close import close, running2finished
15
+ from ._start import start
16
+
17
+ __all__ = [
18
+ "start",
19
+ "close",
20
+ "running2finished",
21
+ ]
22
+
23
+ # EOF
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_close.py
4
+ """Session close functions."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import os as _os
10
+ import shutil
11
+ import time
12
+ from glob import glob as _glob
13
+ from pathlib import Path
14
+
15
+ from scitex.logging import getLogger
16
+ from scitex.utils._notify import notify as scitex_utils_notify
17
+
18
+ from .._manager import get_global_session_manager
19
+ from ._config import save_configs
20
+ from ._utils import args_to_str, escape_ansi_from_log_files, process_timestamp
21
+
22
+ logger = getLogger(__name__)
23
+
24
+
25
+ def running2finished(CONFIG, exit_status=None, remove_src_dir=True, max_wait=60):
26
+ """Move session from RUNNING to FINISHED directory.
27
+
28
+ Parameters
29
+ ----------
30
+ CONFIG : dict
31
+ Session configuration dictionary
32
+ exit_status : int, optional
33
+ Exit status code (0=success, 1=error, None=finished)
34
+ remove_src_dir : bool, default=True
35
+ Whether to remove source directory after copy
36
+ max_wait : int, default=60
37
+ Maximum seconds to wait for copy operation
38
+
39
+ Returns
40
+ -------
41
+ dict
42
+ Updated configuration with new SDIR
43
+ """
44
+ if exit_status == 0:
45
+ dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED_SUCCESS/")
46
+ elif exit_status == 1:
47
+ dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED_ERROR/")
48
+ else:
49
+ dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED/")
50
+
51
+ src_dir = str(CONFIG["SDIR_RUN"])
52
+ _os.makedirs(dest_dir, exist_ok=True)
53
+ try:
54
+ # Copy files individually
55
+ for item in _os.listdir(src_dir):
56
+ s = _os.path.join(src_dir, item)
57
+ d = _os.path.join(dest_dir, item)
58
+ if _os.path.isdir(s):
59
+ shutil.copytree(s, d)
60
+ else:
61
+ shutil.copy2(s, d)
62
+
63
+ start_time = time.time()
64
+ while not _os.path.exists(dest_dir) and time.time() - start_time < max_wait:
65
+ time.sleep(0.1)
66
+ if _os.path.exists(dest_dir):
67
+ print()
68
+ logger.success(
69
+ f"Congratulations! The script completed: {dest_dir}",
70
+ )
71
+
72
+ if remove_src_dir:
73
+ shutil.rmtree(src_dir)
74
+
75
+ # Cleanup RUNNING when empty
76
+ running_base = os.path.dirname(src_dir.rstrip("/"))
77
+ if os.path.basename(running_base) == "RUNNING":
78
+ try:
79
+ os.rmdir(running_base)
80
+ except OSError:
81
+ pass
82
+
83
+ else:
84
+ print(f"Copy operation timed out after {max_wait} seconds")
85
+
86
+ CONFIG["SDIR_RUN"] = Path(dest_dir)
87
+ except Exception as e:
88
+ print(e)
89
+
90
+ finally:
91
+ return CONFIG
92
+
93
+
94
+ def close(CONFIG, message=":)", notify=False, verbose=True, exit_status=None):
95
+ """Close experiment session and finalize logging.
96
+
97
+ Parameters
98
+ ----------
99
+ CONFIG : DotDict
100
+ Configuration dictionary from start()
101
+ message : str, default=':)'
102
+ Completion message
103
+ notify : bool, default=False
104
+ Whether to send notification
105
+ verbose : bool, default=True
106
+ Whether to print verbose output
107
+ exit_status : int, optional
108
+ Exit status code (0=success, 1=error, None=finished)
109
+ """
110
+ # Stop verification tracking first
111
+ _stop_verification(exit_status)
112
+
113
+ sys = None
114
+ try:
115
+ CONFIG.EXIT_STATUS = exit_status
116
+ CONFIG = CONFIG.to_dict()
117
+ CONFIG = process_timestamp(CONFIG, verbose=verbose)
118
+ sys = CONFIG.pop("_sys", None)
119
+
120
+ # CRITICAL: Close matplotlib BEFORE closing streams to prevent segfault
121
+ _cleanup_matplotlib(verbose)
122
+
123
+ save_configs(CONFIG)
124
+
125
+ # RUNNING to FINISHED
126
+ CONFIG = running2finished(CONFIG, exit_status=exit_status)
127
+
128
+ # ANSI code escape
129
+ log_files = _glob(str(CONFIG["SDIR_RUN"]) + "logs/*.log")
130
+ escape_ansi_from_log_files(log_files)
131
+
132
+ if CONFIG.get("ARGS"):
133
+ message += f"\n{args_to_str(CONFIG.get('ARGS'))}"
134
+
135
+ if notify:
136
+ try:
137
+ message = (
138
+ "[DEBUG]\n" + str(message)
139
+ if CONFIG.get("DEBUG", False)
140
+ else str(message)
141
+ )
142
+ scitex_utils_notify(
143
+ message=message,
144
+ ID=CONFIG["ID"],
145
+ file=CONFIG.get("FILE"),
146
+ attachment_paths=log_files,
147
+ verbose=verbose,
148
+ )
149
+ except Exception as e:
150
+ print(e)
151
+
152
+ # Close session
153
+ session_manager = get_global_session_manager()
154
+ session_manager.close_session(CONFIG["ID"])
155
+
156
+ finally:
157
+ # Only close if they're custom file objects (Tee objects)
158
+ if sys:
159
+ _close_streams(sys)
160
+
161
+
162
+ def _cleanup_matplotlib(verbose: bool) -> None:
163
+ """Clean up matplotlib resources."""
164
+ try:
165
+ import gc
166
+
167
+ import matplotlib
168
+ import matplotlib.pyplot as plt
169
+
170
+ # Close all figures
171
+ plt.close("all")
172
+
173
+ # CRITICAL: Unregister matplotlib's atexit handlers to prevent segfault
174
+ try:
175
+ if hasattr(matplotlib, "_pylab_helpers"):
176
+ matplotlib._pylab_helpers.Gcf.destroy_all()
177
+
178
+ if hasattr(plt, "get_fignums"):
179
+ for fignum in plt.get_fignums():
180
+ plt.close(fignum)
181
+
182
+ except Exception:
183
+ pass
184
+
185
+ # Force garbage collection
186
+ gc.collect()
187
+
188
+ if verbose:
189
+ logger.info("Matplotlib cleanup completed")
190
+
191
+ except Exception as e:
192
+ if verbose:
193
+ logger.warning(f"Could not close matplotlib: {e}")
194
+
195
+
196
+ def _close_streams(sys) -> None:
197
+ """Close tee-wrapped streams."""
198
+ try:
199
+ # First, flush all outputs
200
+ if hasattr(sys, "stdout") and hasattr(sys.stdout, "flush"):
201
+ sys.stdout.flush()
202
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "flush"):
203
+ sys.stderr.flush()
204
+
205
+ # Then close Tee objects
206
+ if hasattr(sys, "stdout") and hasattr(sys.stdout, "_log_file"):
207
+ sys.stdout.close()
208
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "_log_file"):
209
+ sys.stderr.close()
210
+ except Exception:
211
+ pass
212
+
213
+
214
+ def _stop_verification(exit_status: int) -> None:
215
+ """Stop verification tracking for this session."""
216
+ try:
217
+ from scitex.verify import on_session_close
218
+
219
+ status = "success" if exit_status == 0 else "failed"
220
+ on_session_close(status=status, exit_code=exit_status or 0)
221
+ except Exception:
222
+ pass
223
+
224
+
225
+ # EOF
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_config.py
4
+ """Configuration setup for session lifecycle."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Dict
11
+
12
+ from scitex.logging import getLogger
13
+
14
+ logger = getLogger(__name__)
15
+
16
+
17
+ def setup_configs(
18
+ IS_DEBUG: bool,
19
+ ID: str,
20
+ PID: int,
21
+ file: str,
22
+ sdir: str,
23
+ relative_sdir: str,
24
+ verbose: bool,
25
+ ) -> Dict[str, Any]:
26
+ """Setup configuration dictionary with basic parameters.
27
+
28
+ Parameters
29
+ ----------
30
+ IS_DEBUG : bool
31
+ Debug mode flag
32
+ ID : str
33
+ Unique identifier
34
+ PID : int
35
+ Process ID
36
+ file : str
37
+ File path
38
+ sdir : str
39
+ Save directory path
40
+ relative_sdir : str
41
+ Relative save directory path
42
+ verbose : bool
43
+ Verbosity flag
44
+
45
+ Returns
46
+ -------
47
+ dict
48
+ Configuration dictionary
49
+ """
50
+ # Calculate SDIR_OUT (base output directory)
51
+ # sdir format: /path/to/script_out/RUNNING/ID/
52
+ sdir_path = Path(sdir) if sdir else None
53
+ if sdir_path:
54
+ # Remove /RUNNING/ID/ to get base output dir
55
+ parts = sdir_path.parts
56
+ if "RUNNING" in parts:
57
+ running_idx = parts.index("RUNNING")
58
+ sdir_out = Path(*parts[:running_idx])
59
+ else:
60
+ sdir_out = sdir_path.parent
61
+ else:
62
+ sdir_out = None
63
+
64
+ # Load YAML configs from ./config/*.yaml
65
+ from scitex.io._load_configs import load_configs
66
+
67
+ CONFIGS = load_configs(IS_DEBUG).to_dict()
68
+
69
+ # Add session-specific config with clean structure (Path objects only)
70
+ CONFIGS.update(
71
+ {
72
+ "ID": ID,
73
+ "PID": PID,
74
+ "START_DATETIME": datetime.now(),
75
+ "FILE": Path(file) if file else None,
76
+ "SDIR_OUT": sdir_out,
77
+ "SDIR_RUN": sdir_path,
78
+ }
79
+ )
80
+ return CONFIGS
81
+
82
+
83
+ def save_configs(CONFIG) -> None:
84
+ """Save configuration to files.
85
+
86
+ Note: track=False prevents verification tracking of CONFIG files,
87
+ which would cause false "missing" errors since these files are saved
88
+ in RUNNING/ but then moved to FINISHED_SUCCESS/.
89
+ """
90
+ from scitex.io._save import save as scitex_io_save
91
+
92
+ # Convert to dict with all keys (including private ones) for saving
93
+ config_dict = (
94
+ CONFIG.to_dict(include_private=True) if hasattr(CONFIG, "to_dict") else CONFIG
95
+ )
96
+
97
+ # track=False: Don't track internal config files in verification DB
98
+ scitex_io_save(
99
+ config_dict,
100
+ str(CONFIG["SDIR_RUN"] / "CONFIGS/CONFIG.pkl"),
101
+ verbose=False,
102
+ track=False,
103
+ )
104
+ scitex_io_save(
105
+ config_dict,
106
+ str(CONFIG["SDIR_RUN"] / "CONFIGS/CONFIG.yaml"),
107
+ verbose=False,
108
+ track=False,
109
+ )
110
+
111
+
112
+ # EOF
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_matplotlib.py
4
+ """Matplotlib configuration for session lifecycle."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import platform
10
+ import sys
11
+ from typing import Any, Dict, Optional, Tuple
12
+
13
+ import matplotlib
14
+
15
+ from scitex.logging import getLogger
16
+
17
+ logger = getLogger(__name__)
18
+
19
+ # Detect headless/WSL environments
20
+ is_headless = False
21
+ try:
22
+ # Check for WSL
23
+ if "microsoft" in platform.uname().release.lower() or "WSL" in os.environ.get(
24
+ "WSL_DISTRO_NAME", ""
25
+ ):
26
+ is_headless = True
27
+ # Check for no X11 display
28
+ elif not os.environ.get("DISPLAY"):
29
+ is_headless = True
30
+ except Exception:
31
+ # Fallback: if on Linux without DISPLAY, assume headless
32
+ if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
33
+ is_headless = True
34
+
35
+ if is_headless:
36
+ matplotlib.use("Agg")
37
+
38
+
39
+ from scitex.plt.utils._configure_mpl import configure_mpl
40
+
41
+
42
+ def setup_matplotlib(
43
+ plt=None, agg: bool = False, **mpl_kwargs: Any
44
+ ) -> Tuple[Any, Optional[Dict[str, Any]]]:
45
+ """Configure matplotlib settings.
46
+
47
+ Parameters
48
+ ----------
49
+ plt : module
50
+ Matplotlib.pyplot module (will be replaced with scitex.plt)
51
+ agg : bool
52
+ Whether to use Agg backend
53
+ **mpl_kwargs : dict
54
+ Additional matplotlib configuration parameters
55
+
56
+ Returns
57
+ -------
58
+ tuple
59
+ (plt, COLORS) - Configured scitex.plt module and color cycle
60
+ """
61
+ if plt is not None:
62
+ plt.close("all")
63
+ _, COLORS = configure_mpl(plt, **mpl_kwargs)
64
+ COLORS["gray"] = COLORS["grey"]
65
+
66
+ # Note: Backend is now set early in module initialization
67
+ # to avoid tkinter threading issues in headless/WSL environments.
68
+ # The 'agg' parameter is kept for backwards compatibility but has
69
+ # no effect since backend must be set before pyplot import.
70
+ if agg and not is_headless:
71
+ logger.warning(
72
+ "agg=True specified but backend was already set to Agg "
73
+ "during module initialization for headless environment"
74
+ )
75
+
76
+ # Replace matplotlib.pyplot with scitex.plt to get wrapped functions
77
+ import scitex.plt as stx_plt
78
+
79
+ return stx_plt, COLORS
80
+ return plt, None
81
+
82
+
83
+ # EOF