scitex 2.16.1__py3-none-any.whl → 2.17.0__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.
- scitex/_mcp_resources/_cheatsheet.py +1 -1
- scitex/_mcp_resources/_modules.py +1 -1
- scitex/_mcp_tools/__init__.py +2 -0
- scitex/_mcp_tools/verify.py +256 -0
- scitex/cli/main.py +2 -0
- scitex/cli/verify.py +476 -0
- scitex/dev/plt/__init__.py +1 -1
- scitex/dev/plt/mpl/get_dir_ax.py +1 -1
- scitex/dev/plt/mpl/get_signatures.py +1 -1
- scitex/dev/plt/mpl/get_signatures_details.py +1 -1
- scitex/io/_load.py +8 -1
- scitex/io/_save.py +12 -0
- scitex/session/README.md +2 -2
- scitex/session/__init__.py +1 -0
- scitex/session/_decorator.py +57 -33
- scitex/session/_lifecycle/__init__.py +23 -0
- scitex/session/_lifecycle/_close.py +225 -0
- scitex/session/_lifecycle/_config.py +112 -0
- scitex/session/_lifecycle/_matplotlib.py +83 -0
- scitex/session/_lifecycle/_start.py +246 -0
- scitex/session/_lifecycle/_utils.py +186 -0
- scitex/session/_manager.py +40 -3
- scitex/session/template.py +1 -1
- scitex/template/_templates/plt.py +1 -1
- scitex/template/_templates/session.py +1 -1
- scitex/verify/README.md +312 -0
- scitex/verify/__init__.py +212 -0
- scitex/verify/_chain.py +369 -0
- scitex/verify/_db.py +600 -0
- scitex/verify/_hash.py +187 -0
- scitex/verify/_integration.py +127 -0
- scitex/verify/_rerun.py +253 -0
- scitex/verify/_tracker.py +330 -0
- scitex/verify/_visualize.py +48 -0
- scitex/verify/_viz/__init__.py +56 -0
- scitex/verify/_viz/_colors.py +84 -0
- scitex/verify/_viz/_format.py +302 -0
- scitex/verify/_viz/_json.py +192 -0
- scitex/verify/_viz/_mermaid.py +440 -0
- scitex/verify/_viz/_plotly.py +193 -0
- scitex/verify/_viz/_templates.py +246 -0
- scitex/verify/_viz/_utils.py +56 -0
- {scitex-2.16.1.dist-info → scitex-2.17.0.dist-info}/METADATA +1 -1
- {scitex-2.16.1.dist-info → scitex-2.17.0.dist-info}/RECORD +47 -23
- scitex/session/_lifecycle.py +0 -827
- {scitex-2.16.1.dist-info → scitex-2.17.0.dist-info}/WHEEL +0 -0
- {scitex-2.16.1.dist-info → scitex-2.17.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.16.1.dist-info → scitex-2.17.0.dist-info}/licenses/LICENSE +0 -0
scitex/session/_decorator.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
# -
|
|
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
|
-
-
|
|
104
|
-
and creating named generators via
|
|
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
|
-
**{
|
|
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,
|
|
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["
|
|
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(
|
|
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(
|
|
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(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
_decorator_logger.info(
|
|
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
|
-
"
|
|
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 = {
|
|
242
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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":
|
|
549
|
-
|
|
550
|
-
|
|
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,
|
|
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
|