File indexing completed on 2026-04-09 07:58:22
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018 import argparse
0019 import logging
0020
0021 import os
0022 import json
0023 import sys
0024
0025 import configparser
0026
0027
0028
0029
0030 multi_word_sections = {
0031 'idds_transformer': 'idds_transformer',
0032 }
0033
0034
0035 def load_flat_config(flat_config):
0036 """
0037 takes a dict of the form: {"section_option": "value"}
0038 and converts to {"section": {"option": "value"}
0039 """
0040 config_dict = {}
0041 for flat_key, config_value in flat_config.items():
0042 section = option = None
0043
0044 for mw_key in multi_word_sections:
0045 if flat_key.startswith(mw_key + '_'):
0046 section = mw_key
0047 option = flat_key[len(mw_key) + 1:]
0048
0049
0050 if not section:
0051 section, option = flat_key.split('_', maxsplit=1)
0052
0053 config_dict.setdefault(section, {})[option] = config_value
0054 return config_dict
0055
0056
0057 def fix_multi_word_sections(config_dict):
0058 return {multi_word_sections.get(section, section): config_for_section for section, config_for_section in config_dict.items()}
0059
0060
0061 def config_len(config_dict):
0062 return sum(len(option) for _, option in config_dict.items())
0063
0064
0065 def merge_configs(source_file_paths, dest_file_path, prefix, use_env=True, replace_whole_file=False, env_string=None, logger=logging.log):
0066 """
0067 Merge multiple configuration sources into one destination file.
0068 On conflicting values, relies on the default python's ConfigParser behavior: the value from last source wins.
0069 Sources can be either .ini or .json files. Json is supported as a compromise solution for easier integration
0070 with kubernetes (because both python and helm natively support it).
0071 If use_env=True, env variables starting with RUCIO_CFG_ are also merged as the last (highest priority) source.
0072 """
0073
0074 if replace_whole_file and env_string:
0075 with open(dest_file_path, 'w') as dest_file:
0076 dest_file.write(env_string)
0077 else:
0078 parser = configparser.ConfigParser()
0079 for file_path in source_file_paths:
0080 if not os.path.exists(file_path):
0081 logger(logging.WARNING, "Skipping config file {}: file doesn't exist".format(file_path))
0082 continue
0083
0084 try:
0085 if file_path.endswith('.json'):
0086 with open(file_path, 'r') as f:
0087 file_config = fix_multi_word_sections(json.load(f))
0088 parser.read_dict(file_config)
0089 else:
0090 local_parser = configparser.ConfigParser()
0091 local_parser.read(file_path)
0092 file_config = {section: {option: value for option, value in section_proxy.items()} for section, section_proxy in local_parser.items()}
0093
0094 parser.read_dict(file_config)
0095 logger(logging.INFO, "Merged {} configuration values from {}".format(config_len(file_config), file_path))
0096 except Exception as error:
0097 logger(logging.WARNING, "Skipping config file {} due to error: {}".format(file_path, error))
0098
0099 if use_env:
0100
0101 env_config = {}
0102 for env_key, env_value in os.environ.items():
0103 if not env_key.startswith(prefix):
0104 continue
0105 env_key = env_key[len(prefix):].lower()
0106 env_config[env_key] = env_value
0107
0108 env_config = fix_multi_word_sections(load_flat_config(env_config))
0109 parser.read_dict(env_config)
0110 logger(logging.INFO, "Merged {} configuration values from ENV".format(config_len(env_config)))
0111
0112 if dest_file_path:
0113 with open(dest_file_path, 'w') as dest_file:
0114 parser.write(dest_file)
0115 else:
0116 parser.write(sys.stdout)
0117
0118
0119 logging.getLogger().setLevel(logging.INFO)
0120 parser = argparse.ArgumentParser(description="Merge multiple configuration sources into one configuration file")
0121 parser.add_argument("--use-env", action="store_true", default=False, help='Also source config from RUCIO_CFG_* env variables')
0122 parser.add_argument('-s', '--source', type=str, nargs='*', help='Source config file paths (in .json or .ini format)')
0123 parser.add_argument('-d', '--destination', default=None, help='Destination file path')
0124 parser.add_argument('-p', '--prefix', default=None, help='Prefix to match the env')
0125 parser.add_argument('-r', "--replace-whole-file", action="store_true", default=False, help='whether to replace the whole file with the env-string')
0126 parser.add_argument('-e', '--env-string', default=None, help='The environment string to replace the whole file')
0127 args = parser.parse_args()
0128
0129 merge_configs(args.source or [], args.destination, use_env=args.use_env, prefix=args.prefix,
0130 replace_whole_file=args.replace_whole_file, env_string=args.env_string)