@@ -9,6 +9,7 @@ Do `rimport --help` for more information.
99from __future__ import annotations
1010
1111import argparse
12+ import logging
1213import os
1314import pwd
1415import shutil
@@ -26,6 +27,9 @@ STAGE_OWNER = "cesmdata"
2627INDENT = " "
2728INPUTDATA_URL = "https://osdf-data.gdex.ucar.edu/ncar/gdex/d651077/cesmdata/inputdata"
2829
30+ # Configure logging
31+ logger = logging .getLogger (__name__ )
32+
2933
3034def build_parser () -> argparse .ArgumentParser :
3135 """Build and configure the argument parser for rimport.
@@ -177,7 +181,7 @@ def stage_data(
177181 f"Source is a symlink, but target ({ src .resolve ()} ) is outside staging directory "
178182 f"({ staging_root } )"
179183 )
180- print ( f" { INDENT } File is already published and linked." )
184+ logger . info ( "%sFile is already published and linked.", INDENT )
181185 print_can_file_be_downloaded (
182186 can_file_be_downloaded (src .resolve (), staging_root )
183187 )
@@ -200,21 +204,19 @@ def stage_data(
200204 dst = staging_root / rel
201205
202206 if dst .exists ():
203- print (f"{ INDENT } File is already published but NOT linked; do" )
204- print (f"{ 2 * INDENT } relink.py { rel } " )
205- print (f"{ INDENT } to resolve." )
206- print_can_file_be_downloaded (
207- can_file_be_downloaded (rel , staging_root )
208- )
207+ logger .info ("%sFile is already published but NOT linked; do" , INDENT )
208+ logger .info ("%srelink.py %s" , 2 * INDENT , rel )
209+ logger .info ("%sto resolve." , INDENT )
210+ print_can_file_be_downloaded (can_file_be_downloaded (rel , staging_root ))
209211 return
210212
211213 if check :
212- print ( f" { INDENT } File is not already published" )
214+ logger . info ( "%sFile is not already published", INDENT )
213215 return
214216
215217 dst .parent .mkdir (parents = True , exist_ok = True )
216218 shutil .copy2 (src , dst )
217- print ( f" { INDENT } [rimport] staged { src } -> { dst } " )
219+ logger . info ( "%s [rimport] staged %s -> %s" , INDENT , src , dst )
218220
219221
220222def ensure_running_as (target_user : str , argv : list [str ]) -> None :
@@ -237,20 +239,18 @@ def ensure_running_as(target_user: str, argv: list[str]) -> None:
237239 try :
238240 target_uid = pwd .getpwnam (target_user ).pw_uid
239241 except KeyError as exc :
240- print (
241- f"rimport: target user '{ target_user } ' not found on this system" ,
242- file = sys .stderr ,
243- )
242+ logger .error ("rimport: target user '%s' not found on this system" , target_user )
244243 raise SystemExit (2 ) from exc
245244
246245 if os .geteuid () != target_uid :
247246 try :
248247 assert sys .stdin .isatty ()
249248 except AssertionError as exc :
250- print (
251- f"rimport: need interactive TTY to authenticate as '{ target_user } ' (2FA).\n "
252- f" Try: sudo -u { target_user } rimport …" ,
253- file = sys .stderr ,
249+ logger .error (
250+ "rimport: need interactive TTY to authenticate as '%s' (2FA).\n "
251+ " Try: sudo -u %s rimport …" ,
252+ target_user ,
253+ target_user ,
254254 )
255255 raise SystemExit (2 ) from exc
256256 # Re-exec under target user; this invokes sudo’s normal password/2FA flow.
@@ -310,9 +310,37 @@ def print_can_file_be_downloaded(file_can_be_downloaded: bool):
310310 file_can_be_downloaded: Boolean indicating if the file can be downloaded.
311311 """
312312 if file_can_be_downloaded :
313- print ( f" { INDENT } File is available for download." )
313+ logger . info ( "%sFile is available for download.", INDENT )
314314 else :
315- print (f"{ INDENT } File is not (yet) available for download." )
315+ logger .info ("%sFile is not (yet) available for download." , INDENT )
316+
317+
318+ def configure_logging () -> None :
319+ """Configure logging to send INFO/WARNING to stdout and ERROR/CRITICAL to stderr.
320+
321+ Sets up two handlers:
322+ - INFO handler: Sends INFO and WARNING level messages to stdout
323+ - ERROR handler: Sends ERROR and CRITICAL level messages to stderr
324+
325+ Both handlers use simple message-only formatting without timestamps or level names.
326+ """
327+ logger .setLevel (logging .INFO )
328+
329+ # Handler for INFO and WARNING level messages -> stdout
330+ info_handler = logging .StreamHandler (sys .stdout )
331+ info_handler .setLevel (logging .INFO )
332+ info_handler .addFilter (lambda record : record .levelno < logging .ERROR )
333+ info_handler .setFormatter (logging .Formatter ("%(message)s" ))
334+
335+ # Handler for ERROR and CRITICAL level messages -> stderr
336+ error_handler = logging .StreamHandler (sys .stderr )
337+ error_handler .setLevel (logging .ERROR )
338+ error_handler .setFormatter (logging .Formatter ("%(message)s" ))
339+
340+ # Clear any existing handlers and add our custom ones
341+ logger .handlers .clear ()
342+ logger .addHandler (info_handler )
343+ logger .addHandler (error_handler )
316344
317345
318346def main (argv : List [str ] | None = None ) -> int :
@@ -337,6 +365,8 @@ def main(argv: List[str] | None = None) -> int:
337365 1: One or more files failed to stage (errors printed to stderr).
338366 2: Fatal error (missing inputdata directory, missing file list, etc.).
339367 """
368+ configure_logging ()
369+
340370 parser = build_parser ()
341371 args = parser .parse_args (argv )
342372
@@ -348,7 +378,7 @@ def main(argv: List[str] | None = None) -> int:
348378
349379 root = Path (args .inputdata ).expanduser ().resolve ()
350380 if not root .exists ():
351- print ( f "rimport: inputdata directory does not exist: { root } " , file = sys . stderr )
381+ logger . error ( "rimport: inputdata directory does not exist: %s " , root )
352382 return 2
353383
354384 # Determine the list of relative filenames to handle
@@ -357,11 +387,11 @@ def main(argv: List[str] | None = None) -> int:
357387 else :
358388 list_path = Path (args .filelist ).expanduser ().resolve ()
359389 if not list_path .exists ():
360- print ( f "rimport: list file not found: { list_path } " , file = sys . stderr )
390+ logger . error ( "rimport: list file not found: %s " , list_path )
361391 return 2
362392 relnames = read_filelist (list_path )
363393 if not relnames :
364- print ( f "rimport: no filenames found in list: { list_path } " , file = sys . stderr )
394+ logger . error ( "rimport: no filenames found in list: %s " , list_path )
365395 return 2
366396
367397 # Resolve to full paths (keep accepting absolute names too)
@@ -370,13 +400,13 @@ def main(argv: List[str] | None = None) -> int:
370400 # Execute the new action per file
371401 errors = 0
372402 for p in paths :
373- print ( f"' { p } ':" )
403+ logger . info ( "'%s ':", p )
374404 try :
375405 stage_data (p , root , staging_root , args .check )
376406 except Exception as e : # pylint: disable=broad-exception-caught
377407 # General Exception keeps CLI robust for batch runs
378408 errors += 1
379- print ( f" { INDENT } rimport : error processing { p } : { e } " , file = sys . stderr )
409+ logger . error ( "%srimport : error processing %s: %s " , INDENT , p , e )
380410
381411 return 0 if errors == 0 else 1
382412
0 commit comments