@@ -24,6 +24,18 @@ STAGE_OWNER = "cesmdata"
2424
2525
2626def build_parser () -> argparse .ArgumentParser :
27+ """Build and configure the argument parser for rimport.
28+
29+ Creates an ArgumentParser with the following options:
30+ - Mutually exclusive required group:
31+ --file: Import a single file (relative to inputdata directory)
32+ --list: Import multiple files from a list file
33+ - Optional:
34+ --inputdata: Override the default inputdata directory
35+
36+ Returns:
37+ argparse.ArgumentParser: Configured parser ready to parse command-line arguments.
38+ """
2739 parser = argparse .ArgumentParser (
2840 description = "Copy files from CESM inputdata directory to a publishing directory."
2941 )
@@ -64,7 +76,18 @@ def build_parser() -> argparse.ArgumentParser:
6476
6577
6678def read_filelist (list_path : Path ) -> List [str ]:
67- """Read list file, ignoring blank lines and comments starting with #."""
79+ """Read a file list and return non-empty, non-comment lines.
80+
81+ Reads a text file containing a list of filenames, filtering out:
82+ - Blank lines (empty or whitespace-only)
83+ - Comment lines (starting with '#')
84+
85+ Args:
86+ list_path: Path to the file containing the list of filenames.
87+
88+ Returns:
89+ List of stripped, non-empty lines that are not comments.
90+ """
6891 lines : List [str ] = []
6992 with list_path .open ("r" , encoding = "utf-8" ) as f :
7093 for raw in f :
@@ -76,6 +99,20 @@ def read_filelist(list_path: Path) -> List[str]:
7699
77100
78101def resolve_paths (root : Path , relnames : Iterable [str ]) -> List [Path ]:
102+ """Convert relative or absolute path names to resolved absolute Paths.
103+
104+ For each name in relnames:
105+ - If the name is relative, it is resolved relative to `root`
106+ - If the name is already absolute, it is resolved as-is
107+ All paths are resolved to their canonical absolute form.
108+
109+ Args:
110+ root: Base directory for resolving relative paths.
111+ relnames: Iterable of path names (relative or absolute) to resolve.
112+
113+ Returns:
114+ List of resolved absolute Path objects.
115+ """
79116 paths : List [Path ] = []
80117 for name in relnames :
81118 p = (
@@ -89,13 +126,25 @@ def resolve_paths(root: Path, relnames: Iterable[str]) -> List[Path]:
89126
90127def stage_data (src : Path , inputdata_root : Path , staging_root : Path ) -> None :
91128 """Stage a file by mirroring its path under `staging_root`.
92- Destination path is computed by replacing the `args.inputdata` prefix of `src`
129+
130+ Destination path is computed by replacing the `inputdata_root` prefix of `src`
93131 with `staging_root`, i.e.:
94- dst = staging_root / src.relative_to(inputdata_root)
132+ dst = staging_root / src.relative_to(inputdata_root)
133+
134+ Args:
135+ src: Source file path to stage.
136+ inputdata_root: Root directory of the inputdata tree.
137+ staging_root: Root directory where files will be staged.
138+
139+ Raises:
140+ RuntimeError: If `src` is a live symlink (already published), or if `src`
141+ is outside the inputdata root, or if `src` is already under staging directory.
142+ RuntimeError: If `src` is a broken symlink.
143+ FileNotFoundError: If `src` does not exist.
95144
96145 Guardrails:
97- * Raise if `src` is a *live* symlink ("already published").
98- * Raise if `src` is a broken symlink or is outside the inputdata root.
146+ * Raise if `src` is a *live* symlink ("already published").
147+ * Raise if `src` is a broken symlink or is outside the inputdata root.
99148 """
100149 if src .is_symlink () and src .exists ():
101150 # TODO: This should be a regular message, not an error.
@@ -123,7 +172,22 @@ def stage_data(src: Path, inputdata_root: Path, staging_root: Path) -> None:
123172
124173
125174def ensure_running_as (target_user : str , argv : list [str ]) -> None :
126- """If not running as `target_user`, re-exec via sudo -u target_user (handles 2FA via PAM)."""
175+ """Ensure the script is running as the target user, re-executing via sudo if needed.
176+
177+ If not running as `target_user`, re-exec via sudo -u target_user (handles 2FA via PAM).
178+ This function will not return if re-execution is needed; it replaces the current process.
179+
180+ Args:
181+ target_user: Username to run as (e.g., 'cesmdata').
182+ argv: Command-line arguments to pass to the re-executed process.
183+
184+ Raises:
185+ SystemExit: If the target user is not found on the system (exit code 2).
186+ SystemExit: If not running interactively and authentication is required (exit code 2).
187+
188+ Note:
189+ If re-execution is needed, this function calls os.execvp() and does not return.
190+ """
127191 try :
128192 target_uid = pwd .getpwnam (target_user ).pw_uid
129193 except KeyError as exc :
@@ -148,15 +212,41 @@ def ensure_running_as(target_user: str, argv: list[str]) -> None:
148212
149213
150214def get_staging_root () -> Path :
151- """Return the staging root. Uses $RIMPORT_STAGING if set, otherwise
152- creates a sibling directory named '<inputdata_root>.staging'."""
215+ """Return the staging root directory path.
216+
217+ Uses $RIMPORT_STAGING if set, otherwise returns the default staging root.
218+
219+ Returns:
220+ Path: Resolved absolute path to the staging root directory.
221+ """
153222 env = os .getenv ("RIMPORT_STAGING" )
154223 if env :
155224 return Path (env ).expanduser ().resolve ()
156225 return DEFAULT_STAGING_ROOT
157226
158227
159228def main (argv : List [str ] | None = None ) -> int :
229+ """Main entry point for the rimport tool.
230+
231+ Copies files from the CESM inputdata directory to a staging/publishing directory,
232+ preserving the directory structure. Ensures the script runs as the correct user
233+ (STAGE_OWNER) and handles both single files and file lists.
234+
235+ Args:
236+ argv: Command-line arguments to parse. If None, uses sys.argv.
237+
238+ Returns:
239+ int: Exit code (0 for success, 1 if any files had errors, 2 for fatal errors).
240+
241+ Environment Variables:
242+ RIMPORT_SKIP_USER_CHECK: Set to "1" to skip automatic user switching.
243+ RIMPORT_STAGING: Override the default staging root directory.
244+
245+ Exit Codes:
246+ 0: All files staged successfully.
247+ 1: One or more files failed to stage (errors printed to stderr).
248+ 2: Fatal error (missing inputdata directory, missing file list, etc.).
249+ """
160250 parser = build_parser ()
161251 args = parser .parse_args (argv )
162252
0 commit comments