meerschaum.config
Meerschaum v3.2.3
1#! /usr/bin/env python 2# -*- coding: utf-8 -*- 3# vim:fenc=utf-8 4 5""" 6Import and update configuration dictionary 7and if interactive, print the welcome message. 8""" 9 10from __future__ import annotations 11 12import os 13import shutil 14import sys 15import copy 16import contextlib 17 18import meerschaum.config.paths as paths 19from meerschaum.utils.typing import Any, Dict, Optional, Union 20from meerschaum.utils.threading import RLock 21 22from meerschaum.config._version import __version__ 23from meerschaum.config._edit import edit_config, write_config 24from meerschaum.config._read_config import ( 25 search_and_substitute_config, 26 revert_symlinks_config, 27 get_possible_keys, 28 get_keyfile_path, 29 read_config, 30) 31from meerschaum._internal.static import STATIC_CONFIG 32 33from meerschaum.config._patch import ( 34 apply_patch_to_config, 35) 36__all__ = ( 37 'get_plugin_config', 38 'write_plugin_config', 39 'get_config', 40 'write_config', 41 'edit_config', 42 'set_config', 43 'replace_config', 44 'search_and_substitute_config', 45 'revert_symlinks_config', 46 'get_possible_keys', 47 'get_keyfile_path', 48 'apply_patch_to_config', 49 'read_config', 50 'paths', 51 'STATIC_CONFIG', 52) 53__pdoc__ = {'static': False, 'resources': False, 'stack': False, } 54_locks = {'config': RLock()} 55 56### apply config preprocessing (e.g. main to meta) 57config = {} 58_backup_config = None 59_allow_write_missing: bool = True 60 61 62def _config( 63 *keys: str, 64 reload: bool = False, 65 substitute: bool = True, 66 sync_files: bool = True, 67 allow_replaced: bool = True, 68 write_missing: bool = True, 69) -> Dict[str, Any]: 70 """ 71 Read and process the configuration file. 72 """ 73 global config, _backup_config 74 75 if config is None or reload: 76 with _locks['config']: 77 config = {} 78 79 if keys and keys[0] not in config: 80 from meerschaum.config._sync import sync_files as _sync_files 81 key_config = read_config( 82 keys = [keys[0]], 83 substitute = substitute, 84 write_missing = write_missing and _allow_write_missing, 85 ) 86 if keys[0] in key_config: 87 config[keys[0]] = key_config[keys[0]] 88 if sync_files and _allow_write_missing: 89 _sync_files(keys=[keys[0] if keys else None]) 90 91 if not allow_replaced: 92 return _backup_config if _backup_config is not None else config 93 94 return config 95 96 97def set_config(cf: Dict[str, Any]) -> Dict[str, Any]: 98 """ 99 Set the configuration dictionary. 100 """ 101 global config 102 if not isinstance(cf, dict): 103 from meerschaum.utils.warnings import error 104 error(f"Invalid value for config: {cf}") 105 with _locks['config']: 106 config = cf 107 return config 108 109 110def get_config( 111 *keys: str, 112 patch: bool = True, 113 substitute: bool = True, 114 sync_files: bool = True, 115 write_missing: bool = True, 116 as_tuple: bool = False, 117 warn: bool = True, 118 debug: bool = False 119) -> Any: 120 """ 121 Return the Meerschaum configuration dictionary. 122 If positional arguments are provided, index by the keys. 123 Raises a warning if invalid keys are provided. 124 125 Parameters 126 ---------- 127 keys: str: 128 List of strings to index. 129 130 patch: bool, default True 131 If `True`, patch missing default keys into the config directory. 132 Defaults to `True`. 133 134 sync_files: bool, default True 135 If `True`, sync files if needed. 136 Defaults to `True`. 137 138 write_missing: bool, default True 139 If `True`, write default values when the main config files are missing. 140 Defaults to `True`. 141 142 substitute: bool, default True 143 If `True`, subsitute 'MRSM{}' values. 144 Defaults to `True`. 145 146 as_tuple: bool, default False 147 If `True`, return a tuple of type (success, value). 148 Defaults to `False`. 149 150 Returns 151 ------- 152 The value in the configuration directory, indexed by the provided keys. 153 154 Examples 155 -------- 156 >>> get_config('meerschaum', 'instance') 157 'sql:main' 158 >>> get_config('does', 'not', 'exist') 159 UserWarning: Invalid keys in config: ('does', 'not', 'exist') 160 """ 161 import json 162 163 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 164 if debug: 165 from meerschaum.utils.debug import dprint 166 dprint(f"Indexing keys: {keys}", color=False) 167 168 if len(keys) == 0: 169 _rc = _config( 170 substitute=substitute, 171 sync_files=sync_files, 172 write_missing=(write_missing and _allow_write_missing), 173 ) 174 if as_tuple: 175 return True, _rc 176 return _rc 177 178 ### Weird threading issues, only import if substitute is True. 179 if substitute: 180 from meerschaum.config._read_config import search_and_substitute_config 181 ### Invalidate the cache if it was read before with substitute=False 182 ### but there still exist substitutions. 183 if ( 184 config is not None and substitute and keys[0] != symlinks_key 185 and 'MRSM{' in json.dumps(config.get(keys[0])) 186 ): 187 try: 188 _subbed = search_and_substitute_config({keys[0]: config[keys[0]]}) 189 except Exception: 190 import traceback 191 traceback.print_exc() 192 _subbed = {keys[0]: config[keys[0]]} 193 194 config[keys[0]] = _subbed[keys[0]] 195 if symlinks_key in _subbed: 196 if symlinks_key not in config: 197 config[symlinks_key] = {} 198 config[symlinks_key] = apply_patch_to_config( 199 _subbed.get(symlinks_key, {}), 200 config.get(symlinks_key, {}), 201 ) 202 203 from meerschaum.config._sync import sync_files as _sync_files 204 if config is None: 205 _config(*keys, sync_files=sync_files) 206 207 invalid_keys = False 208 if keys[0] not in config and keys[0] != symlinks_key: 209 single_key_config = read_config( 210 keys=[keys[0]], substitute=substitute, write_missing=write_missing 211 ) 212 if keys[0] not in single_key_config: 213 invalid_keys = True 214 else: 215 config[keys[0]] = single_key_config.get(keys[0], None) 216 if symlinks_key in single_key_config and keys[0] in single_key_config[symlinks_key]: 217 if symlinks_key not in config: 218 config[symlinks_key] = {} 219 config[symlinks_key][keys[0]] = single_key_config[symlinks_key][keys[0]] 220 221 if sync_files: 222 _sync_files(keys=[keys[0]]) 223 224 c = config 225 if len(keys) > 0: 226 for k in keys: 227 try: 228 c = c[k] 229 except Exception: 230 invalid_keys = True 231 break 232 if invalid_keys: 233 ### Check if the keys are in the default configuration. 234 from meerschaum.config._default import default_config 235 in_default = True 236 patched_default_config = ( 237 search_and_substitute_config(default_config) 238 if substitute else copy.deepcopy(default_config) 239 ) 240 _c = patched_default_config 241 for k in keys: 242 try: 243 _c = _c[k] 244 except Exception: 245 in_default = False 246 if in_default: 247 c = _c 248 invalid_keys = False 249 warning_msg = f"Invalid keys in config: {keys}" 250 if not in_default: 251 try: 252 if warn: 253 from meerschaum.utils.warnings import warn as _warn 254 _warn(warning_msg, stacklevel=3, color=False) 255 except Exception: 256 if warn: 257 print(warning_msg) 258 if as_tuple: 259 return False, None 260 return None 261 262 ### Don't write keys that we haven't yet loaded into memory. 263 not_loaded_keys = [k for k in patched_default_config if k not in config] 264 for k in not_loaded_keys: 265 patched_default_config.pop(k, None) 266 267 set_config( 268 apply_patch_to_config( 269 patched_default_config, 270 config, 271 ) 272 ) 273 if patch and keys[0] != symlinks_key: 274 if write_missing: 275 write_config(config, debug=debug) 276 277 if as_tuple: 278 return (not invalid_keys), c 279 return c 280 281 282def get_plugin_config( 283 *keys: str, 284 warn: bool = False, 285 **kw: Any 286) -> Optional[Any]: 287 """ 288 This may only be called from within a Meerschaum plugin. 289 See `meerschaum.config.get_config` for arguments. 290 """ 291 from meerschaum.utils.warnings import error 292 from meerschaum.plugins import _get_parent_plugin 293 parent_plugin_name = _get_parent_plugin(2) 294 if parent_plugin_name is None: 295 error( 296 "You may only call `get_plugin_config()` " 297 "from within a Meerschaum plugin." 298 ) 299 300 return get_config( 301 *(['plugins', parent_plugin_name] + list(keys)), 302 warn=warn, 303 **kw 304 ) 305 306 307def write_plugin_config( 308 config_dict: Dict[str, Any], 309 **kw: Any 310): 311 """ 312 Write a plugin's configuration dictionary. 313 """ 314 from meerschaum.utils.warnings import error 315 from meerschaum.plugins import _get_parent_plugin 316 parent_plugin_name = _get_parent_plugin(2) 317 if parent_plugin_name is None: 318 error("You may only call `get_plugin_config()` from within a Meerschaum plugin.") 319 plugins_cf = get_config('plugins', warn=False) 320 if plugins_cf is None: 321 plugins_cf = {} 322 plugins_cf.update({parent_plugin_name: config_dict}) 323 cf = {'plugins' : plugins_cf} 324 return write_config(cf, **kw) 325 326 327@contextlib.contextmanager 328def replace_config(config_: Union[Dict[str, Any], None]): 329 """ 330 Temporarily override the Meerschaum config dictionary. 331 332 Parameters 333 ---------- 334 config_: Dict[str, Any] 335 The new config dictionary to temporarily replace the canonical `config`. 336 """ 337 if config_ is None: 338 try: 339 yield 340 except Exception: 341 pass 342 return 343 344 global _backup_config, _allow_write_missing 345 346 _backup_config = _config() 347 _allow_write_missing = False 348 set_config(config_) 349 350 try: 351 yield 352 finally: 353 set_config(_backup_config) 354 _allow_write_missing = True 355 356### This need to be below get_config to avoid a circular import. 357from meerschaum.config._read_config import read_config 358 359### If environment variable MRSM_CONFIG or MRSM_PATCH is set, patch config before anything else. 360from meerschaum.config.environment import ( 361 apply_environment_patches as _apply_environment_patches, 362 apply_environment_uris as _apply_environment_uris, 363) 364_apply_environment_uris() 365_apply_environment_patches() 366 367patch_config = None 368if paths.PATCH_DIR_PATH.exists(): 369 from meerschaum.utils.yaml import yaml, _yaml 370 if _yaml is not None: 371 patch_config = read_config(directory=paths.PATCH_DIR_PATH) 372 373permanent_patch_config = None 374if paths.PERMANENT_PATCH_DIR_PATH.exists(): 375 from meerschaum.utils.yaml import yaml, _yaml 376 if _yaml is not None: 377 permanent_patch_config = read_config(directory=paths.PERMANENT_PATCH_DIR_PATH) 378### If patches exist, apply to config. 379if patch_config is not None: 380 set_config(apply_patch_to_config(_config(), patch_config)) 381 if paths.PATCH_DIR_PATH.exists(): 382 shutil.rmtree(paths.PATCH_DIR_PATH) 383 384### if permanent_patch.yaml exists, apply patch to config, write config, and delete patch 385if permanent_patch_config is not None and paths.PERMANENT_PATCH_DIR_PATH.exists(): 386 print( 387 "Found permanent patch configuration. " + 388 "Updating main config and deleting permanent patch..." 389 ) 390 set_config(apply_patch_to_config(_config(), permanent_patch_config)) 391 write_config(_config()) 392 permanent_patch_config = None 393 if paths.PERMANENT_PATCH_DIR_PATH.exists(): 394 shutil.rmtree(paths.PERMANENT_PATCH_DIR_PATH) 395 if paths.DEFAULT_CONFIG_DIR_PATH.exists(): 396 shutil.rmtree(paths.DEFAULT_CONFIG_DIR_PATH) 397 398 399### Make sure readline is available for the portable version. 400environment_runtime = STATIC_CONFIG['environment']['runtime'] 401if environment_runtime in os.environ: 402 if os.environ[environment_runtime] == 'portable': 403 from meerschaum.utils.packages import ensure_readline 404 if not paths.PORTABLE_CHECK_READLINE_PATH.exists(): 405 ensure_readline() 406 paths.PORTABLE_CHECK_READLINE_PATH.touch() 407 408 409### If interactive REPL, print welcome header. 410__doc__ = f"Meerschaum v{__version__}" 411try: 412 interactive = False 413 if sys.ps1: 414 interactive = True 415except AttributeError: 416 interactive = False 417if interactive: 418 msg = __doc__ 419 print(msg, file=sys.stderr)
283def get_plugin_config( 284 *keys: str, 285 warn: bool = False, 286 **kw: Any 287) -> Optional[Any]: 288 """ 289 This may only be called from within a Meerschaum plugin. 290 See `meerschaum.config.get_config` for arguments. 291 """ 292 from meerschaum.utils.warnings import error 293 from meerschaum.plugins import _get_parent_plugin 294 parent_plugin_name = _get_parent_plugin(2) 295 if parent_plugin_name is None: 296 error( 297 "You may only call `get_plugin_config()` " 298 "from within a Meerschaum plugin." 299 ) 300 301 return get_config( 302 *(['plugins', parent_plugin_name] + list(keys)), 303 warn=warn, 304 **kw 305 )
This may only be called from within a Meerschaum plugin.
See meerschaum.config.get_config for arguments.
308def write_plugin_config( 309 config_dict: Dict[str, Any], 310 **kw: Any 311): 312 """ 313 Write a plugin's configuration dictionary. 314 """ 315 from meerschaum.utils.warnings import error 316 from meerschaum.plugins import _get_parent_plugin 317 parent_plugin_name = _get_parent_plugin(2) 318 if parent_plugin_name is None: 319 error("You may only call `get_plugin_config()` from within a Meerschaum plugin.") 320 plugins_cf = get_config('plugins', warn=False) 321 if plugins_cf is None: 322 plugins_cf = {} 323 plugins_cf.update({parent_plugin_name: config_dict}) 324 cf = {'plugins' : plugins_cf} 325 return write_config(cf, **kw)
Write a plugin's configuration dictionary.
111def get_config( 112 *keys: str, 113 patch: bool = True, 114 substitute: bool = True, 115 sync_files: bool = True, 116 write_missing: bool = True, 117 as_tuple: bool = False, 118 warn: bool = True, 119 debug: bool = False 120) -> Any: 121 """ 122 Return the Meerschaum configuration dictionary. 123 If positional arguments are provided, index by the keys. 124 Raises a warning if invalid keys are provided. 125 126 Parameters 127 ---------- 128 keys: str: 129 List of strings to index. 130 131 patch: bool, default True 132 If `True`, patch missing default keys into the config directory. 133 Defaults to `True`. 134 135 sync_files: bool, default True 136 If `True`, sync files if needed. 137 Defaults to `True`. 138 139 write_missing: bool, default True 140 If `True`, write default values when the main config files are missing. 141 Defaults to `True`. 142 143 substitute: bool, default True 144 If `True`, subsitute 'MRSM{}' values. 145 Defaults to `True`. 146 147 as_tuple: bool, default False 148 If `True`, return a tuple of type (success, value). 149 Defaults to `False`. 150 151 Returns 152 ------- 153 The value in the configuration directory, indexed by the provided keys. 154 155 Examples 156 -------- 157 >>> get_config('meerschaum', 'instance') 158 'sql:main' 159 >>> get_config('does', 'not', 'exist') 160 UserWarning: Invalid keys in config: ('does', 'not', 'exist') 161 """ 162 import json 163 164 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 165 if debug: 166 from meerschaum.utils.debug import dprint 167 dprint(f"Indexing keys: {keys}", color=False) 168 169 if len(keys) == 0: 170 _rc = _config( 171 substitute=substitute, 172 sync_files=sync_files, 173 write_missing=(write_missing and _allow_write_missing), 174 ) 175 if as_tuple: 176 return True, _rc 177 return _rc 178 179 ### Weird threading issues, only import if substitute is True. 180 if substitute: 181 from meerschaum.config._read_config import search_and_substitute_config 182 ### Invalidate the cache if it was read before with substitute=False 183 ### but there still exist substitutions. 184 if ( 185 config is not None and substitute and keys[0] != symlinks_key 186 and 'MRSM{' in json.dumps(config.get(keys[0])) 187 ): 188 try: 189 _subbed = search_and_substitute_config({keys[0]: config[keys[0]]}) 190 except Exception: 191 import traceback 192 traceback.print_exc() 193 _subbed = {keys[0]: config[keys[0]]} 194 195 config[keys[0]] = _subbed[keys[0]] 196 if symlinks_key in _subbed: 197 if symlinks_key not in config: 198 config[symlinks_key] = {} 199 config[symlinks_key] = apply_patch_to_config( 200 _subbed.get(symlinks_key, {}), 201 config.get(symlinks_key, {}), 202 ) 203 204 from meerschaum.config._sync import sync_files as _sync_files 205 if config is None: 206 _config(*keys, sync_files=sync_files) 207 208 invalid_keys = False 209 if keys[0] not in config and keys[0] != symlinks_key: 210 single_key_config = read_config( 211 keys=[keys[0]], substitute=substitute, write_missing=write_missing 212 ) 213 if keys[0] not in single_key_config: 214 invalid_keys = True 215 else: 216 config[keys[0]] = single_key_config.get(keys[0], None) 217 if symlinks_key in single_key_config and keys[0] in single_key_config[symlinks_key]: 218 if symlinks_key not in config: 219 config[symlinks_key] = {} 220 config[symlinks_key][keys[0]] = single_key_config[symlinks_key][keys[0]] 221 222 if sync_files: 223 _sync_files(keys=[keys[0]]) 224 225 c = config 226 if len(keys) > 0: 227 for k in keys: 228 try: 229 c = c[k] 230 except Exception: 231 invalid_keys = True 232 break 233 if invalid_keys: 234 ### Check if the keys are in the default configuration. 235 from meerschaum.config._default import default_config 236 in_default = True 237 patched_default_config = ( 238 search_and_substitute_config(default_config) 239 if substitute else copy.deepcopy(default_config) 240 ) 241 _c = patched_default_config 242 for k in keys: 243 try: 244 _c = _c[k] 245 except Exception: 246 in_default = False 247 if in_default: 248 c = _c 249 invalid_keys = False 250 warning_msg = f"Invalid keys in config: {keys}" 251 if not in_default: 252 try: 253 if warn: 254 from meerschaum.utils.warnings import warn as _warn 255 _warn(warning_msg, stacklevel=3, color=False) 256 except Exception: 257 if warn: 258 print(warning_msg) 259 if as_tuple: 260 return False, None 261 return None 262 263 ### Don't write keys that we haven't yet loaded into memory. 264 not_loaded_keys = [k for k in patched_default_config if k not in config] 265 for k in not_loaded_keys: 266 patched_default_config.pop(k, None) 267 268 set_config( 269 apply_patch_to_config( 270 patched_default_config, 271 config, 272 ) 273 ) 274 if patch and keys[0] != symlinks_key: 275 if write_missing: 276 write_config(config, debug=debug) 277 278 if as_tuple: 279 return (not invalid_keys), c 280 return c
Return the Meerschaum configuration dictionary. If positional arguments are provided, index by the keys. Raises a warning if invalid keys are provided.
Parameters
- keys (str:): List of strings to index.
- patch (bool, default True):
If
True, patch missing default keys into the config directory. Defaults toTrue. - sync_files (bool, default True):
If
True, sync files if needed. Defaults toTrue. - write_missing (bool, default True):
If
True, write default values when the main config files are missing. Defaults toTrue. - substitute (bool, default True):
If
True, subsitute 'MRSM{}' values. Defaults toTrue. - as_tuple (bool, default False):
If
True, return a tuple of type (success, value). Defaults toFalse.
Returns
- The value in the configuration directory, indexed by the provided keys.
Examples
>>> get_config('meerschaum', 'instance')
'sql:main'
>>> get_config('does', 'not', 'exist')
UserWarning: Invalid keys in config: ('does', 'not', 'exist')
78def write_config( 79 config_dict: Optional[Dict[str, Any]] = None, 80 directory: Union[str, pathlib.Path, None] = None, 81 debug: bool = False, 82 **kw: Any 83) -> bool: 84 """Write YAML and JSON files to the configuration directory. 85 86 Parameters 87 ---------- 88 config_dict: Optional[Dict[str, Any]], default None 89 A dictionary of keys to dictionaries of configuration. 90 Each key corresponds to a .yaml or .json config file. 91 Writing config to a directory with different keys 92 does not affect existing keys in that directory. 93 If not provided, use the currently loaded config dictionary. 94 95 directory: Union[str, pathlib.Path, None], default None 96 The directory to which the keys are written. 97 If not provided, use the default config path (`~/.config/meerschaum/config/`). 98 99 Returns 100 ------- 101 A bool indicating success. 102 103 """ 104 if directory is None: 105 import meerschaum.config.paths as paths 106 directory = paths.CONFIG_DIR_PATH 107 108 from meerschaum.config import _allow_write_missing 109 from meerschaum._internal.static import STATIC_CONFIG 110 from meerschaum.config._default import default_header_comment 111 from meerschaum.config._read_config import get_keyfile_path, revert_symlinks_config 112 from meerschaum.utils.yaml import yaml 113 from meerschaum.utils.misc import filter_keywords 114 import json 115 import os 116 if config_dict is None: 117 from meerschaum.config import _config 118 cf = _config(allow_replaced=False) 119 config_dict = cf 120 121 if not _allow_write_missing: 122 return False 123 124 default_filetype = STATIC_CONFIG['config']['default_filetype'] 125 filetype_dumpers = { 126 'yml' : yaml.dump, 127 'yaml' : yaml.dump, 128 'json' : json.dump, 129 } 130 131 config_dict = revert_symlinks_config(config_dict) 132 133 def determine_filetype(k, v): 134 if k == 'meerschaum': 135 return 'yaml' 136 if isinstance(v, dict) and 'filetype' in v: 137 return v['filetype'] 138 path = get_keyfile_path(k, create_new=False, directory=directory) 139 if path is None: 140 return default_filetype 141 filetype = path.suffix[1:] 142 if not isinstance(filetype, str) or filetype not in filetype_dumpers: 143 print(f"Invalid filetype '{filetype}' for '{k}'. Assuming {default_filetype}...") 144 filetype = default_filetype 145 return filetype 146 147 for k, v in config_dict.items(): 148 filetype = determine_filetype(k, v) 149 filename = str(k) + '.' + str(filetype) 150 filepath = os.path.join(directory, filename) 151 pathlib.Path(filepath).parent.mkdir(exist_ok=True) 152 with open(filepath, 'w+') as f: 153 try: 154 if k == 'meerschaum': 155 f.write(default_header_comment) 156 filetype_dumpers[filetype]( 157 v, f, 158 **filter_keywords( 159 filetype_dumpers[filetype], 160 sort_keys = False, 161 indent = 2 162 ) 163 ) 164 success = True 165 except Exception as e: 166 success = False 167 print("FAILED TO WRITE!") 168 print(e) 169 print(filter_keywords( 170 filetype_dumpers[filetype], 171 sort_keys=False, 172 indent = 2 173 )) 174 175 if not success: 176 try: 177 if os.path.exists(filepath): 178 os.remove(filepath) 179 except Exception: 180 print(f"Failed to write '{k}'") 181 return False 182 183 return True
Write YAML and JSON files to the configuration directory.
Parameters
- config_dict (Optional[Dict[str, Any]], default None): A dictionary of keys to dictionaries of configuration. Each key corresponds to a .yaml or .json config file. Writing config to a directory with different keys does not affect existing keys in that directory. If not provided, use the currently loaded config dictionary.
- directory (Union[str, pathlib.Path, None], default None):
The directory to which the keys are written.
If not provided, use the default config path (
~/.config/meerschaum/config/).
Returns
- A bool indicating success.
16def edit_config( 17 keys: Optional[List[str]] = None, 18 params: Optional[Dict[str, Any]] = None, 19 debug: bool = False, 20 **kw: Any 21) -> SuccessTuple: 22 """Edit the configuration files.""" 23 import meerschaum.config.paths as paths 24 from meerschaum.config import get_config, config 25 from meerschaum.config._read_config import get_keyfile_path, read_config, revert_symlinks_config 26 from meerschaum._internal.static import STATIC_CONFIG 27 from meerschaum.utils.packages import reload_meerschaum 28 from meerschaum.utils.misc import edit_file 29 from meerschaum.utils.warnings import warn 30 from meerschaum.utils.prompt import prompt 31 32 if keys is None: 33 keys = [] 34 35 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 36 def _edit_key(key: str): 37 new_key_config = None 38 while True: 39 ### If defined in default, create the config file. 40 symlinks_key_config = config.get(symlinks_key, {}).get(key, {}) 41 key_config = revert_symlinks_config({ 42 key: config.pop(key, {}), 43 symlinks_key: {key: symlinks_key_config}, 44 }) 45 keyfile_path = get_keyfile_path(key, create_new=True) 46 get_config(key, write_missing=True, warn=False) 47 48 edit_file(get_keyfile_path(key, create_new=True)) 49 50 ### TODO: verify that the file is valid. Retry if not. 51 try: 52 new_key_config = read_config( 53 paths.CONFIG_DIR_PATH, 54 [key], 55 write_missing=False, 56 raise_parsing_errors=True, 57 ) 58 except Exception: 59 if key_config: 60 config[key] = key_config 61 warn(f"Could not parse key '{key}'.", stack=False) 62 _ = prompt(f"Press [Enter] to edit '{keyfile_path}', [CTRL+C] to exit.") 63 continue 64 65 if new_key_config: 66 break 67 68 try: 69 for k in keys: 70 _edit_key(k) 71 except KeyboardInterrupt: 72 return False, "" 73 74 reload_meerschaum(debug=debug) 75 return (True, "Success")
Edit the configuration files.
98def set_config(cf: Dict[str, Any]) -> Dict[str, Any]: 99 """ 100 Set the configuration dictionary. 101 """ 102 global config 103 if not isinstance(cf, dict): 104 from meerschaum.utils.warnings import error 105 error(f"Invalid value for config: {cf}") 106 with _locks['config']: 107 config = cf 108 return config
Set the configuration dictionary.
328@contextlib.contextmanager 329def replace_config(config_: Union[Dict[str, Any], None]): 330 """ 331 Temporarily override the Meerschaum config dictionary. 332 333 Parameters 334 ---------- 335 config_: Dict[str, Any] 336 The new config dictionary to temporarily replace the canonical `config`. 337 """ 338 if config_ is None: 339 try: 340 yield 341 except Exception: 342 pass 343 return 344 345 global _backup_config, _allow_write_missing 346 347 _backup_config = _config() 348 _allow_write_missing = False 349 set_config(config_) 350 351 try: 352 yield 353 finally: 354 set_config(_backup_config) 355 _allow_write_missing = True
Temporarily override the Meerschaum config dictionary.
Parameters
- config_ (Dict[str, Any]):
The new config dictionary to temporarily replace the canonical
config.
216def search_and_substitute_config( 217 config: Dict[str, Any], 218 leading_key: str = "MRSM", 219 delimiter: str = ":", 220 begin_key: str = "{", 221 end_key: str = "}", 222 literal_key: str = '!', 223 keep_symlinks: bool = True, 224) -> Dict[str, Any]: 225 """ 226 Search the config for Meerschaum substitution syntax and substite with value of keys. 227 228 Parameters 229 ---------- 230 config: Dict[str, Any] 231 The Meerschaum configuration dictionary to search through. 232 233 leading_key: str, default 'MRSM' 234 The string with which to start the search. 235 236 begin_key: str, default '{' 237 The string to start the keys list. 238 239 end_key: str, default '}' 240 The string to end the keys list. 241 242 literal_key: str, default '!' 243 The string to force an literal interpretation of a value. 244 When the string is isolated, a literal interpreation is assumed and the surrounding 245 quotes are replaced. 246 247 E.g. Suppose a:b:c produces a dictionary {'d': 1}. 248 - 'MRSM{a:b:c}' => {'d': 1} : isolated 249 - ' MRSM{a:b:c} ' => ' "{\'d\': 1}"' : not isolated 250 - ' MRSM{!a:b:c} ' => ' {"d": 1}' : literal 251 252 keep_symlinks: bool, default True 253 If True, include the symlinks under the top-level key '_symlinks' (never written to a file). 254 Defaults to True. 255 256 Example: 257 258 ``` 259 MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host'] 260 ``` 261 262 Returns 263 ------- 264 The configuration dictionary with `MRSM{}` symlinks replaced with 265 the values from the current configuration. 266 """ 267 import re 268 import json 269 from meerschaum.config import get_config 270 needle = leading_key + begin_key 271 272 _links = [] 273 if keep_symlinks: 274 def _find_symlinks(d, _keys: Optional[List[str]] = None): 275 if _keys is None: 276 _keys = [] 277 if not isinstance(d, dict): 278 return 279 for k, v in d.items(): 280 if isinstance(v, dict): 281 _find_symlinks(v, _keys + [k]) 282 elif isinstance(v, str) and needle in v: 283 _links.append((_keys + [k], v)) 284 _find_symlinks(config) 285 286 haystack = json.dumps(config, separators=(',', ':')) 287 if needle not in haystack: 288 parsed_config = config 289 else: 290 patterns = {} 291 isolated_patterns = {} 292 literal_patterns = {} 293 memo = {} 294 295 pattern_re = re.compile( 296 re.escape(needle) + r'(?P<pattern_keys>.*?)' + re.escape(end_key) 297 ) 298 299 for match in pattern_re.finditer(haystack): 300 pattern = match.group(0) 301 pattern_keys_raw = match.group('pattern_keys') 302 pattern_keys = pattern_keys_raw.split(delimiter) 303 304 start, end = match.span() 305 prior = haystack[start - 1] if start > 0 else None 306 after = haystack[end] if end < len(haystack) else None 307 308 force_literal = False 309 keys = [k for k in pattern_keys] 310 if keys and str(keys[0]).startswith(literal_key): 311 keys[0] = str(keys[0])[len(literal_key):] 312 force_literal = True 313 if len(keys) == 1 and keys[0] == '': 314 keys = [] 315 316 cache_key = tuple(keys) 317 if cache_key in memo: 318 valid, value = memo[cache_key] 319 else: 320 try: 321 valid, value = get_config( 322 *keys, 323 substitute=False, 324 as_tuple=True, 325 write_missing=False, 326 sync_files=False, 327 ) 328 except Exception: 329 valid, value = False, None 330 memo[cache_key] = (valid, value) 331 332 if not valid: 333 continue 334 335 patterns[pattern] = value 336 isolated_patterns[pattern] = (prior == '"' and after == '"') 337 literal_patterns[pattern] = force_literal 338 339 for pattern, value in patterns.items(): 340 if isolated_patterns[pattern]: 341 haystack = haystack.replace( 342 json.dumps(pattern), 343 json.dumps(value), 344 ) 345 elif literal_patterns[pattern]: 346 haystack = haystack.replace( 347 pattern, 348 ( 349 json.dumps(value) 350 .replace("\\", "\\\\") 351 .replace('"', '\\"') 352 .replace("'", "\\'") 353 ) 354 ) 355 else: 356 haystack = haystack.replace(pattern, str(value)) 357 358 parsed_config = json.loads(haystack) or {} 359 360 symlinks = {} 361 if keep_symlinks: 362 for _keys, _pattern in _links: 363 s = symlinks 364 for k in _keys[:-1]: 365 if k not in s: 366 s[k] = {} 367 s = s[k] 368 s[_keys[-1]] = _pattern 369 370 from meerschaum.config._patch import apply_patch_to_config 371 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 372 if symlinks: 373 if symlinks_key not in parsed_config: 374 parsed_config[symlinks_key] = symlinks 375 else: 376 parsed_config[symlinks_key] = apply_patch_to_config( 377 parsed_config[symlinks_key], 378 symlinks, 379 warn=False, 380 ) 381 382 return parsed_config
Search the config for Meerschaum substitution syntax and substite with value of keys.
Parameters
- config (Dict[str, Any]): The Meerschaum configuration dictionary to search through.
- leading_key (str, default 'MRSM'): The string with which to start the search.
- begin_key (str, default '{'): The string to start the keys list.
- end_key (str, default '}'): The string to end the keys list.
literal_key (str, default '!'): The string to force an literal interpretation of a value. When the string is isolated, a literal interpreation is assumed and the surrounding quotes are replaced.
E.g. Suppose a:b:c produces a dictionary {'d': 1}.
- 'MRSM{a:b:c}' => {'d': 1} : isolated
- ' MRSM{a:b:c} ' => ' "{'d': 1}"' : not isolated
- ' MRSM{!a:b:c} ' => ' {"d": 1}' : literal
keep_symlinks (bool, default True): If True, include the symlinks under the top-level key '_symlinks' (never written to a file). Defaults to True.
Example:
MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host']
Returns
- The configuration dictionary with
MRSM{}symlinks replaced with - the values from the current configuration.
385def revert_symlinks_config(config: Dict[str, Any]) -> Dict[str, Any]: 386 """ 387 Given a configuration dictionary, re-apply the values from the 388 accompanying `_symlinks` dictionary. 389 390 Parameters 391 ---------- 392 config: Dict[str, Any] 393 The configuration dictionary containing a `_symlinks` dictionary. 394 395 Returns 396 ------- 397 A configuration dictionary with `_symlinks` re-applied. 398 """ 399 import copy 400 from meerschaum._internal.static import STATIC_CONFIG 401 402 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 403 if symlinks_key not in config: 404 return config 405 406 reverted_config = copy.deepcopy(config) 407 symlinks_config = reverted_config.pop(symlinks_key) 408 409 def deep_patch(target_dict, patch_dict): 410 for key, value in patch_dict.items(): 411 if isinstance(value, dict) and key in target_dict and isinstance(target_dict[key], dict): 412 deep_patch(target_dict[key], value) 413 else: 414 target_dict[key] = value 415 416 deep_patch(reverted_config, symlinks_config) 417 return reverted_config
Given a configuration dictionary, re-apply the values from the
accompanying _symlinks dictionary.
Parameters
- config (Dict[str, Any]):
The configuration dictionary containing a
_symlinksdictionary.
Returns
- A configuration dictionary with
_symlinksre-applied.
420def get_possible_keys() -> List[str]: 421 """ 422 Return a list of possible top-level keys. 423 """ 424 import os 425 import meerschaum.config.paths as paths 426 from meerschaum.config._default import default_config 427 keys = set() 428 for key in default_config: 429 keys.add(key) 430 for filename in os.listdir(paths.CONFIG_DIR_PATH): 431 keys.add('.'.join(filename.split('.')[:-1])) 432 return sorted(list(keys))
Return a list of possible top-level keys.
435def get_keyfile_path( 436 key: str, 437 create_new: bool = False, 438 directory: Union[pathlib.Path, str, None] = None, 439) -> Union[pathlib.Path, None]: 440 """Determine a key's file path.""" 441 import os 442 import pathlib 443 if directory is None: 444 import meerschaum.config.paths as paths 445 directory = paths.CONFIG_DIR_PATH 446 447 try: 448 return pathlib.Path( 449 os.path.join( 450 directory, 451 read_config( 452 keys=[key], 453 with_filenames=True, 454 write_missing=False, 455 substitute=False, 456 )[1][0] 457 ) 458 ) 459 except IndexError: 460 if create_new: 461 default_filetype = STATIC_CONFIG['config']['default_filetype'] 462 return pathlib.Path(os.path.join(directory, key + '.' + default_filetype)) 463 return None
Determine a key's file path.
15def apply_patch_to_config( 16 config: Dict[str, Any], 17 patch: Dict[str, Any], 18 warn: bool = False, 19) -> Dict[str, Any]: 20 """Patch the config dict with a new dict (cascade patching).""" 21 if not isinstance(patch, dict) or not patch: 22 return config 23 if not isinstance(config, dict) or not config: 24 return copy.deepcopy(patch) 25 26 base = config.copy() 27 28 for key, value in patch.items(): 29 if isinstance(value, dict) and isinstance(base.get(key), dict): 30 base[key] = apply_patch_to_config(base[key], value, warn=warn) 31 elif isinstance(value, dict): 32 if warn and key in base: 33 _warn(f"Overwriting the value {base[key]} with a dictionary:\n{value}") 34 base[key] = copy.deepcopy(value) 35 else: 36 base[key] = value 37 38 return base
Patch the config dict with a new dict (cascade patching).
16def read_config( 17 directory: Union[pathlib.Path, str, None] = None, 18 keys: Optional[List[str]] = None, 19 write_missing: bool = True, 20 substitute: bool = True, 21 with_filenames: bool = False, 22 raise_parsing_errors: bool = False, 23) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[str]]]: 24 """ 25 Read the configuration directory. 26 27 Parameters 28 ---------- 29 directory: Union[pathlib.Path, str, None], default None 30 The directory with configuration files (.json and .yaml). 31 32 keys: Optional[List[str]], default None 33 Which configuration files to read. 34 35 write_missing: bool, default True 36 If a keyfile does not exist but is defined in the default configuration, 37 write the file to disk. 38 39 substitute: bool, default True 40 Replace `MRSM{}` syntax with configuration values. 41 42 with_filename: bool, default False 43 If `True`, return a tuple of the configuration dictionary with a list of read filenames. 44 45 raise_parsing_errors: bool, default False 46 If `True`, re-raise parsing exceptions. 47 48 Examples 49 -------- 50 >>> read_config(keys=['meerschaum'], with_filename=True) 51 >>> ({...}, ['meerschaum.yaml']) 52 """ 53 import os 54 import json 55 import itertools 56 import meerschaum.config.paths as paths 57 from meerschaum.utils.yaml import yaml, _yaml 58 from meerschaum.config._patch import apply_patch_to_config 59 if directory is None: 60 directory = paths.CONFIG_DIR_PATH 61 62 if _yaml is None: 63 print('Could not import YAML! Reverting to default configuration.') 64 from meerschaum.config._default import default_config 65 return default_config 66 67 ### Each key corresponds to a YAML or JSON file. 68 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 69 config = {} 70 config_to_write = {} 71 72 default_filetype = STATIC_CONFIG['config']['default_filetype'] 73 filetype_loaders = { 74 'yml': yaml.load, 75 'yaml': yaml.load, 76 'json': json.load, 77 } 78 79 ### Construct filekeys (files to parse). 80 filekeys = [] 81 filenames = os.listdir(directory) 82 missing_keys, found_keys = set(), set() 83 if keys is None: 84 _filekeys = filenames 85 else: 86 _filekeys = [] 87 for k in keys: 88 for ft in filetype_loaders: 89 if str(k) + '.' + str(ft) in filenames: 90 _filekeys.append(str(k) + '.' + str(ft)) 91 found_keys.add(k) 92 if k in missing_keys: 93 missing_keys.remove(k) 94 elif k not in found_keys: 95 missing_keys.add(k) 96 97 ### Check for missing files with default keys. 98 if len(missing_keys) > 0: 99 from meerschaum.config._default import default_config 100 for mk in missing_keys: 101 if mk not in default_config: 102 continue 103 _default_dict = ( 104 search_and_substitute_config(default_config) if substitute 105 else default_config 106 ) 107 ### If default config contains symlinks, add them to the config to write. 108 try: 109 _default_symlinks = _default_dict[symlinks_key][mk] 110 except KeyError: 111 _default_symlinks = {} 112 113 config[mk] = _default_dict[mk] 114 if _default_symlinks: 115 if symlinks_key not in config: 116 config[symlinks_key] = {} 117 if symlinks_key not in config_to_write: 118 config_to_write[symlinks_key] = {} 119 120 if mk not in config[symlinks_key]: 121 config[symlinks_key][mk] = {} 122 if mk not in config_to_write[symlinks_key]: 123 config_to_write[symlinks_key][mk] = {} 124 125 config[symlinks_key][mk] = apply_patch_to_config( 126 config[symlinks_key][mk], 127 _default_symlinks 128 ) 129 config_to_write[symlinks_key][mk] = config[symlinks_key][mk] 130 131 ### Write the default key. 132 config_to_write[mk] = config[mk] 133 134 ### Write missing keys if necessary. 135 if len(config_to_write) > 0 and write_missing: 136 from meerschaum.config._edit import write_config 137 write_config(config_to_write, directory) 138 139 ### Check for duplicate files. 140 ### Found help on StackOverflow: 141 ### https://stackoverflow.com/questions/26618688/python-iterate-over-a-list- 142 ### of-files-finding-same-filenames-but-different-exten 143 keygroups = { 144 key: list(value) 145 for key, value in itertools.groupby( 146 sorted(_filekeys, key = lambda e: os.path.splitext(e)[0]), 147 key = lambda e: os.path.splitext(e)[0] 148 ) 149 } 150 for k, v in keygroups.items(): 151 fn = v[0] 152 if len(v) > 1: 153 if k + '.' + default_filetype in v: 154 fn = k + '.' + default_filetype 155 print( 156 f"Found multiple config files named '{k}'. " + 157 f"Will attempt to parse '{fn}' for key '{k}'." 158 ) 159 filekeys.append(fn) 160 161 _seen_keys = [] 162 for filename in filekeys: 163 filepath = os.path.join(directory, filename) 164 _parts = filename.split('.') 165 _type = _parts[-1] 166 key = '.'.join(_parts[:-1]) 167 ### Check if we've seen this key before (e.g. test.yaml, test.yml, test.json). 168 if key in _seen_keys: 169 print( 170 f"Multiple files with the name '{key}' found in '{str(directory)}'. " + 171 f"Reading from '{filename}'." 172 ) 173 if len(_parts) < 2 or _type not in filetype_loaders: 174 print(f"Unknown file '{filename}' in '{str(directory)}'. Skipping...") 175 176 while True: 177 try: 178 with open(filepath, 'r', encoding='utf-8') as f: 179 try: 180 _config_key = filetype_loaders[_type](f) 181 except Exception as e: 182 print(f"Error processing file: {filepath}") 183 if raise_parsing_errors: 184 raise e 185 import traceback 186 traceback.print_exc() 187 _config_key = {} 188 _single_key_config = ( 189 search_and_substitute_config({key: _config_key}) if substitute 190 else {key: _config_key} 191 ) 192 config[key] = _single_key_config[key] 193 if ( 194 symlinks_key in _single_key_config 195 and key in _single_key_config[symlinks_key] 196 ): 197 if symlinks_key not in config: 198 config[symlinks_key] = {} 199 config[symlinks_key][key] = _single_key_config[symlinks_key][key] 200 break 201 except Exception as e: 202 if raise_parsing_errors: 203 raise e 204 print(f"Unable to parse {filename}!") 205 import traceback 206 traceback.print_exc() 207 input(f"Press [Enter] to open '{filename}' and fix formatting errors.") 208 from meerschaum.utils.misc import edit_file 209 edit_file(filepath) 210 211 if with_filenames: 212 return config, filekeys 213 return config
Read the configuration directory.
Parameters
- directory (Union[pathlib.Path, str, None], default None): The directory with configuration files (.json and .yaml).
- keys (Optional[List[str]], default None): Which configuration files to read.
- write_missing (bool, default True): If a keyfile does not exist but is defined in the default configuration, write the file to disk.
- substitute (bool, default True):
Replace
MRSM{}syntax with configuration values. - with_filename (bool, default False):
If
True, return a tuple of the configuration dictionary with a list of read filenames. - raise_parsing_errors (bool, default False):
If
True, re-raise parsing exceptions.
Examples
>>> read_config(keys=['meerschaum'], with_filename=True)
>>> ({...}, ['meerschaum.yaml'])