"""Shared INI/path helpers used across the COSEDA pipeline."""
import os
import configparser
import ctypes
[docs]
def is_parent_config_file(file_path):
"""Return True if the INI declares `[General] config_type = parent`."""
# Check if the given INI file has a [General] section with config_type = parent
config = configparser.ConfigParser()
config.read(file_path)
if config.has_section('General') and config.has_option('General', 'config_type'):
config_type = config.get('General', 'config_type').strip().lower()
if config_type == 'parent':
return True
return False
[docs]
def parse_config(configfile):
"""
Parse an INI file (UTF-8 with latin-1 fallback) and resolve common paths.
Returns:
`(config, outputfolder, originalfile, logfile, path, outputfolder_path, originalfile_path, logfile_path, framepath, h5file, h5file_path)`
"""
config = configparser.ConfigParser()
# Try reading the file with 'utf-8' encoding first
try:
with open(configfile, 'r', encoding='utf-8') as f:
config.read_file(f)
except UnicodeDecodeError:
# If 'utf-8' fails, try 'latin-1' encoding
try:
with open(configfile, 'r', encoding='latin-1') as f:
config.read_file(f)
except UnicodeDecodeError:
# If both 'utf-8' and 'latin-1' fail, raise an error
raise UnicodeDecodeError(f"Cannot decode file {configfile}. Please check its encoding.")
# Required parameters
outputfolder = config.get('Paths', 'outputfolder')
if config.has_option('Paths', 'originalfile'):
originalfile = config.get('Paths', 'originalfile')
elif config.has_option('Paths', 'originalfolder'):
originalfile = config.get('Paths', 'originalfolder')
logfile = config.get('Paths', 'logfile')
path, _ = os.path.split(configfile)
outputfolder_path = os.path.join(path, outputfolder)
originalfile_path = os.path.join(path, originalfile)
logfile_path = os.path.join(outputfolder_path, logfile)
# Optional parameters
framepath = config.get('Paths','framepath', fallback=None)
h5file = config.get('Paths', 'h5file', fallback=None)
h5file_path = os.path.join(outputfolder_path, h5file) if h5file else None
return config, outputfolder, originalfile, logfile, path, outputfolder_path, originalfile_path, logfile_path, framepath, h5file, h5file_path
[docs]
def get_free_space_windows(path):
"""Return available bytes for a Windows path via GetDiskFreeSpaceEx."""
free_bytes = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
return free_bytes.value
[docs]
def get_free_space_unix(path):
"""Return available bytes for a Unix path via statvfs."""
statvfs = os.statvfs(path)
return statvfs.f_frsize * statvfs.f_bavail
[docs]
def get_dir_size(directory):
"""Calculate total size of a directory tree, skipping symlinks."""
total_size = 0
for dirpath, dirnames, filenames in os.walk(directory):
for f in filenames:
fp = os.path.join(dirpath, f)
# skip if it is symbolic link
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
return total_size
[docs]
def config_to_paths(configfile: str):
"""
Given the path to a .ini configfile, determine and return:
outputfolder, outputfolder_path, logfile, logfile_path, h5file, h5file_path
Path A: if <basename>.h5 exists in the same directory as the .ini:
- outputfolder: basename of the ini (without extension)
- outputfolder_path: same directory as ini
- logfile: <basename>.log
- logfile_path: same directory as ini
- h5file: <basename>.h5
- h5file_path: same directory as ini
Path B: otherwise, read the [Paths] section of the INI:
- outputfolder: config.get('Paths','outputfolder')
- outputfolder_path: parent_dir/outputfolder
- logfile: config.get('Paths','logfile')
- logfile_path: parent_dir/logfile
- h5file: config.get('Paths','h5file')
- h5file_path: outputfolder_path/h5file
"""
parent_dir = os.path.dirname(os.path.abspath(configfile))
basename = os.path.splitext(os.path.basename(configfile))[0]
# Path A: check for HDF5 alongside INI
candidate_h5 = os.path.join(parent_dir, f"{basename}.h5")
if os.path.isfile(candidate_h5):
outputfolder = basename
outputfolder_path = parent_dir
logfile = f"{basename}.log"
logfile_path = os.path.join(parent_dir, logfile)
h5file = f"{basename}.h5"
h5file_path = candidate_h5
return outputfolder, outputfolder_path, logfile, logfile_path, h5file, h5file_path
# Path B: read from INI’s [Paths] section
config = configparser.ConfigParser()
config.read(configfile)
section = 'Paths'
outputfolder = config.get(section, 'outputfolder', fallback=None)
logfile = config.get(section, 'logfile', fallback=None)
h5file = config.get(section, 'h5file', fallback=None)
outputfolder_path = os.path.join(parent_dir, outputfolder) if outputfolder else parent_dir
logfile_path = os.path.join(parent_dir, logfile) if logfile else None
h5file_path = os.path.join(outputfolder_path, h5file) if outputfolder and h5file else None
return outputfolder, outputfolder_path, logfile, logfile_path, h5file, h5file_path
[docs]
def read_config(configfile):
"""
Read an INI file and return the ConfigParser, tolerating legacy encodings.
Attempts UTF-8 first, then falls back to latin-1 so older datasets do not
fail early when metadata contains non-UTF characters.
"""
config = configparser.ConfigParser()
# Try reading the file with 'utf-8' encoding first
try:
with open(configfile, 'r', encoding='utf-8') as f:
config.read_file(f)
except UnicodeDecodeError:
# If 'utf-8' fails, try 'latin-1' encoding
try:
with open(configfile, 'r', encoding='latin-1') as f:
config.read_file(f)
except UnicodeDecodeError:
# If both 'utf-8' and 'latin-1' fail, raise an error
raise UnicodeDecodeError(f"Cannot decode file {configfile}. Please check its encoding.")
return config
[docs]
def update_paths_old(configfile_path): # Update paths
"""Legacy helper to rebuild logfile path from an INI location and [Paths]."""
config = configparser.ConfigParser()
config.read(configfile_path)
outputfolder = config.get('Paths', 'outputfolder')
logfile = config.get('Paths', 'logfile')
path, _ = os.path.split(configfile_path)
logfile_path = os.path.join(path, outputfolder, logfile)