Code Structure#
Repository layout#
launchcontainers/
├── cli.py ← entry point; argparse subcommands
├── do_prepare.py ← prepare orchestration (DWI legacy path)
├── do_launch.py ← job submission
├── do_qc.py ← quality control
├── utils.py ← shared utilities (read_yaml, read_df, …)
├── log_setup.py ← Rich console + Python logging setup
│
├── prepare/
│ ├── __init__.py ← PREPARER_REGISTRY
│ ├── base_preparer.py ← BasePreparer, PrepIssue, PrepResult
│ ├── glm_preparer.py ← GLMPreparer
│ ├── prepare_dwi.py ← DWI container preparation (legacy)
│ └── dwi_prepare_input.py
│
├── gen_jobscript/ ← job-command generation (one sub-module per job type)
│ ├── __init__.py ← gen_launch_cmd() orchestrator; routes by container type
│ ├── gen_container_cmd.py ← Apptainer/Singularity command builder
│ ├── gen_matlab_cmd.py ← MATLAB script command builder (stub)
│ └── gen_py_cmd.py ← Python script command builder (stub)
│
├── clusters/
│ ├── slurm.py ← SLURM job submission helpers
│ ├── sge.py ← SGE job submission helpers
│ └── local.py ← local serial/parallel execution (concurrent.futures)
│
├── check/
│ ├── general_checks.py ← shared file-existence checks
│ └── check_dwi_pipelines.py ← DWI-specific checks
│
└── helper_function/
├── gen_subses.py ← subseslist generation
├── create_bids.py ← fake BIDS structure for testing
├── copy_configs.py ← copy example configs to working dir
└── zip_example_config.py ← archive configs (developer utility)
Logging architecture#
Every CLI command sets up two log files before doing any work:
analysis_dir/
├── prepare_log/
│ ├── lc_prepare_<timestamp>.log ← all messages (DEBUG and above)
│ └── lc_prepare_<timestamp>.err ← warnings and errors only
├── run_log/
│ ├── lc_run_<timestamp>.log
│ └── lc_run_<timestamp>.err
└── qc_log/
├── qc_<timestamp>.log
└── qc_<timestamp>.err
The key component is _LoggingConsole in log_setup.py, a subclass of
Rich’s Console that intercepts every console.print() call and
simultaneously forwards it to a Python logging.Logger:
style="red"orstyle="bold red"→logger.error()style="yellow"→logger.warning()everything else →
logger.info()
This means no code change is needed throughout the codebase — all existing
console.print() calls are captured automatically. The set_log_files()
function in log_setup.py attaches the two FileHandler instances; it is
called once per CLI command from cli.py before any work starts.
gen_jobscript package#
Command generation is organised as a package so that different job types
(Apptainer containers, Python scripts, MATLAB scripts) each have their own
module. The orchestrator in __init__.py reads lc_config["general"]["container"]
and routes to the appropriate builder:
# gen_jobscript/__init__.py
if container in _CONTAINER_JOBS:
_gen_cmd = gen_RTP2_cmd # apptainer
elif container == "matlab":
_gen_cmd = gen_matlab_cmd
elif container == "python":
_gen_cmd = gen_py_cmd
Adding support for a new job type means creating a new module and adding one
elif branch — the orchestrator and do_launch.py need no other changes.
Entry points (pyproject.toml)#
[tool.poetry.scripts]
lc = "launchcontainers.cli:main"
checker = "analysis_checker.check_analysis_integrity:main"
Dispatch logic in cli.py#
lc prepare uses a two-path dispatch based on the container key:
if container in PREPARER_REGISTRY:
# New-style: BasePreparer subclass owns all preparation logic
cls = PREPARER_REGISTRY[container]
preparer = cls(config=lc_config, subseslist=subsesrows, output_root=output_root)
preparer.run(dry_run=False)
else:
# Legacy DWI path (untouched)
do_prepare.main(parse_namespace, analysis_dir)
This means all DWI container pipelines use the original do_prepare code,
while analysis-based pipelines (glm, prf, …) go through the
BasePreparer class hierarchy. Adding a new analysis type never requires
modifying the legacy path.
BasePreparer class hierarchy#
BasePreparer (abstract)
├── GLMPreparer
└── PRFPreparer (planned)
BasePreparer owns all orchestration: directory creation, config
freezing, issue processing, Rich summary printing, and log writing.
Subclasses implement only two abstract methods:
check_requirements(sub, ses) → list[PrepIssue]generate_run_script(sub, ses, analysis_dir) → Path
Data classes#
@dataclass
class PrepIssue:
sub: str
ses: str
category: str
severity: str # "blocking" | "warn" | "auto_fix"
message: str
fix_fn: Callable | None = None
@dataclass
class PrepResult:
sub: str
ses: str
status: str # "ready" | "fixed" | "warn" | "blocked"
issues: list[PrepIssue]