diff --git a/pyproject.toml b/pyproject.toml index bdfecd61..8f6bea6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -635,10 +635,8 @@ security = [] # Session Module - Session management # Use: pip install scitex[session] -session = [ - "matplotlib", - "PyYAML", -] +# Real implementation lives in the standalone scitex-session package. +session = ["scitex-session>=0.1.0"] # Sh Module - Shell utilities # Use: pip install scitex[sh] diff --git a/src/scitex/session/README.md b/src/scitex/session/README.md deleted file mode 100644 index a5fbb322..00000000 --- a/src/scitex/session/README.md +++ /dev/null @@ -1,252 +0,0 @@ - - -# scitex.session - -Experiment session management for reproducible scientific computing. - -## Overview - -scitex.session provides lifecycle management for scientific experiments with automatic logging, reproducibility settings, and session tracking. - -## Quick Start - -```python -import scitex - -@scitex.session -def main( - CONFIG=scitex.INJECTED, - plt=scitex.INJECTED, - COLORS=scitex.INJECTED, - rngg=scitex.INJECTED, -): - """Args injected by @scitex.session decorator""" - print(f"Session ID: {CONFIG['ID']}") - # Your experiment code here - -if __name__ == "__main__": - main() -``` - -## Core Functions - -### @session.session Decorator - -The recommended way to use scitex.session is through the `@scitex.session` decorator, which automatically handles session initialization and cleanup. - -```python -import scitex - -@scitex.session -def main( - CONFIG=scitex.INJECTED, - plt=scitex.INJECTED, - COLORS=scitex.INJECTED, - rngg=scitex.INJECTED, -): - """Args injected by @scitex.session decorator""" - print(f"Session ID: {CONFIG['ID']}") - -if __name__ == "__main__": - main() -``` - -The decorator automatically injects the following parameters: -- `CONFIG`: Configuration dictionary with session metadata -- `plt`: Configured matplotlib.pyplot module -- `COLORS`: Color cycle dictionary -- `rng`: RandomStateManager instance - -Using `scitex.INJECTED` as default values makes it explicit that these parameters are injected by the decorator. - -Decorator Parameters: -- file: Script file path (auto-detected if None) -- sdir: Save directory (auto-generated if None) -- seed: Random seed for reproducibility (default: 42) -- agg: Use matplotlib Agg backend (default: False) -- verbose: Print detailed information (default: True) - -### session.start() (Advanced) - -Initialize experiment session with reproducibility settings. Note: The decorator approach is recommended over direct start()/close() calls. - -Parameters: -- sys: Python sys module for I/O redirection -- plt: Matplotlib pyplot module -- file: Script file path (auto-detected if None) -- sdir: Save directory (auto-generated if None) -- seed: Random seed for reproducibility (default: 42) -- agg: Use matplotlib Agg backend (default: False) -- verbose: Print detailed information (default: True) - -Returns: -- CONFIGS: Configuration dictionary with session metadata -- stdout, stderr: Redirected output streams -- plt: Configured matplotlib.pyplot module -- COLORS: Color cycle dictionary -- rng: RandomStateManager instance - -### session.close() (Advanced) - -Close experiment session and finalize logging. Note: The decorator handles this automatically. - -Parameters: -- CONFIG: Configuration dictionary from start() -- message: Completion message (default: ':)') -- notify: Send notification (default: False) -- verbose: Print verbose output (default: True) -- exit_status: 0=success, 1=error, None=finished - -## Features - -### Automatic Logging - -- Redirects stdout/stderr to log files -- Saves logs to SDIR/logs/ -- Removes ANSI escape codes -- Captures all print statements - -### Reproducibility - -- Fixed random seeds via RandomStateManager -- Supports os, random, numpy, torch -- Records all configuration parameters -- Timestamps and session IDs - -### Session Tracking - -- Unique session IDs (4 characters) -- Process ID (PID) tracking -- Start/end timestamps -- Runtime calculation - -### Directory Management - -- Auto-generates save directories -- RUNNING/ for active sessions -- FINISHED/ for completed sessions -- FINISHED_SUCOLORSESS/ and FINISHED_ERROR/ based on exit status - -### Configuration Management - -- Saves CONFIG as .pkl and .yaml -- Includes all session metadata -- Command-line arguments captured -- Path objects and strings supported - -## Advanced Usage - -### Custom Save Directory - -```python -import scitex - -@scitex.session(sdir="/custom/path/") -def main(): - # Your experiment code here - pass - -if __name__ == "__main__": - main() -``` - -### Debug Mode - -Set IS_DEBUG in ./config/IS_DEBUG.yaml: - -```yaml -IS_DEBUG: true -``` - -Session IDs will be prefixed with "DEBUG_" - -### Session Manager - -```python -from scitex.session import SessionManager - -manager = SessionManager() -active = manager.get_active_sessions() -info = manager.get_session(session_id) -``` - -## Directory Structure - -``` -/path/to/script.py -/path/to/script_out/ -├── RUNNING/ -│ └── XXXX/ # Session ID -│ ├── logs/ -│ │ ├── stdout.log -│ │ └── stderr.log -│ └── CONFIGS/ -│ ├── CONFIG.pkl -│ └── CONFIG.yaml -├── FINISHED/ -├── FINISHED_SUCOLORSESS/ -└── FINISHED_ERROR/ -``` - -## Configuration Object - -CONFIG contains: -- ID: Session identifier -- PID: Process ID -- START_DATETIME: Session start timestamp (datetime object) -- END_DATETIME: Session end timestamp (datetime object) -- RUN_DURATION: Formatted runtime string (HH:MM:SS) -- FILE: Script file path (Path object) -- SDIR_OUT: Base output directory (Path object) -- SDIR_RUN: Current session directory (Path object) -- ARGS: Command-line arguments (dict) -- EXIT_STATUS: Exit code (0=success, 1=error) - -## Matplotlib Integration - -```python -import scitex - -@scitex.session( - fig_size_mm=(160, 100), - dpi_save=300, - hide_top_right_spines=True, - alpha=0.9 -) -def main(): - # plt and COLORS are automatically available - plt.plot([1, 2, 3], color=COLORS[0]) - plt.show() - -if __name__ == "__main__": - main() -``` - -- plt is replaced with scitex.plt wrapper -- COLORS provides color cycle dictionary -- Automatic style configuration - -## Random State Management - -```python -import scitex - -@scitex.session(seed=42) -def main(): - # rng is automatically available - random_array = rng.random((10, 10)) - print(random_array) - -if __name__ == "__main__": - main() -``` - -- rng is global RandomStateManager -- Automatically fixes seeds for all libraries -- Reproducible across runs - - diff --git a/src/scitex/session/__init__.py b/src/scitex/session/__init__.py index 636c1242..9b110e24 100755 --- a/src/scitex/session/__init__.py +++ b/src/scitex/session/__init__.py @@ -1,75 +1,20 @@ -#!/usr/bin/env python3 -# Timestamp: "2025-08-21 20:36:45 (ywatanabe)" -# File: /home/ywatanabe/proj/SciTeX-Code/src/scitex/session/__init__.py -# ---------------------------------------- -from __future__ import annotations +"""SciTeX session — thin compatibility shim for scitex-session. -import os +Aliases ``scitex.session`` to the standalone ``scitex_session`` package via +``sys.modules``. ``scitex.session is scitex_session``. -__FILE__ = __file__ -__DIR__ = os.path.dirname(__FILE__) -# ---------------------------------------- - -"""Experiment session management for SciTeX. - -This module provides session lifecycle management functionality that was previously -in scitex.session.start and scitex.session.close, now as a dedicated session management system. - -Usage: - # Session management (replaces scitex.session.start/close) - import sys - import matplotlib.pyplot as plt - from scitex import session - - # Start a session - CONFIG, sys.stdout, sys.stderr, plt, COLORS, rng = session.start(sys, plt) - - # Your experiment code here - - # Close the session - session.close(CONFIG) - - # Session manager for advanced use cases - manager = session.SessionManager() - active_sessions = manager.get_active_sessions() - - # Using INJECTED sentinel for decorator parameters - @stx.session - def main(CONFIG=stx.session.INJECTED, plt=stx.session.INJECTED): - ... +Install: ``pip install scitex[session]`` (or ``pip install scitex-session``). +See: https://github.com/ywatanabe1989/scitex-session """ +import sys as _sys -# Sentinel object for decorator-injected parameters -class _InjectedSentinel: - """Sentinel value indicating a parameter will be injected by a decorator.""" - - def __repr__(self): - return "" - - -INJECTED = _InjectedSentinel() - - -# Import session management functionality -# Use refactored _lifecycle subpackage (verification hooks included) -from ._decorator import run, session -from ._lifecycle import close, running2finished, start -from ._manager import SessionManager - -# Export public API -__all__ = [ - # Sentinel for injected parameters - "INJECTED", - # Session lifecycle (main functions) - "start", - "close", - "running2finished", - # Session decorator (new simplified API) - "session", - "run", - # Advanced session management - "SessionManager", -] +try: + import scitex_session as _real +except ImportError as _e: # pragma: no cover + raise ImportError( + "scitex.session requires the 'scitex-session' package. " + "Install with: pip install scitex[session] (or: pip install scitex-session)" + ) from _e -# EOF +_sys.modules[__name__] = _real diff --git a/src/scitex/session/_decorator.py b/src/scitex/session/_decorator.py deleted file mode 100755 index c9087915..00000000 --- a/src/scitex/session/_decorator.py +++ /dev/null @@ -1,640 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Timestamp: "2026-02-01 08:38:19 (ywatanabe)" -# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_decorator.py - -# Timestamp: "2025-11-05" -"""Session decorator for scitex. - -Provides @stx.session decorator that automatically: -- Generates CLI from function signature -- Manages session lifecycle -- Handles errors -- Organizes outputs -""" - -import argparse -import functools -import inspect -import sys as sys_module -from pathlib import Path -from typing import Any, Callable, get_type_hints - -from scitex.logging import getLogger - -from . import INJECTED # Use local INJECTED from session module -from ._lifecycle import close, start - -# Internal logger for the decorator itself -_decorator_logger = getLogger(__name__) - - -def session( - func: Callable = None, - *, - verbose: bool = False, - agg: bool = True, - notify: bool = False, - sdir_suffix: str = None, - **session_kwargs, -) -> Callable: - """Decorator to wrap function in scitex session. - - Automatically handles: - - CLI argument parsing from function signature - - Session initialization (logging, output directories) - - Execution - - Cleanup - - Error handling - - This decorator is designed for script entry points. The decorated function - should be called without arguments from `if __name__ == '__main__':` to - trigger CLI parsing and session management. - - Args: - func: Function to wrap (set automatically by decorator) - verbose: Enable verbose logging - agg: Use matplotlib Agg backend - notify: Send notification on completion - sdir_suffix: Suffix for output directory name - **session_kwargs: Additional session configuration parameters - - Example: - @stx.session - def analyze(data_path: str, threshold: float = 0.5): - '''Analyze data file.''' - data = stx.io.load(data_path) - result = process(data, threshold) - stx.io.save(result, "output.csv") - return 0 - - if __name__ == '__main__': - analyze() # No arguments = CLI mode with session management - - # CLI: python script.py --data-path data.csv --threshold 0.7 - - Example with options: - @stx.session(verbose=True, notify=True) - def train_model(model_name: str, epochs: int = 10): - '''Train ML model.''' - # These are automatically available as globals: - # - CONFIG: Session configuration dict - # - plt: Matplotlib pyplot (configured for session) - # - COLORS: Custom Colors - # - rngg: RandomStateManager (fixes seeds, creates named generators) - logger.info(f"Session ID: {CONFIG['ID']}") - logger.info(f"Output directory: {CONFIG['SDIR_RUN']}") - # ... training code ... - return 0 - - if __name__ == '__main__': - train_model() - - Notes: - - Function name can be anything (not just 'main') - - Calling with arguments bypasses session management: analyze('/path', 0.5) - - Only one session-managed function per script - - Do NOT call multiple @session decorated functions from one script - - Do NOT nest session-decorated function calls without arguments - - Injected Global Variables: - When called without arguments (CLI mode), these are injected into globals: - - CONFIG (dict): Session configuration with ID, SDIR, paths, etc. - - plt (module): matplotlib.pyplot configured with session settings - - COLORS (CustomColors): Custom Colors for consistent plotting - - rngg (RandomStateManager): Manages reproducibility by fixing global seeds - and creating named generators via rngg("name") - """ - - def decorator(func: Callable) -> Callable: - @functools.wraps(func) - def wrapper(*args, **kwargs): - # If called with arguments (not CLI), run directly - if args or kwargs: - return func(*args, **kwargs) - - # Otherwise, parse CLI and run with session management - return _run_with_session( - func, - verbose=verbose, - agg=agg, - notify=notify, - sdir_suffix=sdir_suffix, - **session_kwargs, - ) - - # Store original function for direct access - wrapper._func = func - wrapper._is_session_wrapped = True - - return wrapper - - # Handle @stx.session vs @stx.session() - if func is None: - # Called with arguments: @stx.session(verbose=True) - return decorator - else: - # Called without arguments: @stx.session - return decorator(func) - - -def _run_with_session( - func: Callable, - verbose: bool, - agg: bool, - notify: bool, - sdir_suffix: str, - **session_kwargs, -) -> Any: - """Run function with full session management.""" - - # Get calling file - frame = inspect.currentframe() - caller_frame = frame.f_back.f_back # Go up two levels - caller_file = caller_frame.f_globals.get("__file__", "unknown.py") - - # Generate argparse from function signature - parser = _create_parser(func) - args = parser.parse_args() - - # Clean up INJECTED sentinels from args before passing to session - cleaned_args = argparse.Namespace( - **{k: v for k, v in vars(args).items() if not isinstance(v, type(INJECTED))} - ) - - # Start session - import matplotlib.pyplot as plt - - CONFIG, stdout, stderr, plt, COLORS, rngg = start( - sys=sys_module, - plt=plt, - args=cleaned_args, - file=caller_file, - sdir_suffix=sdir_suffix or func.__name__, - verbose=verbose, - agg=agg, - **session_kwargs, - ) - - # Create a logger for the user's script - script_logger = getLogger(func.__module__) - - # Store session variables in function globals - func_globals = func.__globals__ - func_globals["CONFIG"] = CONFIG - func_globals["plt"] = plt - func_globals["COLORS"] = COLORS - func_globals["rngg"] = rngg - func_globals["logger"] = script_logger - - # Log injected globals for user awareness (only in verbose mode) - if verbose: - _decorator_logger.info("=" * 60) - _decorator_logger.info( - "Injected Global Variables (available in your function):" - ) - _decorator_logger.info(" • CONFIG - Session configuration dict") - _decorator_logger.info(f" - CONFIG['ID']: {CONFIG['ID']}") - _decorator_logger.info(f" - CONFIG['SDIR_RUN']: {CONFIG['SDIR_RUN']}") - _decorator_logger.info(f" - CONFIG['PID']: {CONFIG['PID']}") - _decorator_logger.info(" • plt - matplotlib.pyplot (configured for session)") - _decorator_logger.info(" • COLORS - CustomColors (for consistent plotting)") - _decorator_logger.info(" • rngg - RandomStateManager (for reproducibility)") - _decorator_logger.info( - " • logger - SciTeX logger (configured for your script)" - ) - _decorator_logger.info("=" * 60) - - # Run function - exit_status = 0 - result = None - - try: - # Convert args namespace to kwargs - kwargs = vars(args) - - # Get function parameters - sig = inspect.signature(func) - func_params = set(sig.parameters.keys()) - - # Map of injected variable names to their actual objects - injection_map = { - "CONFIG": CONFIG, - "plt": plt, - "COLORS": COLORS, - "rngg": rngg, - "logger": script_logger, - } - - # Build filtered_kwargs with user args and injected values - filtered_kwargs = {} - - # First, add all parsed CLI arguments - for k, v in kwargs.items(): - if k in func_params: - filtered_kwargs[k] = v - - # Then, inject parameters that have INJECTED as default - for param_name, param in sig.parameters.items(): - if param.default != inspect.Parameter.empty: - if isinstance(param.default, type(INJECTED)): - # This parameter should be injected - if param_name in injection_map: - filtered_kwargs[param_name] = injection_map[param_name] - - # Log injected arguments summary (only in verbose mode) - if verbose: - args_summary = {k: type(v).__name__ for k, v in filtered_kwargs.items()} - _decorator_logger.info(f"Running {func.__name__} with injected parameters:") - _decorator_logger.info(args_summary, pprint=True, indent=2) - - # Execute function - result = func(**filtered_kwargs) - - # Handle return value - if isinstance(result, int): - exit_status = result - else: - exit_status = 0 - - except Exception as e: - _decorator_logger.error(f"Error in {func.__name__}: {e}", exc_info=True) - exit_status = 1 - raise - - finally: - # Close session with error handling - try: - close( - CONFIG=CONFIG, - verbose=verbose, - notify=notify, - message=f"{func.__name__} completed", - exit_status=exit_status, - ) - except SystemExit: - # Allow normal exits - raise - except KeyboardInterrupt: - # Allow Ctrl+C - raise - except Exception as e: - # Log but don't crash on cleanup errors - try: - _decorator_logger.error(f"Session cleanup error: {e}") - except: - print(f"Session cleanup error: {e}") - - # Final matplotlib cleanup (belt and suspenders approach) - try: - import matplotlib.pyplot as plt - - plt.close("all") - except: - pass - - return result - - -def _create_parser(func: Callable) -> argparse.ArgumentParser: - """Create ArgumentParser from function signature. - - Args: - func: Function to create parser for - - Returns: - Configured ArgumentParser - """ - - # Get function info - sig = inspect.signature(func) - doc = inspect.getdoc(func) or f"Run {func.__name__}" - - # Try to get type hints - try: - type_hints = get_type_hints(func) - except Exception: - type_hints = {} - - # Get actual values for deterministic items - # Get calling file from the decorated function's module - caller_file = func.__globals__.get("__file__", "unknown.py") - - # Calculate SDIR_OUT (base output directory) - import os - - sdir_out = Path(os.path.splitext(caller_file)[0] + "_out") - sdir_run_example = sdir_out / "RUNNING" / "" - - # Get current PID - current_pid = os.getpid() - - # Check for config YAML files and list all variables with values - config_status = "" - try: - config_dir = Path("./config") - if config_dir.exists(): - yaml_files = sorted(config_dir.glob("*.yaml")) - if yaml_files: - config_status = " CONFIG from YAML files:\n" - - # Load and list all config variables with their values - try: - import yaml - - all_vars = [] - for yaml_file in yaml_files: - with open(yaml_file, "r") as f: - data = yaml.safe_load(f) - if isinstance(data, dict): - namespace = yaml_file.stem.upper() - for key, value in data.items(): - # Format value for display (truncate if too long) - value_str = str(value) - if len(value_str) > 50: - value_str = value_str[:47] + "..." - all_vars.append( - f" - CONFIG.{namespace}.{key} (from ./config/{yaml_file.name})\n {value_str}" - ) - - if all_vars: - config_status += "\n".join(all_vars) - else: - config_status = " CONFIG from YAML files:\n (no variables found)" - except Exception as e: - # If we can't load the YAML files, just show error - config_status = " CONFIG from YAML files:\n (unable to load at help-time, will be available at runtime)" - else: - config_status = ( - " CONFIG from YAML files:\n (no .yaml files found)" - ) - else: - config_status = " CONFIG from YAML files:\n (./config/ directory not found)" - except: - config_status = ( - " CONFIG from YAML files:\n (unable to check at help-time)" - ) - - # Get available color keys - try: - import matplotlib.pyplot as plt_temp - - from scitex.plt.utils._configure_mpl import configure_mpl - - _, colors_dict = configure_mpl(plt_temp) - # Show all color keys - sorted_keys = sorted(colors_dict.keys()) - color_keys = ", ".join(f"'{k}'" for k in sorted_keys) - except Exception as e: - # Fallback if configure_mpl fails - color_keys = "'blue', 'red', 'green', 'yellow', 'purple', 'orange', ..." - - # Create parser with epilog documenting injected globals with actual values - epilog = f""" -Global Variables Injected by @session Decorator: - - CONFIG (DotDict) - Session configuration with ID, paths, timestamps - Access: CONFIG['key'] or CONFIG.key (both work!) - - - CONFIG.ID - (created at runtime, e.g., '2025Y-11M-18D-07h53m37s_Z5MR') - - CONFIG.FILE - {Path(caller_file)} - - CONFIG.SDIR_OUT - {sdir_out} - - CONFIG.SDIR_RUN - {sdir_run_example} - - CONFIG.PID - {current_pid} (current Python process) - - CONFIG.ARGS - {{'arg1': ''}} (parsed from command line) - -{config_status} - - plt (module) - matplotlib.pyplot configured for session - - COLORS (DotDict) - Color palette for consistent plotting - Access: COLORS.blue or COLORS['blue'] (both work!) - - Available keys: - {color_keys} - - Usage: - plt.plot(x, y, color=COLORS.blue) - plt.plot(x, y, color=COLORS['blue']) - - rngg (RandomStateManager) - Manages reproducible randomness - - logger (SciTeXLogger) - Logger configured for your script -""" - - parser = argparse.ArgumentParser( - description=doc, - epilog=epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - # Add arguments from function signature (skip injected parameters) - # Track used short forms to avoid conflicts - used_short_forms = {"h"} # Reserve -h for help - - for param_name, param in sig.parameters.items(): - # Skip parameters with INJECTED as default (these are injected by decorator) - if param.default != inspect.Parameter.empty: - if isinstance(param.default, type(INJECTED)): - continue # Skip injected parameters - - # Generate short form - short_form = _generate_short_form(param_name, used_short_forms) - if short_form: - used_short_forms.add(short_form) - - _add_argument(parser, param_name, param, type_hints, short_form) - - return parser - - -def _generate_short_form(param_name: str, used_short_forms: set) -> str: - """Generate a short form for a parameter name avoiding conflicts. - - Args: - param_name: Full parameter name - used_short_forms: Set of already used short forms - - Returns: - Short form character or None if no unique form can be generated - """ - # Strategy 1: Try first letter - first_letter = param_name[0].lower() - if first_letter not in used_short_forms: - return first_letter - - # Strategy 2: Try first letter of each word (for snake_case or camelCase) - words = param_name.replace("_", " ").replace("-", " ").split() - if len(words) > 1: - acronym = "".join(w[0].lower() for w in words) - if len(acronym) == 1 and acronym not in used_short_forms: - return acronym - - # Strategy 3: Try first two letters - if len(param_name) >= 2: - two_letters = param_name[:2].lower() - if two_letters not in used_short_forms: - return two_letters - - # Strategy 4: Try each character in sequence - for char in param_name.lower(): - if char.isalnum() and char not in used_short_forms: - return char - - # Give up if no unique short form found - return None - - -def _add_argument( - parser: argparse.ArgumentParser, - param_name: str, - param: inspect.Parameter, - type_hints: dict, - short_form: str = None, -): - """Add single argument to parser. - - Args: - parser: ArgumentParser to add to - param_name: Parameter name - param: Parameter object - type_hints: Type hints dictionary - short_form: Optional short form (e.g., 'a' for -a) - """ - from typing import Literal, get_args, get_origin - - # Get type - param_type = type_hints.get(param_name, param.annotation) - if param_type == inspect.Parameter.empty: - param_type = str - - # Get default - has_default = param.default != inspect.Parameter.empty - default = param.default if has_default else None - - # Convert parameter name to CLI format - arg_name = f"--{param_name.replace('_', '-')}" - - # Build argument names list (long form, optionally short form) - arg_names = [arg_name] - if short_form: - arg_names.insert(0, f"-{short_form}") - - # Check for Literal type (choices) - choices = None - origin = get_origin(param_type) - if origin is Literal: - choices = list(get_args(param_type)) - param_type = type(choices[0]) if choices else str - - # Handle different types - if param_type == bool: - # Boolean flags - parser.add_argument( - *arg_names, - action="store_true" if not default else "store_false", - default=default, - help=f"(default: {default})", - ) - else: - # Regular arguments - choices_str = f", choices: {choices}" if choices else "" - kwargs = { - "type": param_type, - "help": ( - f"(default: {default}{choices_str})" - if has_default - else f"(required{choices_str})" - ), - } - - if choices: - kwargs["choices"] = choices - - if has_default: - kwargs["default"] = default - else: - kwargs["required"] = True - - parser.add_argument(*arg_names, **kwargs) - - -def run(func: Callable, parse_args: Callable = None, **session_kwargs) -> Any: - """Run function with session management. - - Alternative to decorator for more explicit control. - - Args: - func: Function to run - parse_args: Optional custom argument parser - **session_kwargs: Session configuration - - Example: - def main(args): - # Your code - return 0 - - if __name__ == '__main__': - stx.session.run(main) - """ - - if parse_args is None: - # Auto-generate parser - parser = _create_parser(func) - args = parser.parse_args() - else: - # Use custom parser - args = parse_args() - - # Get file - frame = inspect.currentframe() - caller_frame = frame.f_back - caller_file = caller_frame.f_globals.get("__file__", "unknown.py") - - # Start session - import matplotlib.pyplot as plt - - CONFIG, stdout, stderr, plt, COLORS, rngg = start( - sys=sys_module, - plt=plt, - args=args, - file=caller_file, - **session_kwargs, - ) - - # Run - try: - if hasattr(args, "__dict__"): - exit_status = func(args) - else: - exit_status = func() - - exit_status = exit_status or 0 - - except Exception as e: - _decorator_logger.error(f"Error: {e}", exc_info=True) - exit_status = 1 - raise - - finally: - close( - CONFIG=CONFIG, - exit_status=exit_status, - **session_kwargs, - ) - - return exit_status - - -# EOF diff --git a/src/scitex/session/_lifecycle/__init__.py b/src/scitex/session/_lifecycle/__init__.py deleted file mode 100755 index 12ca44f6..00000000 --- a/src/scitex/session/_lifecycle/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: "2026-02-01 (ywatanabe)" -# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/__init__.py -"""Session lifecycle management - refactored subpackage. - -This package contains the split modules from the original _lifecycle.py: -- _start.py: Session start function -- _close.py: Session close and running2finished functions -- _config.py: Configuration setup -- _matplotlib.py: Matplotlib configuration -- _utils.py: Utility functions -""" - -from ._close import close, running2finished -from ._start import start - -__all__ = [ - "start", - "close", - "running2finished", -] - -# EOF diff --git a/src/scitex/session/_lifecycle/_close.py b/src/scitex/session/_lifecycle/_close.py deleted file mode 100755 index 3ef2f411..00000000 --- a/src/scitex/session/_lifecycle/_close.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: "2026-02-01 (ywatanabe)" -# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_close.py -"""Session close functions.""" - -from __future__ import annotations - -import os -import os as _os -import shutil -import time -from glob import glob as _glob -from pathlib import Path - -from scitex.logging import getLogger -from scitex.utils._notify import notify as scitex_utils_notify - -from .._manager import get_global_session_manager -from ._config import save_configs -from ._utils import args_to_str, escape_ansi_from_log_files, process_timestamp - -logger = getLogger(__name__) - - -def running2finished(CONFIG, exit_status=None, remove_src_dir=True, max_wait=60): - """Move session from RUNNING to FINISHED directory. - - Parameters - ---------- - CONFIG : dict - Session configuration dictionary - exit_status : int, optional - Exit status code (0=success, 1=error, None=finished) - remove_src_dir : bool, default=True - Whether to remove source directory after copy - max_wait : int, default=60 - Maximum seconds to wait for copy operation - - Returns - ------- - dict - Updated configuration with new SDIR - """ - if exit_status == 0: - dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED_SUCCESS/") - elif exit_status == 1: - dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED_ERROR/") - else: - dest_dir = str(CONFIG["SDIR_RUN"]).replace("RUNNING/", "FINISHED/") - - src_dir = str(CONFIG["SDIR_RUN"]) - _os.makedirs(dest_dir, exist_ok=True) - try: - # Copy files individually - for item in _os.listdir(src_dir): - s = _os.path.join(src_dir, item) - d = _os.path.join(dest_dir, item) - if _os.path.isdir(s): - shutil.copytree(s, d) - else: - shutil.copy2(s, d) - - start_time = time.time() - while not _os.path.exists(dest_dir) and time.time() - start_time < max_wait: - time.sleep(0.1) - if _os.path.exists(dest_dir): - print() - if exit_status == 1: - logger.error( - f"Script failed: {dest_dir}", - ) - elif exit_status == 0: - logger.success( - f"Congratulations! The script completed: {dest_dir}", - ) - else: - logger.info( - f"Script finished: {dest_dir}", - ) - - if remove_src_dir: - shutil.rmtree(src_dir) - - # Cleanup RUNNING when empty - running_base = os.path.dirname(src_dir.rstrip("/")) - if os.path.basename(running_base) == "RUNNING": - try: - os.rmdir(running_base) - except OSError: - pass - - else: - print(f"Copy operation timed out after {max_wait} seconds") - - CONFIG["SDIR_RUN"] = Path(dest_dir) - except Exception as e: - print(e) - - finally: - return CONFIG - - -def close(CONFIG, message=":)", notify=False, verbose=True, exit_status=None): - """Close experiment session and finalize logging. - - Parameters - ---------- - CONFIG : DotDict - Configuration dictionary from start() - message : str, default=':)' - Completion message - notify : bool, default=False - Whether to send notification - verbose : bool, default=True - Whether to print verbose output - exit_status : int, optional - Exit status code (0=success, 1=error, None=finished) - """ - # Stop verification tracking first - _stop_verification(exit_status) - - sys = None - try: - CONFIG.EXIT_STATUS = exit_status - CONFIG = CONFIG.to_dict() - CONFIG = process_timestamp(CONFIG, verbose=verbose) - sys = CONFIG.pop("_sys", None) - - # CRITICAL: Close matplotlib BEFORE closing streams to prevent segfault - _cleanup_matplotlib(verbose) - - save_configs(CONFIG) - - # RUNNING to FINISHED - CONFIG = running2finished(CONFIG, exit_status=exit_status) - - # ANSI code escape - log_files = _glob(str(CONFIG["SDIR_RUN"]) + "logs/*.log") - escape_ansi_from_log_files(log_files) - - if CONFIG.get("ARGS"): - message += f"\n{args_to_str(CONFIG.get('ARGS'))}" - - if notify: - try: - message = ( - "[DEBUG]\n" + str(message) - if CONFIG.get("DEBUG", False) - else str(message) - ) - scitex_utils_notify( - message=message, - ID=CONFIG["ID"], - file=CONFIG.get("FILE"), - attachment_paths=log_files, - verbose=verbose, - ) - except Exception as e: - print(e) - - # Close session - session_manager = get_global_session_manager() - session_manager.close_session(CONFIG["ID"]) - - finally: - # Only close if they're custom file objects (Tee objects) - if sys: - _close_streams(sys) - - -def _cleanup_matplotlib(verbose: bool) -> None: - """Clean up matplotlib resources.""" - try: - import gc - - import matplotlib - import matplotlib.pyplot as plt - - # Close all figures - plt.close("all") - - # CRITICAL: Unregister matplotlib's atexit handlers to prevent segfault - try: - if hasattr(matplotlib, "_pylab_helpers"): - matplotlib._pylab_helpers.Gcf.destroy_all() - - if hasattr(plt, "get_fignums"): - for fignum in plt.get_fignums(): - plt.close(fignum) - - except Exception: - pass - - # Force garbage collection - gc.collect() - - if verbose: - logger.info("Matplotlib cleanup completed") - - except Exception as e: - if verbose: - logger.warning(f"Could not close matplotlib: {e}") - - -def _close_streams(sys) -> None: - """Close tee-wrapped streams.""" - try: - # First, flush all outputs - if hasattr(sys, "stdout") and hasattr(sys.stdout, "flush"): - sys.stdout.flush() - if hasattr(sys, "stderr") and hasattr(sys.stderr, "flush"): - sys.stderr.flush() - - # Then close Tee objects - if hasattr(sys, "stdout") and hasattr(sys.stdout, "_log_file"): - sys.stdout.close() - if hasattr(sys, "stderr") and hasattr(sys.stderr, "_log_file"): - sys.stderr.close() - except Exception: - pass - - -def _stop_verification(exit_status: int) -> None: - """Stop verification tracking for this session.""" - try: - from scitex.clew import on_session_close - - status = "success" if exit_status == 0 else "failed" - on_session_close(status=status, exit_code=exit_status or 0) - except Exception: - pass - - -# EOF diff --git a/src/scitex/session/_lifecycle/_config.py b/src/scitex/session/_lifecycle/_config.py deleted file mode 100755 index 67d12456..00000000 --- a/src/scitex/session/_lifecycle/_config.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: "2026-02-01 (ywatanabe)" -# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_config.py -"""Configuration setup for session lifecycle.""" - -from __future__ import annotations - -from datetime import datetime -from pathlib import Path -from typing import Any, Dict - -from scitex.logging import getLogger - -logger = getLogger(__name__) - - -def setup_configs( - IS_DEBUG: bool, - ID: str, - PID: int, - file: str, - sdir: str, - relative_sdir: str, - verbose: bool, -) -> Dict[str, Any]: - """Setup configuration dictionary with basic parameters. - - Parameters - ---------- - IS_DEBUG : bool - Debug mode flag - ID : str - Unique identifier - PID : int - Process ID - file : str - File path - sdir : str - Save directory path - relative_sdir : str - Relative save directory path - verbose : bool - Verbosity flag - - Returns - ------- - dict - Configuration dictionary - """ - # Calculate SDIR_OUT (base output directory) - # sdir format: /path/to/script_out/RUNNING/ID/ - sdir_path = Path(sdir) if sdir else None - if sdir_path: - # Remove /RUNNING/ID/ to get base output dir - parts = sdir_path.parts - if "RUNNING" in parts: - running_idx = parts.index("RUNNING") - sdir_out = Path(*parts[:running_idx]) - else: - sdir_out = sdir_path.parent - else: - sdir_out = None - - # Load YAML configs from ./config/*.yaml - from scitex.io import load_configs - - CONFIGS = load_configs(IS_DEBUG).to_dict() - - # Add session-specific config with clean structure (Path objects only) - CONFIGS.update( - { - "ID": ID, - "PID": PID, - "START_DATETIME": datetime.now(), - "FILE": Path(file) if file else None, - "SDIR_OUT": sdir_out, - "SDIR_RUN": sdir_path, - } - ) - return CONFIGS - - -def save_configs(CONFIG) -> None: - """Save configuration to files. - - Note: track=False prevents verification tracking of CONFIG files, - which would cause false "missing" errors since these files are saved - in RUNNING/ but then moved to FINISHED_SUCCESS/. - """ - from scitex.io._save import save as scitex_io_save - - # Convert to dict with all keys (including private ones) for saving - config_dict = ( - CONFIG.to_dict(include_private=True) if hasattr(CONFIG, "to_dict") else CONFIG - ) - - # track=False: Don't track internal config files in verification DB - scitex_io_save( - config_dict, - str(CONFIG["SDIR_RUN"] / "CONFIGS/CONFIG.pkl"), - verbose=False, - track=False, - ) - scitex_io_save( - config_dict, - str(CONFIG["SDIR_RUN"] / "CONFIGS/CONFIG.yaml"), - verbose=False, - track=False, - ) - - -# EOF diff --git a/src/scitex/session/_lifecycle/_matplotlib.py b/src/scitex/session/_lifecycle/_matplotlib.py deleted file mode 100755 index fe59a6f3..00000000 --- a/src/scitex/session/_lifecycle/_matplotlib.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: "2026-02-01 (ywatanabe)" -# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_matplotlib.py -"""Matplotlib configuration for session lifecycle.""" - -from __future__ import annotations - -import os -import platform -import sys -from typing import Any, Dict, Optional, Tuple - -import matplotlib - -from scitex.logging import getLogger - -logger = getLogger(__name__) - -# Detect headless/WSL environments -is_headless = False -try: - # Check for WSL - if "microsoft" in platform.uname().release.lower() or "WSL" in os.environ.get( - "WSL_DISTRO_NAME", "" - ): - is_headless = True - # Check for no X11 display - elif not os.environ.get("DISPLAY"): - is_headless = True -except Exception: - # Fallback: if on Linux without DISPLAY, assume headless - if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): - is_headless = True - -if is_headless: - matplotlib.use("Agg") - - -from scitex.plt.utils._configure_mpl import configure_mpl - - -def setup_matplotlib( - plt=None, agg: bool = False, **mpl_kwargs: Any -) -> Tuple[Any, Optional[Dict[str, Any]]]: - """Configure matplotlib settings. - - Parameters - ---------- - plt : module - Matplotlib.pyplot module (will be replaced with scitex.plt) - agg : bool - Whether to use Agg backend - **mpl_kwargs : dict - Additional matplotlib configuration parameters - - Returns - ------- - tuple - (plt, COLORS) - Configured scitex.plt module and color cycle - """ - if plt is not None: - plt.close("all") - _, COLORS = configure_mpl(plt, **mpl_kwargs) - COLORS["gray"] = COLORS["grey"] - - # Note: Backend is now set early in module initialization - # to avoid tkinter threading issues in headless/WSL environments. - # The 'agg' parameter is kept for backwards compatibility but has - # no effect since backend must be set before pyplot import. - if agg and not is_headless: - logger.warning( - "agg=True specified but backend was already set to Agg " - "during module initialization for headless environment" - ) - - # Replace matplotlib.pyplot with scitex.plt to get wrapped functions - import scitex.plt as stx_plt - - return stx_plt, COLORS - return plt, None - - -# EOF diff --git a/src/scitex/session/_lifecycle/_start.py b/src/scitex/session/_lifecycle/_start.py deleted file mode 100755 index 95984728..00000000 --- a/src/scitex/session/_lifecycle/_start.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: "2026-02-01 (ywatanabe)" -# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_start.py -"""Session start function.""" - -from __future__ import annotations - -import inspect -import logging -import os as _os -from pathlib import Path -from typing import Any, Dict, Optional, Tuple, Union - -from scitex.dict import DotDict -from scitex.logging import getLogger -from scitex.repro import RandomStateManager -from scitex.str import clean_path - -from .._manager import get_global_session_manager -from ._config import setup_configs -from ._matplotlib import setup_matplotlib -from ._utils import ( - clear_python_log_dir, - get_debug_mode, - initialize_env, - print_header, - simplify_relative_path, -) - -logger = getLogger(__name__) - -# For development code flow analysis -try: - from scitex.dev._analyze_code_flow import analyze_code_flow -except ImportError: - - def analyze_code_flow(file): - return "Code flow analysis not available" - - -def start( - sys=None, - plt=None, - file: Optional[str] = None, - sdir: Optional[Union[str, Path]] = None, - sdir_suffix: Optional[str] = None, - args: Optional[Any] = None, - os: Optional[Any] = None, - random: Optional[Any] = None, - np: Optional[Any] = None, - torch: Optional[Any] = None, - seed: int = 42, - agg: bool = False, - fig_size_mm: Tuple[int, int] = (160, 100), - fig_scale: float = 1.0, - dpi_display: int = 100, - dpi_save: int = 300, - fontsize="small", - autolayout=False, - show_execution_flow=False, - hide_top_right_spines: bool = True, - alpha: float = 0.9, - line_width: float = 1.0, - clear_logs: bool = False, - verbose: bool = True, -) -> Tuple[DotDict, Any, Any, Any, Optional[Dict[str, Any]], Any]: - """Initialize experiment session with reproducibility settings. - - Parameters - ---------- - sys : module, optional - Python sys module for I/O redirection - plt : module, optional - Matplotlib pyplot module for plotting configuration - file : str, optional - Script file path. If None, automatically detected - sdir : Union[str, Path], optional - Save directory path - sdir_suffix : str, optional - Suffix to append to save directory - args : object, optional - Command line arguments or configuration object - seed : int, default=42 - Random seed for reproducibility - agg : bool, default=False - Whether to use matplotlib Agg backend - verbose : bool, default=True - Whether to print detailed information - - Returns - ------- - tuple - (CONFIGS, stdout, stderr, plt, COLORS, rng) - """ - IS_DEBUG = get_debug_mode() - ID, PID = initialize_env(IS_DEBUG) - - # Convert Path objects to strings for internal processing - if sdir is not None and isinstance(sdir, Path): - sdir = str(sdir) - - # Defines SDIR - if sdir is None: - # Define __file__ - if file: - caller_file = file - else: - caller_file = inspect.stack()[1].filename - if "ipython" in caller_file.lower(): - try: - from scitex.gen._detect_notebook_path import get_notebook_path - - nb_path = get_notebook_path() - caller_file = ( - nb_path if nb_path else f"/tmp/{_os.getenv('USER')}.py" - ) - except Exception: - caller_file = f"/tmp/{_os.getenv('USER')}.py" - - # Convert to absolute path if relative and resolve symlinks - if not _os.path.isabs(caller_file): - caller_file = _os.path.realpath(_os.path.abspath(caller_file)) - else: - caller_file = _os.path.realpath(caller_file) - - # Define sdir - sdir = clean_path(_os.path.splitext(caller_file)[0] + f"_out/RUNNING/{ID}/") - - # Optional - if sdir_suffix: - sdir = sdir[:-1] + f"-{sdir_suffix}/" - else: - caller_file = file - - if clear_logs and caller_file: - clear_python_log_dir(sdir + caller_file + "/") - _os.makedirs(sdir, exist_ok=True) - relative_sdir = simplify_relative_path(sdir) - - # Setup configs - use caller_file (computed) instead of file (parameter) - CONFIGS = setup_configs( - IS_DEBUG, ID, PID, caller_file, sdir, relative_sdir, verbose - ) - - # Logging - if sys is not None: - from scitex_io._flush import flush - - from scitex.logging import tee - - flush(sys) - sys.stdout, sys.stderr = tee(sys, sdir=sdir, verbose=verbose) - CONFIGS["_sys"] = sys - - # Redirect logging handlers to use the tee-wrapped streams - _redirect_logging_handlers(sys) - - # Initialize RandomStateManager - rng = RandomStateManager(seed=seed, verbose=verbose) - if verbose: - logger.info(f"Initialized RandomStateManager with seed {seed}") - - # Matplotlib configurations - plt, COLORS = setup_matplotlib( - plt, - agg, - fig_size_mm=fig_size_mm, - fig_scale=fig_scale, - dpi_display=dpi_display, - dpi_save=dpi_save, - hide_top_right_spines=hide_top_right_spines, - alpha=alpha, - line_width=line_width, - fontsize=fontsize, - autolayout=autolayout, - verbose=verbose, - ) - - # Adds argument-parsed variables - if args is not None: - CONFIGS["ARGS"] = vars(args) if hasattr(args, "__dict__") else args - - CONFIGS = DotDict(CONFIGS) - - # Register session - session_manager = get_global_session_manager() - session_manager.create_session(ID, CONFIGS) - - print_header(ID, PID, caller_file, args, CONFIGS, verbose) - - if show_execution_flow: - from scitex.str import printc as _printc - - structure = analyze_code_flow(caller_file) - _printc(structure) - - # Start verification tracking - _start_verification(CONFIGS) - - # Return appropriate values based on whether sys was provided - if sys is not None: - return CONFIGS, sys.stdout, sys.stderr, plt, COLORS, rng - else: - return CONFIGS, None, None, plt, COLORS, rng - - -def _redirect_logging_handlers(sys) -> None: - """Redirect logging handlers to use tee-wrapped streams.""" - # Update all existing StreamHandler instances - for logger_name in list(logging.Logger.manager.loggerDict.keys()): - try: - lgr = logging.getLogger(logger_name) - for handler in lgr.handlers: - if isinstance(handler, logging.StreamHandler): - if not hasattr(handler, "stream"): - continue - if handler.stream in (sys.__stderr__, sys.__stdout__): - handler.stream = ( - sys.stderr - if handler.stream == sys.__stderr__ - else sys.stdout - ) - except Exception: - pass - - # Also update the root logger handlers - try: - root_logger = logging.getLogger() - for handler in root_logger.handlers: - if isinstance(handler, logging.StreamHandler): - if not hasattr(handler, "stream"): - continue - if handler.stream in (sys.__stderr__, sys.__stdout__): - handler.stream = ( - sys.stderr if handler.stream == sys.__stderr__ else sys.stdout - ) - except Exception: - pass - - -def _start_verification(CONFIG) -> None: - """Start verification tracking for this session.""" - try: - from scitex.clew import on_session_start - - session_id = CONFIG.get("ID", "unknown") - file_path = CONFIG.get("FILE") - if file_path is not None: - file_path = str(file_path) - - metadata = None - if file_path and file_path.endswith(".ipynb"): - metadata = {"notebook_path": file_path} - - on_session_start( - session_id=session_id, script_path=file_path, metadata=metadata - ) - except Exception: - pass - - -# EOF diff --git a/src/scitex/session/_lifecycle/_utils.py b/src/scitex/session/_lifecycle/_utils.py deleted file mode 100755 index 241c60ea..00000000 --- a/src/scitex/session/_lifecycle/_utils.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: "2026-02-01 (ywatanabe)" -# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_lifecycle/_utils.py -"""Utility functions for session lifecycle.""" - -from __future__ import annotations - -import os as _os -import re -from datetime import datetime -from time import sleep -from typing import Any, Dict, Tuple - -from scitex.logging import getLogger -from scitex.repro import gen_ID -from scitex.str import printc as _printc - -logger = getLogger(__name__) - - -def get_scitex_version() -> str: - """Gets scitex version.""" - try: - import scitex - - return scitex.__version__ - except Exception as e: - print(e) - return "(not found)" - - -def get_debug_mode() -> bool: - """Get debug mode from configuration.""" - try: - from scitex.io._load import load - - IS_DEBUG_PATH = "./config/IS_DEBUG.yaml" - if _os.path.exists(IS_DEBUG_PATH): - IS_DEBUG = load(IS_DEBUG_PATH).get("IS_DEBUG", False) - if IS_DEBUG == "true": - IS_DEBUG = True - else: - IS_DEBUG = False - - except Exception as e: - print(e) - IS_DEBUG = False - return IS_DEBUG - - -def initialize_env(IS_DEBUG: bool) -> Tuple[str, int]: - """Initialize environment with ID and PID. - - Parameters - ---------- - IS_DEBUG : bool - Debug mode flag - - Returns - ------- - tuple - (ID, PID) - Unique identifier and Process ID - """ - ID = gen_ID(N=4) if not IS_DEBUG else "DEBUG_" + gen_ID(N=4) - PID = _os.getpid() - return ID, PID - - -def simplify_relative_path(sdir: str) -> str: - """Simplify the relative path by removing specific patterns. - - Parameters - ---------- - sdir : str - The directory path to simplify - - Returns - ------- - str - Simplified relative path - """ - base_path = _os.getcwd() - relative_sdir = _os.path.relpath(sdir, base_path) if base_path else sdir - simplified_path = relative_sdir.replace("scripts/", "./scripts/").replace( - "RUNNING/", "" - ) - # Remove date-time pattern and random string - simplified_path = re.sub( - r"\d{4}Y-\d{2}M-\d{2}D-\d{2}h\d{2}m\d{2}s_\w+/?$", "", simplified_path - ) - return simplified_path - - -def clear_python_log_dir(log_dir: str) -> None: - """Clear Python log directory.""" - try: - if _os.path.exists(log_dir): - _os.system(f"rm -rf {log_dir}") - except Exception as e: - print(f"Failed to clear directory {log_dir}: {e}") - - -def print_header( - ID: str, - PID: int, - file: str, - args: Any, - configs: Dict[str, Any], - verbose: bool = True, -) -> None: - """Prints formatted header with scitex version, ID, and PID information.""" - if args is not None and hasattr(args, "_get_kwargs"): - args_str = "Arguments:" - for arg, value in args._get_kwargs(): - args_str += f"\n {arg}: {value}" - else: - args_str = "Arguments: None" - - _printc( - (f"SciTeX v{get_scitex_version()} | {ID} (PID: {PID})\n\n{file}\n\n{args_str}"), - char="=", - ) - - sleep(1) - if verbose: - from pprint import pformat - - config_str = pformat(configs.to_dict()) - logger.info(f"\n{'-' * 40}\n\n{config_str}\n\n{'-' * 40}\n") - sleep(1) - - -def format_diff_time(diff_time) -> str: - """Format time difference as HH:MM:SS.""" - total_seconds = int(diff_time.total_seconds()) - hours = total_seconds // 3600 - minutes = (total_seconds % 3600) // 60 - seconds = total_seconds % 60 - return f"{hours:02d}:{minutes:02d}:{seconds:02d}" - - -def process_timestamp(CONFIG, verbose=True): - """Process session timestamps.""" - try: - CONFIG["END_DATETIME"] = datetime.now() - CONFIG["RUN_DURATION"] = format_diff_time( - CONFIG["END_DATETIME"] - CONFIG["START_DATETIME"] - ) - if verbose: - logger.info( - f"\nSTART TIME: {CONFIG['START_DATETIME']}\n" - f"END TIME: {CONFIG['END_DATETIME']}\n" - f"RUN DURATION: {CONFIG['RUN_DURATION']}\n" - ) - - except Exception as e: - print(e) - - return CONFIG - - -def args_to_str(args_dict) -> str: - """Convert args dictionary to formatted string.""" - if args_dict: - max_key_length = max(len(str(k)) for k in args_dict.keys()) - return "\n".join( - f"{str(k):<{max_key_length}} : {str(v)}" - for k, v in sorted(args_dict.items()) - ) - else: - return "" - - -def escape_ansi_from_log_files(log_files) -> None: - """Remove ANSI escape sequences from log files.""" - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") - - for f in log_files: - with open(f, encoding="utf-8") as file: - content = file.read() - cleaned_content = ansi_escape.sub("", content) - with open(f, "w", encoding="utf-8") as file: - file.write(cleaned_content) - - -# EOF diff --git a/src/scitex/session/_manager.py b/src/scitex/session/_manager.py deleted file mode 100755 index 4b5adb83..00000000 --- a/src/scitex/session/_manager.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: "2025-08-21 20:36:50 (ywatanabe)" -# File: /home/ywatanabe/proj/SciTeX-Code/src/scitex/session/_manager.py -# ---------------------------------------- -from __future__ import annotations - -import os - -__FILE__ = __file__ -__DIR__ = os.path.dirname(__FILE__) -# ---------------------------------------- - -"""Session manager for tracking active experiment sessions.""" - -from datetime import datetime -from typing import Any, Dict - - -class SessionManager: - """Manages experiment sessions with tracking and lifecycle management.""" - - def __init__(self): - self.active_sessions = {} - - def create_session( - self, - session_id: str, - config: Dict[str, Any], - script_path: str = None, - ) -> None: - """Register a new session. - - Parameters - ---------- - session_id : str - Unique identifier for the session - config : Dict[str, Any] - Session configuration dictionary - script_path : str, optional - Path to the script being run - """ - self.active_sessions[session_id] = { - "config": config, - "start_time": datetime.now(), - "status": "running", - "script_path": script_path, - } - - # Start verification tracking (silent fail) - try: - from scitex.clew import on_session_start - - on_session_start( - session_id=session_id, - script_path=script_path, - ) - except Exception: - pass - - def close_session( - self, - session_id: str, - status: str = "success", - exit_code: int = 0, - ) -> None: - """Mark a session as closed. - - Parameters - ---------- - session_id : str - Unique identifier for the session to close - status : str, optional - Final status (success, failed, error) - exit_code : int, optional - Exit code of the session - """ - if session_id in self.active_sessions: - self.active_sessions[session_id]["status"] = "closed" - self.active_sessions[session_id]["end_time"] = datetime.now() - self.active_sessions[session_id]["exit_code"] = exit_code - - # Stop verification tracking (silent fail) - try: - from scitex.clew import on_session_close - - on_session_close(status=status, exit_code=exit_code) - except Exception: - pass - - def get_active_sessions(self) -> Dict[str, Any]: - """Get all active sessions. - - Returns - ------- - Dict[str, Any] - Dictionary of active session information - """ - return { - k: v for k, v in self.active_sessions.items() if v["status"] == "running" - } - - def get_session(self, session_id: str) -> Dict[str, Any]: - """Get specific session information. - - Parameters - ---------- - session_id : str - Session ID to retrieve - - Returns - ------- - Dict[str, Any] - Session information dictionary - """ - return self.active_sessions.get(session_id, {}) - - def list_sessions(self) -> Dict[str, Any]: - """Get all sessions (active and closed). - - Returns - ------- - Dict[str, Any] - Dictionary of all session information - """ - return self.active_sessions.copy() - - -# Global session manager -_session_manager = SessionManager() - - -def get_global_session_manager() -> SessionManager: - """Get the global session manager instance. - - Returns - ------- - SessionManager - Global session manager instance - """ - return _session_manager - - -# EOF diff --git a/src/scitex/session/_skills/SKILL.md b/src/scitex/session/_skills/SKILL.md deleted file mode 100644 index 99003f6c..00000000 --- a/src/scitex/session/_skills/SKILL.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -name: stx.session -description: Experiment session management with auto-organized outputs, config injection, and reproducibility tracking. ---- - -# stx.session - -The `stx.session` module manages the full lifecycle of a reproducible experiment: -output directories, logging, matplotlib configuration, YAML config loading, CLI -generation, and clean teardown. The primary interface is the `@stx.session` -decorator. - -## Sub-skills - -### Core Usage -- [decorator.md](decorator.md) — `@stx.session` decorator: behavior, options, direct vs CLI mode, full script template -- [cli-generation.md](cli-generation.md) — Auto-ArgumentParser from function signatures: type mapping, short forms, `Literal` choices, `--help` epilog - -### Session Internals -- [lifecycle.md](lifecycle.md) — `start()` / `close()` / `running2finished()`: what each does, output directory structure, CONFIG keys -- [config.md](config.md) — CONFIG DotDict: YAML namespacing, session keys, CLI args, debug mode, persistence -- [injected-globals.md](injected-globals.md) — The five injected objects (CONFIG, plt, COLORS, rngg, logger): origin, usage, INJECTED sentinel - -### Advanced -- [session-manager.md](session-manager.md) — `SessionManager` class: tracking concurrent sessions, global singleton, clew hooks - -## Quick Reference - -```python -import scitex as stx - -# Primary API: decorator -@stx.session -def main( - data_path: str, - threshold: float = 0.5, - CONFIG=stx.INJECTED, - logger=stx.INJECTED, -): - """Analyze data. Becomes --help description.""" - logger.info(f"Output: {CONFIG.SDIR_RUN}") - stx.io.save(process(data_path, threshold), "result.csv") - return 0 # exit code - -if __name__ == "__main__": - main() # no args -> CLI mode with full session management - -# Manual API -import sys, matplotlib.pyplot as plt -CONFIG, sys.stdout, sys.stderr, plt, COLORS, rng = stx.session.start(sys, plt) -# ... experiment code ... -stx.session.close(CONFIG) - -# Session manager -manager = stx.session.SessionManager() -active = manager.get_active_sessions() -``` - -## Public API - -| Symbol | Description | -|---|---| -| `stx.session` / `@stx.session` | Decorator (also callable as `stx.session(verbose=True)`) | -| `stx.session.start(...)` | Manual session start; returns `(CONFIG, stdout, stderr, plt, COLORS, rng)` | -| `stx.session.close(CONFIG, ...)` | Manual session close; promotes dir to `FINISHED_*` | -| `stx.session.running2finished(CONFIG, exit_status)` | Move output dir from `RUNNING/` to `FINISHED_*/` | -| `stx.session.run(func, ...)` | Explicit alternative to decorator | -| `stx.session.SessionManager` | Class for tracking multiple sessions | -| `stx.session.INJECTED` | Sentinel to mark decorator-injected parameters | -| `stx.INJECTED` | Same sentinel, re-exported from `scitex.__init__` | diff --git a/src/scitex/session/_skills/cli-generation.md b/src/scitex/session/_skills/cli-generation.md deleted file mode 100644 index e809357f..00000000 --- a/src/scitex/session/_skills/cli-generation.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -description: How @stx.session auto-generates an ArgumentParser from function signatures, including type inference, short forms, boolean flags, and Literal choices. ---- - -# Automatic CLI Generation - -`_decorator.py:_create_parser(func)` builds an `argparse.ArgumentParser` directly -from the function's signature and type hints. Every parameter that is **not** -`INJECTED`-defaulted becomes a CLI argument. - -## How Parameters Map to Arguments - -| Python parameter | CLI form | -|---|---| -| `data_path: str` | `--data-path` (underscores become hyphens) | -| `n_epochs: int = 100` | `--n-epochs` with default 100 | -| `verbose: bool = False` | `--verbose` flag (`store_true`) | -| `verbose: bool = True` | `--verbose` flag (`store_false`) | -| `mode: Literal["train","eval"] = "train"` | `--mode` with `choices=["train","eval"]` | -| `CONFIG=stx.INJECTED` | **skipped** — not in CLI | - -Parameters without defaults are marked `required=True` in argparse. - -## Short Forms - -`_generate_short_form(param_name, used_short_forms)` attempts to assign a single- -character short flag (`-x`) to each parameter, avoiding conflicts: - -1. First letter of param name (e.g., `data_path` → `-d`) -2. Acronym of snake_case words (e.g., `data_path` → `-dp` if `-d` taken, but only - if result is a single character) -3. First two letters (`da`) -4. Each character in sequence until an unused one is found -5. No short form if all are taken - -`-h` is always reserved for `--help`. - -## Type Inference - -Types come from `get_type_hints(func)` first, then `param.annotation`, then `str` -as fallback if `inspect.Parameter.empty`. - -`typing.Literal` is handled specially: choices are extracted with `get_args()` and -the base type is inferred from the first choice value. - -## Help Text in `--help` - -The function's docstring becomes the parser `description`. An `epilog` is generated -at help-time that documents all five injected globals with their actual values: - -- `CONFIG.ID` — example session ID format -- `CONFIG.FILE` — absolute path to the calling script -- `CONFIG.SDIR_OUT` — computed output base directory -- `CONFIG.SDIR_RUN` — running session subdirectory -- `CONFIG.PID` — current process ID -- YAML config variables from `./config/*.yaml` (loaded and listed with values) -- `COLORS` available keys (loaded from `configure_mpl`) - -## Example - -Given: - -```python -@stx.session -def analyze( - data_path: str, - threshold: float = 0.5, - n_samples: int = 1000, - mode: Literal["fast", "accurate"] = "fast", - debug: bool = False, - CONFIG=stx.INJECTED, -): - """Analyze dataset.""" - ... -``` - -Generated CLI: - -``` -usage: script.py [-h] [-d DATA_PATH] [-t THRESHOLD] [-n N_SAMPLES] - [-m {fast,accurate}] [--debug] - -Analyze dataset. - -options: - -h, --help show this help message and exit - -d DATA_PATH, --data-path DATA_PATH - (required) - -t THRESHOLD, --threshold THRESHOLD - (default: 0.5) - -n N_SAMPLES, --n-samples N_SAMPLES - (default: 1000) - -m {fast,accurate}, --mode {fast,accurate} - (default: fast, choices: ['fast', 'accurate']) - --debug (default: False) -``` - -## Accessing Parsed Args in Function - -CLI args land in `CONFIG['ARGS']` as a dict: - -```python -@stx.session -def main(threshold: float = 0.5, CONFIG=stx.INJECTED): - print(CONFIG['ARGS']) - # {'threshold': 0.5} - print(CONFIG.ARGS.threshold) # DotDict access also works -``` diff --git a/src/scitex/session/_skills/config.md b/src/scitex/session/_skills/config.md deleted file mode 100644 index 2b682736..00000000 --- a/src/scitex/session/_skills/config.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -description: How session CONFIG is built from YAML files, session keys, and CLI args; DotDict access patterns; and config persistence. ---- - -# Session Configuration (CONFIG) - -CONFIG is a `scitex.dict.DotDict` assembled by `_lifecycle/_config.py:setup_configs()` -during `start()`. It combines YAML files from `./config/`, session-specific metadata, -and parsed CLI arguments. - -## YAML Loading - -`setup_configs()` calls `scitex.io.load_configs(IS_DEBUG)` which reads all -`./config/*.yaml` files. Each YAML file is namespaced by its filename (uppercased): - -``` -./config/ - PARAMS.yaml -> CONFIG.PARAMS.* - IS_DEBUG.yaml -> CONFIG.IS_DEBUG (or top-level bool) - PATHS.yaml -> CONFIG.PATHS.* -``` - -Example `./config/PARAMS.yaml`: - -```yaml -lr: 1e-3 -n_epochs: 100 -batch_size: 32 -``` - -Accessed in code: - -```python -@stx.session -def main(CONFIG=stx.INJECTED): - lr = CONFIG.PARAMS.lr # 0.001 - epochs = CONFIG['PARAMS']['n_epochs'] # 100 (dict-style also works) -``` - -## Session Keys Added by `start()` - -These keys are always present after `start()`: - -```python -CONFIG.ID # "2025Y-11M-18D-07h53m37s_Z5MR" -CONFIG.PID # 12345 -CONFIG.START_DATETIME # datetime(2025, 11, 18, 7, 53, 37) -CONFIG.FILE # Path("/path/to/script.py") -CONFIG.SDIR_OUT # Path("/path/to/script_out") -CONFIG.SDIR_RUN # Path("/path/to/script_out/RUNNING/2025Y-.../") -``` - -`CONFIG.DEBUG` is also set from `IS_DEBUG.yaml`; if `True` the session ID has a -`DEBUG_` prefix. - -## CLI Args in CONFIG.ARGS - -When `args` is passed to `start()` (done automatically by the decorator), parsed -CLI arguments are stored under `CONFIG.ARGS`: - -```python -@stx.session -def main(threshold: float = 0.5, CONFIG=stx.INJECTED): - print(CONFIG.ARGS) # {'threshold': 0.5} - print(CONFIG.ARGS.threshold) # 0.5 (DotDict access) -``` - -## DotDict Access Patterns - -Both attribute access and dict access work identically: - -```python -CONFIG['ID'] # string -CONFIG.ID # same thing - -CONFIG['PARAMS']['lr'] # nested dict-style -CONFIG.PARAMS.lr # nested attribute-style -``` - -## Debug Mode - -If `./config/IS_DEBUG.yaml` contains `IS_DEBUG: true`, the session ID is prefixed: - -``` -DEBUG_2025Y-11M-18D-07h53m37s_Z5MR -``` - -This propagates to the output directory name so debug runs are visually distinct. - -## Config Persistence - -At `close()` time, `save_configs()` writes CONFIG to two files in the session -output directory: - -``` -/CONFIGS/CONFIG.pkl # Python pickle (full fidelity) -/CONFIGS/CONFIG.yaml # Human-readable YAML snapshot -``` - -Both files use `track=False` so the verification system does not flag them as -"missing" after the `RUNNING/` → `FINISHED_SUCCESS/` directory move. - -## Using CONFIG in the --help Epilog - -The decorator's `_create_parser()` reads `./config/*.yaml` at help-time and shows -all variable names and values in the `--help` output under "Global Variables -Injected by @session Decorator". Values longer than 50 characters are truncated -with `...`. diff --git a/src/scitex/session/_skills/decorator.md b/src/scitex/session/_skills/decorator.md deleted file mode 100644 index 44d2ae08..00000000 --- a/src/scitex/session/_skills/decorator.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -description: The @stx.session decorator: automatic CLI generation, session lifecycle, global injection, and error handling for experiment entry points. ---- - -# @stx.session Decorator - -The `@stx.session` decorator (defined in `_decorator.py`) wraps a function into a -self-contained experiment entry point. When the wrapped function is called with no -arguments (i.e., from `if __name__ == "__main__":`), the decorator takes over: it -parses CLI arguments, starts a session, injects globals, runs the function, then -closes the session cleanly. - -## Signature - -```python -def session( - func: Callable = None, - *, - verbose: bool = False, - agg: bool = True, - notify: bool = False, - sdir_suffix: str = None, - **session_kwargs, -) -> Callable: -``` - -## Two Invocation Styles - -```python -# Style 1: bare decorator (no options) -@stx.session -def main(threshold: float = 0.5): - ... - -# Style 2: decorator with options -@stx.session(verbose=True, notify=True) -def main(threshold: float = 0.5): - ... -``` - -Both are supported. When called with options, `session()` returns a decorator; -when called bare, `session(func)` wraps directly. - -## Behavior When Called Without Arguments (CLI Mode) - -1. Reads `__file__` from the calling frame to determine the script path. -2. Builds an `argparse.ArgumentParser` from the function signature (see - [cli-generation.md](cli-generation.md)). -3. Calls `session.start(...)` — sets up output directories, logging, matplotlib, - `RandomStateManager`, and YAML configs. -4. Injects five globals into the function's module namespace: - - `CONFIG` — `DotDict` with session ID, paths, YAML data, and parsed args - - `plt` — `scitex.plt` module, configured for the session - - `COLORS` — `DotDict` of named colors from `configure_mpl` - - `rngg` — `RandomStateManager` instance (seed=42 by default) - - `logger` — `SciTeXLogger` bound to `func.__module__` -5. Executes the function, passing only the parameters it declares (CLI args - plus any `INJECTED`-defaulted params). -6. On success, return value is used as exit code (int expected; 0 assumed if None). -7. On error, logs exception and re-raises. -8. In the `finally` block, calls `session.close(CONFIG, ...)` regardless of - outcome, then calls `plt.close("all")`. - -## Behavior When Called With Arguments (Direct Mode) - -```python -# Bypasses all session management and calls the function directly. -main("/path/to/data.csv", threshold=0.7) -``` - -The wrapper checks `if args or kwargs:` and short-circuits to `func(*args, **kwargs)`. - -## Injected Globals vs Declared Parameters - -Parameters whose default is `stx.INJECTED` (the `_InjectedSentinel` instance) are -recognized during injection. They are **skipped** during CLI argument registration -and filled from the injection map at call time. - -```python -@stx.session -def main( - data_path: str, # Required CLI arg: --data-path - threshold: float = 0.5, # Optional CLI arg: --threshold (default 0.5) - CONFIG=stx.INJECTED, # Never a CLI arg; injected as CONFIG DotDict - logger=stx.INJECTED, # Never a CLI arg; injected as SciTeXLogger -): - ... -``` - -Supported injection names and their objects: - -| Parameter name | Injected object | -|---|---| -| `CONFIG` | `DotDict` from `start()` | -| `plt` | `scitex.plt` module | -| `COLORS` | `DotDict` of color palette | -| `rngg` | `RandomStateManager` | -| `logger` | `SciTeXLogger` for `func.__module__` | - -## Output Directory Suffix - -By default the output directory suffix is the function name: - -``` -script_out/RUNNING/-/ -``` - -Override with `sdir_suffix`: - -```python -@stx.session(sdir_suffix="experiment_v2") -def main(...): - ... -# -> script_out/RUNNING/-experiment_v2/ -``` - -## `run()` — Explicit Alternative - -`stx.session.run(func, parse_args=None, **session_kwargs)` provides the same -lifecycle without the decorator syntax: - -```python -def main(args): - return 0 - -if __name__ == "__main__": - stx.session.run(main) -``` - -Accepts an optional `parse_args` callable for custom argument parsing. - -## Decorator Metadata - -The wrapper sets two attributes on the returned callable: - -```python -wrapper._func # Reference to the original unwrapped function -wrapper._is_session_wrapped # True — lets callers detect session-wrapped functions -``` - -## Full Script Template - -```python -import scitex as stx - -@stx.session -def main( - data_path: str, - n_iterations: int = 1000, - threshold: float = 0.5, - CONFIG=stx.INJECTED, - logger=stx.INJECTED, -): - """Run analysis on data file. - - This docstring becomes the --help description. - """ - logger.info(f"Session ID: {CONFIG['ID']}") - logger.info(f"Output dir: {CONFIG['SDIR_RUN']}") - - data = stx.io.load(data_path) - result = process(data, threshold=threshold) - stx.io.save(result, "result.csv") - return 0 # exit code - -if __name__ == "__main__": - main() -``` - -CLI usage: -``` -python script.py --data-path data.csv --n-iterations 500 --threshold 0.3 -python script.py --help -``` diff --git a/src/scitex/session/_skills/injected-globals.md b/src/scitex/session/_skills/injected-globals.md deleted file mode 100644 index c9d7b4c8..00000000 --- a/src/scitex/session/_skills/injected-globals.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -description: The five globals injected by @stx.session: CONFIG, plt, COLORS, rngg, logger — what they are, how they are created, and how to use them. ---- - -# Injected Globals - -When `@stx.session` runs a function in CLI mode (no arguments), it injects five -objects into the function's module `__globals__` dict **and** passes matching -parameters as keyword arguments to the function if they are declared with -`=stx.INJECTED`. - -## The Five Injected Objects - -### CONFIG - -`DotDict` assembled from YAML files + session metadata. See [config.md](config.md) -for the full key reference. - -```python -CONFIG.ID # session ID string -CONFIG.SDIR_RUN # Path to the running output directory -CONFIG.PID # process ID -CONFIG.FILE # Path to the script -CONFIG.ARGS # DotDict of parsed CLI arguments -CONFIG.PARAMS.lr # from ./config/PARAMS.yaml (if it exists) -``` - -### plt - -`scitex.plt` module, configured for the session. This replaces the standard -`matplotlib.pyplot` with scitex's wrapped version. It has the same API but records -plot data for CSV export. - -```python -fig, ax = plt.subplots() -ax.plot([1, 2, 3], label="signal") -stx.io.save(fig, "plot.png") # also writes plot.csv automatically -``` - -The backend is set to `Agg` automatically in headless/WSL environments (detected -at module load time in `_matplotlib.py`). The `agg=True` parameter on the decorator -is kept for backwards compatibility. - -### COLORS - -`DotDict` of named color hex strings, sourced from `scitex.plt.utils.configure_mpl`. -`COLORS.gray` is aliased to `COLORS.grey`. - -```python -plt.plot(x, y, color=COLORS.blue) -plt.plot(x, y, color=COLORS['red']) -plt.scatter(x, y, c=COLORS.orange) -``` - -Available keys are listed in `--help` at runtime. - -### rngg - -`scitex.repro.RandomStateManager` instance, created with `seed=42` by default -(override via `start(seed=N)` or `@stx.session` does not expose seed as a -decorator parameter — use manual `start()` for non-default seeds). - -```python -rng_numpy = rngg("numpy_rng") # creates/retrieves a named numpy Generator -rng_torch = rngg("torch_rng") # creates/retrieves a named torch Generator - -sample = rng_numpy.integers(0, 100, size=10) -``` - -### logger - -`scitex.logging.SciTeXLogger` bound to `func.__module__`. Output goes to both -the terminal (through the tee-wrapped stdout) and to the session log files. - -```python -logger.info("Processing started") -logger.warning("Low memory") -logger.error("Failed to load file", exc_info=True) -logger.success("All done") # SciTeX-specific level -``` - -## INJECTED Sentinel - -`stx.INJECTED` (also `stx.session.INJECTED`) is an instance of `_InjectedSentinel`. -Its only purpose is to mark function parameters that should receive injected values -instead of CLI arguments. - -```python -@stx.session -def main( - data_path: str, # CLI arg: --data-path (required) - n: int = 10, # CLI arg: --n (default 10) - CONFIG=stx.INJECTED, # injected — never appears in CLI - plt=stx.INJECTED, # injected - COLORS=stx.INJECTED, # injected - rngg=stx.INJECTED, # injected - logger=stx.INJECTED, # injected -): - ... -``` - -You do **not** need to declare all five in every function. Declare only those you -actually use. - -## Global vs Parameter Injection - -The decorator injects into both places simultaneously: - -1. `func.__globals__["CONFIG"] = CONFIG` — available as a bare global anywhere - in the module -2. `filtered_kwargs["CONFIG"] = CONFIG` — passed as a keyword argument to the - function if it has `CONFIG=stx.INJECTED` in its signature - -This means you can access `CONFIG` either as a function parameter or as a module- -level global from helper functions defined in the same file. - -```python -@stx.session -def main(CONFIG=stx.INJECTED): - helper() # CONFIG is also available here as a module global - -def helper(): - print(CONFIG.ID) # works because CONFIG was injected into __globals__ -``` diff --git a/src/scitex/session/_skills/lifecycle.md b/src/scitex/session/_skills/lifecycle.md deleted file mode 100644 index a5c9dc01..00000000 --- a/src/scitex/session/_skills/lifecycle.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -description: Manual session start/close API, output directory structure, CONFIG DotDict keys, and running2finished directory promotion. ---- - -# Session Lifecycle - -The session lifecycle is implemented in `_lifecycle/` and consists of three -public functions: `start()`, `close()`, and `running2finished()`. - -## `start()` - -```python -def start( - sys=None, - plt=None, - file: Optional[str] = None, - sdir: Optional[Union[str, Path]] = None, - sdir_suffix: Optional[str] = None, - args: Optional[Any] = None, - os: Optional[Any] = None, # unused, kept for compat - random: Optional[Any] = None, # unused, kept for compat - np: Optional[Any] = None, # unused, kept for compat - torch: Optional[Any] = None, # unused, kept for compat - seed: int = 42, - agg: bool = False, - fig_size_mm: Tuple[int, int] = (160, 100), - fig_scale: float = 1.0, - dpi_display: int = 100, - dpi_save: int = 300, - fontsize="small", - autolayout=False, - show_execution_flow=False, - hide_top_right_spines: bool = True, - alpha: float = 0.9, - line_width: float = 1.0, - clear_logs: bool = False, - verbose: bool = True, -) -> Tuple[DotDict, Any, Any, Any, Optional[Dict], Any]: -``` - -Returns `(CONFIG, stdout, stderr, plt, COLORS, rng)`. - -If `sys` is not passed, `stdout` and `stderr` are `None` (no tee-logging). - -### What `start()` does - -1. Reads `./config/IS_DEBUG.yaml` to set debug mode (prefix `DEBUG_` on session ID - if true). -2. Generates a unique session `ID` via `gen_ID(N=4)`, e.g. `2025Y-11M-18D-07h53m37s_Z5MR`. -3. Determines `caller_file` — uses `file` parameter, falls back to - `inspect.stack()[1].filename`, handles IPython/notebook detection. -4. Builds `SDIR_RUN` path: - `