scitex-session 0.1.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.
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2025-08-21 20:36:45 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/SciTeX-Code/src/scitex/session/__init__.py
4
+ # ----------------------------------------
5
+ """scitex-session — @session decorator + lifecycle management (standalone)."""
6
+
7
+ from __future__ import annotations
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ import os
12
+
13
+ __FILE__ = __file__
14
+ __DIR__ = os.path.dirname(__FILE__)
15
+ # ----------------------------------------
16
+
17
+ """Experiment session management for SciTeX.
18
+
19
+ This module provides session lifecycle management functionality that was previously
20
+ in scitex.session.start and scitex.session.close, now as a dedicated session management system.
21
+
22
+ Usage:
23
+ # Session management (replaces scitex.session.start/close)
24
+ import sys
25
+ import matplotlib.pyplot as plt
26
+ from scitex import session
27
+
28
+ # Start a session
29
+ CONFIG, sys.stdout, sys.stderr, plt, COLORS, rng = session.start(sys, plt)
30
+
31
+ # Your experiment code here
32
+
33
+ # Close the session
34
+ session.close(CONFIG)
35
+
36
+ # Session manager for advanced use cases
37
+ manager = session.SessionManager()
38
+ active_sessions = manager.get_active_sessions()
39
+
40
+ # Using INJECTED sentinel for decorator parameters
41
+ @stx.session
42
+ def main(CONFIG=stx.session.INJECTED, plt=stx.session.INJECTED):
43
+ ...
44
+ """
45
+
46
+
47
+ # Sentinel object for decorator-injected parameters
48
+ class _InjectedSentinel:
49
+ """Sentinel value indicating a parameter will be injected by a decorator."""
50
+
51
+ def __repr__(self):
52
+ return "<INJECTED>"
53
+
54
+
55
+ INJECTED = _InjectedSentinel()
56
+
57
+
58
+ # Import session management functionality
59
+ # Use refactored _lifecycle subpackage (verification hooks included)
60
+ from ._decorator import run, session
61
+ from ._lifecycle import close, running2finished, start
62
+ from ._manager import SessionManager
63
+
64
+ # Export public API
65
+ __all__ = [
66
+ # Sentinel for injected parameters
67
+ "INJECTED",
68
+ # Session lifecycle (main functions)
69
+ "start",
70
+ "close",
71
+ "running2finished",
72
+ # Session decorator (new simplified API)
73
+ "session",
74
+ "run",
75
+ # Advanced session management
76
+ "SessionManager",
77
+ ]
78
+
79
+ # EOF
@@ -0,0 +1,640 @@
1
+ #!/usr/bin/env python3
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
+
6
+ # Timestamp: "2025-11-05"
7
+ """Session decorator for scitex.
8
+
9
+ Provides @stx.session decorator that automatically:
10
+ - Generates CLI from function signature
11
+ - Manages session lifecycle
12
+ - Handles errors
13
+ - Organizes outputs
14
+ """
15
+
16
+ import argparse
17
+ import functools
18
+ import inspect
19
+ import sys as sys_module
20
+ from pathlib import Path
21
+ from typing import Any, Callable, get_type_hints
22
+
23
+ from logging import getLogger
24
+
25
+ from . import INJECTED # Use local INJECTED from session module
26
+ from ._lifecycle import close, start
27
+
28
+ # Internal logger for the decorator itself
29
+ _decorator_logger = getLogger(__name__)
30
+
31
+
32
+ def session(
33
+ func: Callable = None,
34
+ *,
35
+ verbose: bool = False,
36
+ agg: bool = True,
37
+ notify: bool = False,
38
+ sdir_suffix: str = None,
39
+ **session_kwargs,
40
+ ) -> Callable:
41
+ """Decorator to wrap function in scitex session.
42
+
43
+ Automatically handles:
44
+ - CLI argument parsing from function signature
45
+ - Session initialization (logging, output directories)
46
+ - Execution
47
+ - Cleanup
48
+ - Error handling
49
+
50
+ This decorator is designed for script entry points. The decorated function
51
+ should be called without arguments from `if __name__ == '__main__':` to
52
+ trigger CLI parsing and session management.
53
+
54
+ Args:
55
+ func: Function to wrap (set automatically by decorator)
56
+ verbose: Enable verbose logging
57
+ agg: Use matplotlib Agg backend
58
+ notify: Send notification on completion
59
+ sdir_suffix: Suffix for output directory name
60
+ **session_kwargs: Additional session configuration parameters
61
+
62
+ Example:
63
+ @stx.session
64
+ def analyze(data_path: str, threshold: float = 0.5):
65
+ '''Analyze data file.'''
66
+ data = stx.io.load(data_path)
67
+ result = process(data, threshold)
68
+ stx.io.save(result, "output.csv")
69
+ return 0
70
+
71
+ if __name__ == '__main__':
72
+ analyze() # No arguments = CLI mode with session management
73
+
74
+ # CLI: python script.py --data-path data.csv --threshold 0.7
75
+
76
+ Example with options:
77
+ @stx.session(verbose=True, notify=True)
78
+ def train_model(model_name: str, epochs: int = 10):
79
+ '''Train ML model.'''
80
+ # These are automatically available as globals:
81
+ # - CONFIG: Session configuration dict
82
+ # - plt: Matplotlib pyplot (configured for session)
83
+ # - COLORS: Custom Colors
84
+ # - rngg: RandomStateManager (fixes seeds, creates named generators)
85
+ logger.info(f"Session ID: {CONFIG['ID']}")
86
+ logger.info(f"Output directory: {CONFIG['SDIR_RUN']}")
87
+ # ... training code ...
88
+ return 0
89
+
90
+ if __name__ == '__main__':
91
+ train_model()
92
+
93
+ Notes:
94
+ - Function name can be anything (not just 'main')
95
+ - Calling with arguments bypasses session management: analyze('/path', 0.5)
96
+ - Only one session-managed function per script
97
+ - Do NOT call multiple @session decorated functions from one script
98
+ - Do NOT nest session-decorated function calls without arguments
99
+
100
+ Injected Global Variables:
101
+ When called without arguments (CLI mode), these are injected into globals:
102
+ - CONFIG (dict): Session configuration with ID, SDIR, paths, etc.
103
+ - plt (module): matplotlib.pyplot configured with session settings
104
+ - COLORS (CustomColors): Custom Colors for consistent plotting
105
+ - rngg (RandomStateManager): Manages reproducibility by fixing global seeds
106
+ and creating named generators via rngg("name")
107
+ """
108
+
109
+ def decorator(func: Callable) -> Callable:
110
+ @functools.wraps(func)
111
+ def wrapper(*args, **kwargs):
112
+ # If called with arguments (not CLI), run directly
113
+ if args or kwargs:
114
+ return func(*args, **kwargs)
115
+
116
+ # Otherwise, parse CLI and run with session management
117
+ return _run_with_session(
118
+ func,
119
+ verbose=verbose,
120
+ agg=agg,
121
+ notify=notify,
122
+ sdir_suffix=sdir_suffix,
123
+ **session_kwargs,
124
+ )
125
+
126
+ # Store original function for direct access
127
+ wrapper._func = func
128
+ wrapper._is_session_wrapped = True
129
+
130
+ return wrapper
131
+
132
+ # Handle @stx.session vs @stx.session()
133
+ if func is None:
134
+ # Called with arguments: @stx.session(verbose=True)
135
+ return decorator
136
+ else:
137
+ # Called without arguments: @stx.session
138
+ return decorator(func)
139
+
140
+
141
+ def _run_with_session(
142
+ func: Callable,
143
+ verbose: bool,
144
+ agg: bool,
145
+ notify: bool,
146
+ sdir_suffix: str,
147
+ **session_kwargs,
148
+ ) -> Any:
149
+ """Run function with full session management."""
150
+
151
+ # Get calling file
152
+ frame = inspect.currentframe()
153
+ caller_frame = frame.f_back.f_back # Go up two levels
154
+ caller_file = caller_frame.f_globals.get("__file__", "unknown.py")
155
+
156
+ # Generate argparse from function signature
157
+ parser = _create_parser(func)
158
+ args = parser.parse_args()
159
+
160
+ # Clean up INJECTED sentinels from args before passing to session
161
+ cleaned_args = argparse.Namespace(
162
+ **{k: v for k, v in vars(args).items() if not isinstance(v, type(INJECTED))}
163
+ )
164
+
165
+ # Start session
166
+ import matplotlib.pyplot as plt
167
+
168
+ CONFIG, stdout, stderr, plt, COLORS, rngg = start(
169
+ sys=sys_module,
170
+ plt=plt,
171
+ args=cleaned_args,
172
+ file=caller_file,
173
+ sdir_suffix=sdir_suffix or func.__name__,
174
+ verbose=verbose,
175
+ agg=agg,
176
+ **session_kwargs,
177
+ )
178
+
179
+ # Create a logger for the user's script
180
+ script_logger = getLogger(func.__module__)
181
+
182
+ # Store session variables in function globals
183
+ func_globals = func.__globals__
184
+ func_globals["CONFIG"] = CONFIG
185
+ func_globals["plt"] = plt
186
+ func_globals["COLORS"] = COLORS
187
+ func_globals["rngg"] = rngg
188
+ func_globals["logger"] = script_logger
189
+
190
+ # Log injected globals for user awareness (only in verbose mode)
191
+ if verbose:
192
+ _decorator_logger.info("=" * 60)
193
+ _decorator_logger.info(
194
+ "Injected Global Variables (available in your function):"
195
+ )
196
+ _decorator_logger.info(" • CONFIG - Session configuration dict")
197
+ _decorator_logger.info(f" - CONFIG['ID']: {CONFIG['ID']}")
198
+ _decorator_logger.info(f" - CONFIG['SDIR_RUN']: {CONFIG['SDIR_RUN']}")
199
+ _decorator_logger.info(f" - CONFIG['PID']: {CONFIG['PID']}")
200
+ _decorator_logger.info(" • plt - matplotlib.pyplot (configured for session)")
201
+ _decorator_logger.info(" • COLORS - CustomColors (for consistent plotting)")
202
+ _decorator_logger.info(" • rngg - RandomStateManager (for reproducibility)")
203
+ _decorator_logger.info(
204
+ " • logger - SciTeX logger (configured for your script)"
205
+ )
206
+ _decorator_logger.info("=" * 60)
207
+
208
+ # Run function
209
+ exit_status = 0
210
+ result = None
211
+
212
+ try:
213
+ # Convert args namespace to kwargs
214
+ kwargs = vars(args)
215
+
216
+ # Get function parameters
217
+ sig = inspect.signature(func)
218
+ func_params = set(sig.parameters.keys())
219
+
220
+ # Map of injected variable names to their actual objects
221
+ injection_map = {
222
+ "CONFIG": CONFIG,
223
+ "plt": plt,
224
+ "COLORS": COLORS,
225
+ "rngg": rngg,
226
+ "logger": script_logger,
227
+ }
228
+
229
+ # Build filtered_kwargs with user args and injected values
230
+ filtered_kwargs = {}
231
+
232
+ # First, add all parsed CLI arguments
233
+ for k, v in kwargs.items():
234
+ if k in func_params:
235
+ filtered_kwargs[k] = v
236
+
237
+ # Then, inject parameters that have INJECTED as default
238
+ for param_name, param in sig.parameters.items():
239
+ if param.default != inspect.Parameter.empty:
240
+ if isinstance(param.default, type(INJECTED)):
241
+ # This parameter should be injected
242
+ if param_name in injection_map:
243
+ filtered_kwargs[param_name] = injection_map[param_name]
244
+
245
+ # Log injected arguments summary (only in verbose mode)
246
+ if verbose:
247
+ args_summary = {k: type(v).__name__ for k, v in filtered_kwargs.items()}
248
+ _decorator_logger.info(f"Running {func.__name__} with injected parameters:")
249
+ _decorator_logger.info(args_summary, pprint=True, indent=2)
250
+
251
+ # Execute function
252
+ result = func(**filtered_kwargs)
253
+
254
+ # Handle return value
255
+ if isinstance(result, int):
256
+ exit_status = result
257
+ else:
258
+ exit_status = 0
259
+
260
+ except Exception as e:
261
+ _decorator_logger.error(f"Error in {func.__name__}: {e}", exc_info=True)
262
+ exit_status = 1
263
+ raise
264
+
265
+ finally:
266
+ # Close session with error handling
267
+ try:
268
+ close(
269
+ CONFIG=CONFIG,
270
+ verbose=verbose,
271
+ notify=notify,
272
+ message=f"{func.__name__} completed",
273
+ exit_status=exit_status,
274
+ )
275
+ except SystemExit:
276
+ # Allow normal exits
277
+ raise
278
+ except KeyboardInterrupt:
279
+ # Allow Ctrl+C
280
+ raise
281
+ except Exception as e:
282
+ # Log but don't crash on cleanup errors
283
+ try:
284
+ _decorator_logger.error(f"Session cleanup error: {e}")
285
+ except:
286
+ print(f"Session cleanup error: {e}")
287
+
288
+ # Final matplotlib cleanup (belt and suspenders approach)
289
+ try:
290
+ import matplotlib.pyplot as plt
291
+
292
+ plt.close("all")
293
+ except:
294
+ pass
295
+
296
+ return result
297
+
298
+
299
+ def _create_parser(func: Callable) -> argparse.ArgumentParser:
300
+ """Create ArgumentParser from function signature.
301
+
302
+ Args:
303
+ func: Function to create parser for
304
+
305
+ Returns:
306
+ Configured ArgumentParser
307
+ """
308
+
309
+ # Get function info
310
+ sig = inspect.signature(func)
311
+ doc = inspect.getdoc(func) or f"Run {func.__name__}"
312
+
313
+ # Try to get type hints
314
+ try:
315
+ type_hints = get_type_hints(func)
316
+ except Exception:
317
+ type_hints = {}
318
+
319
+ # Get actual values for deterministic items
320
+ # Get calling file from the decorated function's module
321
+ caller_file = func.__globals__.get("__file__", "unknown.py")
322
+
323
+ # Calculate SDIR_OUT (base output directory)
324
+ import os
325
+
326
+ sdir_out = Path(os.path.splitext(caller_file)[0] + "_out")
327
+ sdir_run_example = sdir_out / "RUNNING" / "<SESSION_ID>"
328
+
329
+ # Get current PID
330
+ current_pid = os.getpid()
331
+
332
+ # Check for config YAML files and list all variables with values
333
+ config_status = ""
334
+ try:
335
+ config_dir = Path("./config")
336
+ if config_dir.exists():
337
+ yaml_files = sorted(config_dir.glob("*.yaml"))
338
+ if yaml_files:
339
+ config_status = " CONFIG from YAML files:\n"
340
+
341
+ # Load and list all config variables with their values
342
+ try:
343
+ import yaml
344
+
345
+ all_vars = []
346
+ for yaml_file in yaml_files:
347
+ with open(yaml_file, "r") as f:
348
+ data = yaml.safe_load(f)
349
+ if isinstance(data, dict):
350
+ namespace = yaml_file.stem.upper()
351
+ for key, value in data.items():
352
+ # Format value for display (truncate if too long)
353
+ value_str = str(value)
354
+ if len(value_str) > 50:
355
+ value_str = value_str[:47] + "..."
356
+ all_vars.append(
357
+ f" - CONFIG.{namespace}.{key} (from ./config/{yaml_file.name})\n {value_str}"
358
+ )
359
+
360
+ if all_vars:
361
+ config_status += "\n".join(all_vars)
362
+ else:
363
+ config_status = " CONFIG from YAML files:\n (no variables found)"
364
+ except Exception as e:
365
+ # If we can't load the YAML files, just show error
366
+ config_status = " CONFIG from YAML files:\n (unable to load at help-time, will be available at runtime)"
367
+ else:
368
+ config_status = (
369
+ " CONFIG from YAML files:\n (no .yaml files found)"
370
+ )
371
+ else:
372
+ config_status = " CONFIG from YAML files:\n (./config/ directory not found)"
373
+ except:
374
+ config_status = (
375
+ " CONFIG from YAML files:\n (unable to check at help-time)"
376
+ )
377
+
378
+ # Get available color keys
379
+ try:
380
+ import matplotlib.pyplot as plt_temp
381
+
382
+ from scitex.plt.utils._configure_mpl import configure_mpl
383
+
384
+ _, colors_dict = configure_mpl(plt_temp)
385
+ # Show all color keys
386
+ sorted_keys = sorted(colors_dict.keys())
387
+ color_keys = ", ".join(f"'{k}'" for k in sorted_keys)
388
+ except Exception as e:
389
+ # Fallback if configure_mpl fails
390
+ color_keys = "'blue', 'red', 'green', 'yellow', 'purple', 'orange', ..."
391
+
392
+ # Create parser with epilog documenting injected globals with actual values
393
+ epilog = f"""
394
+ Global Variables Injected by @session Decorator:
395
+
396
+ CONFIG (DotDict)
397
+ Session configuration with ID, paths, timestamps
398
+ Access: CONFIG['key'] or CONFIG.key (both work!)
399
+
400
+ - CONFIG.ID
401
+ <SESSION_ID> (created at runtime, e.g., '2025Y-11M-18D-07h53m37s_Z5MR')
402
+ - CONFIG.FILE
403
+ {Path(caller_file)}
404
+ - CONFIG.SDIR_OUT
405
+ {sdir_out}
406
+ - CONFIG.SDIR_RUN
407
+ {sdir_run_example}
408
+ - CONFIG.PID
409
+ {current_pid} (current Python process)
410
+ - CONFIG.ARGS
411
+ {{'arg1': '<value>'}} (parsed from command line)
412
+
413
+ {config_status}
414
+
415
+ plt (module)
416
+ matplotlib.pyplot configured for session
417
+
418
+ COLORS (DotDict)
419
+ Color palette for consistent plotting
420
+ Access: COLORS.blue or COLORS['blue'] (both work!)
421
+
422
+ Available keys:
423
+ {color_keys}
424
+
425
+ Usage:
426
+ plt.plot(x, y, color=COLORS.blue)
427
+ plt.plot(x, y, color=COLORS['blue'])
428
+
429
+ rngg (RandomStateManager)
430
+ Manages reproducible randomness
431
+
432
+ logger (SciTeXLogger)
433
+ Logger configured for your script
434
+ """
435
+
436
+ parser = argparse.ArgumentParser(
437
+ description=doc,
438
+ epilog=epilog,
439
+ formatter_class=argparse.RawDescriptionHelpFormatter,
440
+ )
441
+
442
+ # Add arguments from function signature (skip injected parameters)
443
+ # Track used short forms to avoid conflicts
444
+ used_short_forms = {"h"} # Reserve -h for help
445
+
446
+ for param_name, param in sig.parameters.items():
447
+ # Skip parameters with INJECTED as default (these are injected by decorator)
448
+ if param.default != inspect.Parameter.empty:
449
+ if isinstance(param.default, type(INJECTED)):
450
+ continue # Skip injected parameters
451
+
452
+ # Generate short form
453
+ short_form = _generate_short_form(param_name, used_short_forms)
454
+ if short_form:
455
+ used_short_forms.add(short_form)
456
+
457
+ _add_argument(parser, param_name, param, type_hints, short_form)
458
+
459
+ return parser
460
+
461
+
462
+ def _generate_short_form(param_name: str, used_short_forms: set) -> str:
463
+ """Generate a short form for a parameter name avoiding conflicts.
464
+
465
+ Args:
466
+ param_name: Full parameter name
467
+ used_short_forms: Set of already used short forms
468
+
469
+ Returns:
470
+ Short form character or None if no unique form can be generated
471
+ """
472
+ # Strategy 1: Try first letter
473
+ first_letter = param_name[0].lower()
474
+ if first_letter not in used_short_forms:
475
+ return first_letter
476
+
477
+ # Strategy 2: Try first letter of each word (for snake_case or camelCase)
478
+ words = param_name.replace("_", " ").replace("-", " ").split()
479
+ if len(words) > 1:
480
+ acronym = "".join(w[0].lower() for w in words)
481
+ if len(acronym) == 1 and acronym not in used_short_forms:
482
+ return acronym
483
+
484
+ # Strategy 3: Try first two letters
485
+ if len(param_name) >= 2:
486
+ two_letters = param_name[:2].lower()
487
+ if two_letters not in used_short_forms:
488
+ return two_letters
489
+
490
+ # Strategy 4: Try each character in sequence
491
+ for char in param_name.lower():
492
+ if char.isalnum() and char not in used_short_forms:
493
+ return char
494
+
495
+ # Give up if no unique short form found
496
+ return None
497
+
498
+
499
+ def _add_argument(
500
+ parser: argparse.ArgumentParser,
501
+ param_name: str,
502
+ param: inspect.Parameter,
503
+ type_hints: dict,
504
+ short_form: str = None,
505
+ ):
506
+ """Add single argument to parser.
507
+
508
+ Args:
509
+ parser: ArgumentParser to add to
510
+ param_name: Parameter name
511
+ param: Parameter object
512
+ type_hints: Type hints dictionary
513
+ short_form: Optional short form (e.g., 'a' for -a)
514
+ """
515
+ from typing import Literal, get_args, get_origin
516
+
517
+ # Get type
518
+ param_type = type_hints.get(param_name, param.annotation)
519
+ if param_type == inspect.Parameter.empty:
520
+ param_type = str
521
+
522
+ # Get default
523
+ has_default = param.default != inspect.Parameter.empty
524
+ default = param.default if has_default else None
525
+
526
+ # Convert parameter name to CLI format
527
+ arg_name = f"--{param_name.replace('_', '-')}"
528
+
529
+ # Build argument names list (long form, optionally short form)
530
+ arg_names = [arg_name]
531
+ if short_form:
532
+ arg_names.insert(0, f"-{short_form}")
533
+
534
+ # Check for Literal type (choices)
535
+ choices = None
536
+ origin = get_origin(param_type)
537
+ if origin is Literal:
538
+ choices = list(get_args(param_type))
539
+ param_type = type(choices[0]) if choices else str
540
+
541
+ # Handle different types
542
+ if param_type == bool:
543
+ # Boolean flags
544
+ parser.add_argument(
545
+ *arg_names,
546
+ action="store_true" if not default else "store_false",
547
+ default=default,
548
+ help=f"(default: {default})",
549
+ )
550
+ else:
551
+ # Regular arguments
552
+ choices_str = f", choices: {choices}" if choices else ""
553
+ kwargs = {
554
+ "type": param_type,
555
+ "help": (
556
+ f"(default: {default}{choices_str})"
557
+ if has_default
558
+ else f"(required{choices_str})"
559
+ ),
560
+ }
561
+
562
+ if choices:
563
+ kwargs["choices"] = choices
564
+
565
+ if has_default:
566
+ kwargs["default"] = default
567
+ else:
568
+ kwargs["required"] = True
569
+
570
+ parser.add_argument(*arg_names, **kwargs)
571
+
572
+
573
+ def run(func: Callable, parse_args: Callable = None, **session_kwargs) -> Any:
574
+ """Run function with session management.
575
+
576
+ Alternative to decorator for more explicit control.
577
+
578
+ Args:
579
+ func: Function to run
580
+ parse_args: Optional custom argument parser
581
+ **session_kwargs: Session configuration
582
+
583
+ Example:
584
+ def main(args):
585
+ # Your code
586
+ return 0
587
+
588
+ if __name__ == '__main__':
589
+ stx.session.run(main)
590
+ """
591
+
592
+ if parse_args is None:
593
+ # Auto-generate parser
594
+ parser = _create_parser(func)
595
+ args = parser.parse_args()
596
+ else:
597
+ # Use custom parser
598
+ args = parse_args()
599
+
600
+ # Get file
601
+ frame = inspect.currentframe()
602
+ caller_frame = frame.f_back
603
+ caller_file = caller_frame.f_globals.get("__file__", "unknown.py")
604
+
605
+ # Start session
606
+ import matplotlib.pyplot as plt
607
+
608
+ CONFIG, stdout, stderr, plt, COLORS, rngg = start(
609
+ sys=sys_module,
610
+ plt=plt,
611
+ args=args,
612
+ file=caller_file,
613
+ **session_kwargs,
614
+ )
615
+
616
+ # Run
617
+ try:
618
+ if hasattr(args, "__dict__"):
619
+ exit_status = func(args)
620
+ else:
621
+ exit_status = func()
622
+
623
+ exit_status = exit_status or 0
624
+
625
+ except Exception as e:
626
+ _decorator_logger.error(f"Error: {e}", exc_info=True)
627
+ exit_status = 1
628
+ raise
629
+
630
+ finally:
631
+ close(
632
+ CONFIG=CONFIG,
633
+ exit_status=exit_status,
634
+ **session_kwargs,
635
+ )
636
+
637
+ return exit_status
638
+
639
+
640
+ # EOF