meerschaum.config
Meerschaum v3.3.1
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 'environment', 51 'paths', 52 'STATIC_CONFIG', 53) 54__pdoc__ = {'static': False, 'resources': False, 'stack': False, } 55_locks = {'config': RLock()} 56 57### apply config preprocessing (e.g. main to meta) 58config = {} 59_backup_config = None 60_allow_write_missing: bool = True 61 62 63def _config( 64 *keys: str, 65 reload: bool = False, 66 substitute: bool = True, 67 sync_files: bool = True, 68 allow_replaced: bool = True, 69 write_missing: bool = True, 70) -> Dict[str, Any]: 71 """ 72 Read and process the configuration file. 73 """ 74 global config, _backup_config 75 76 if config is None or reload: 77 with _locks['config']: 78 config = {} 79 80 if keys and keys[0] not in config: 81 from meerschaum.config._sync import sync_files as _sync_files 82 key_config = read_config( 83 keys = [keys[0]], 84 substitute = substitute, 85 write_missing = write_missing and _allow_write_missing, 86 ) 87 if keys[0] in key_config: 88 config[keys[0]] = key_config[keys[0]] 89 if sync_files and _allow_write_missing: 90 _sync_files(keys=[keys[0] if keys else None]) 91 92 if not allow_replaced: 93 return _backup_config if _backup_config is not None else config 94 95 return config 96 97 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 109 110 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 281 282 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 ) 306 307 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) 326 327 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 356 357### This need to be below get_config to avoid a circular import. 358from meerschaum.config._read_config import read_config 359 360### If environment variable MRSM_CONFIG or MRSM_PATCH is set, patch config before anything else. 361from meerschaum.config.environment import ( 362 apply_environment_patches as _apply_environment_patches, 363 apply_environment_uris as _apply_environment_uris, 364) 365_apply_environment_uris() 366_apply_environment_patches() 367 368patch_config = None 369if paths.PATCH_DIR_PATH.exists(): 370 from meerschaum.utils.yaml import yaml, _yaml 371 if _yaml is not None: 372 patch_config = read_config(directory=paths.PATCH_DIR_PATH) 373 374permanent_patch_config = None 375if paths.PERMANENT_PATCH_DIR_PATH.exists(): 376 from meerschaum.utils.yaml import yaml, _yaml 377 if _yaml is not None: 378 permanent_patch_config = read_config(directory=paths.PERMANENT_PATCH_DIR_PATH) 379### If patches exist, apply to config. 380if patch_config is not None: 381 set_config(apply_patch_to_config(_config(), patch_config)) 382 if paths.PATCH_DIR_PATH.exists(): 383 shutil.rmtree(paths.PATCH_DIR_PATH) 384 385### if permanent_patch.yaml exists, apply patch to config, write config, and delete patch 386if permanent_patch_config is not None and paths.PERMANENT_PATCH_DIR_PATH.exists(): 387 print( 388 "Found permanent patch configuration. " + 389 "Updating main config and deleting permanent patch..." 390 ) 391 set_config(apply_patch_to_config(_config(), permanent_patch_config)) 392 write_config(_config()) 393 permanent_patch_config = None 394 if paths.PERMANENT_PATCH_DIR_PATH.exists(): 395 shutil.rmtree(paths.PERMANENT_PATCH_DIR_PATH) 396 if paths.DEFAULT_CONFIG_DIR_PATH.exists(): 397 shutil.rmtree(paths.DEFAULT_CONFIG_DIR_PATH) 398 399 400### Make sure readline is available for the portable version. 401environment_runtime = STATIC_CONFIG['environment']['runtime'] 402if environment_runtime in os.environ: 403 if os.environ[environment_runtime] == 'portable': 404 from meerschaum.utils.packages import ensure_readline 405 if not paths.PORTABLE_CHECK_READLINE_PATH.exists(): 406 ensure_readline() 407 paths.PORTABLE_CHECK_READLINE_PATH.touch() 408 409 410### If interactive REPL, print welcome header. 411__doc__ = f"Meerschaum v{__version__}" 412try: 413 interactive = False 414 if sys.ps1: 415 interactive = True 416except AttributeError: 417 interactive = False 418if interactive: 419 msg = __doc__ 420 print(msg, file=sys.stderr)
284def get_plugin_config( 285 *keys: str, 286 warn: bool = False, 287 **kw: Any 288) -> Optional[Any]: 289 """ 290 This may only be called from within a Meerschaum plugin. 291 See `meerschaum.config.get_config` for arguments. 292 """ 293 from meerschaum.utils.warnings import error 294 from meerschaum.plugins import _get_parent_plugin 295 parent_plugin_name = _get_parent_plugin(2) 296 if parent_plugin_name is None: 297 error( 298 "You may only call `get_plugin_config()` " 299 "from within a Meerschaum plugin." 300 ) 301 302 return get_config( 303 *(['plugins', parent_plugin_name] + list(keys)), 304 warn=warn, 305 **kw 306 )
This may only be called from within a Meerschaum plugin.
See meerschaum.config.get_config for arguments.
309def write_plugin_config( 310 config_dict: Dict[str, Any], 311 **kw: Any 312): 313 """ 314 Write a plugin's configuration dictionary. 315 """ 316 from meerschaum.utils.warnings import error 317 from meerschaum.plugins import _get_parent_plugin 318 parent_plugin_name = _get_parent_plugin(2) 319 if parent_plugin_name is None: 320 error("You may only call `get_plugin_config()` from within a Meerschaum plugin.") 321 plugins_cf = get_config('plugins', warn=False) 322 if plugins_cf is None: 323 plugins_cf = {} 324 plugins_cf.update({parent_plugin_name: config_dict}) 325 cf = {'plugins' : plugins_cf} 326 return write_config(cf, **kw)
Write a plugin's configuration dictionary.
112def get_config( 113 *keys: str, 114 patch: bool = True, 115 substitute: bool = True, 116 sync_files: bool = True, 117 write_missing: bool = True, 118 as_tuple: bool = False, 119 warn: bool = True, 120 debug: bool = False 121) -> Any: 122 """ 123 Return the Meerschaum configuration dictionary. 124 If positional arguments are provided, index by the keys. 125 Raises a warning if invalid keys are provided. 126 127 Parameters 128 ---------- 129 keys: str: 130 List of strings to index. 131 132 patch: bool, default True 133 If `True`, patch missing default keys into the config directory. 134 Defaults to `True`. 135 136 sync_files: bool, default True 137 If `True`, sync files if needed. 138 Defaults to `True`. 139 140 write_missing: bool, default True 141 If `True`, write default values when the main config files are missing. 142 Defaults to `True`. 143 144 substitute: bool, default True 145 If `True`, subsitute 'MRSM{}' values. 146 Defaults to `True`. 147 148 as_tuple: bool, default False 149 If `True`, return a tuple of type (success, value). 150 Defaults to `False`. 151 152 Returns 153 ------- 154 The value in the configuration directory, indexed by the provided keys. 155 156 Examples 157 -------- 158 >>> get_config('meerschaum', 'instance') 159 'sql:main' 160 >>> get_config('does', 'not', 'exist') 161 UserWarning: Invalid keys in config: ('does', 'not', 'exist') 162 """ 163 import json 164 165 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 166 if debug: 167 from meerschaum.utils.debug import dprint 168 dprint(f"Indexing keys: {keys}", color=False) 169 170 if len(keys) == 0: 171 _rc = _config( 172 substitute=substitute, 173 sync_files=sync_files, 174 write_missing=(write_missing and _allow_write_missing), 175 ) 176 if as_tuple: 177 return True, _rc 178 return _rc 179 180 ### Weird threading issues, only import if substitute is True. 181 if substitute: 182 from meerschaum.config._read_config import search_and_substitute_config 183 ### Invalidate the cache if it was read before with substitute=False 184 ### but there still exist substitutions. 185 if ( 186 config is not None and substitute and keys[0] != symlinks_key 187 and 'MRSM{' in json.dumps(config.get(keys[0])) 188 ): 189 try: 190 _subbed = search_and_substitute_config({keys[0]: config[keys[0]]}) 191 except Exception: 192 import traceback 193 traceback.print_exc() 194 _subbed = {keys[0]: config[keys[0]]} 195 196 config[keys[0]] = _subbed[keys[0]] 197 if symlinks_key in _subbed: 198 if symlinks_key not in config: 199 config[symlinks_key] = {} 200 config[symlinks_key] = apply_patch_to_config( 201 _subbed.get(symlinks_key, {}), 202 config.get(symlinks_key, {}), 203 ) 204 205 from meerschaum.config._sync import sync_files as _sync_files 206 if config is None: 207 _config(*keys, sync_files=sync_files) 208 209 invalid_keys = False 210 if keys[0] not in config and keys[0] != symlinks_key: 211 single_key_config = read_config( 212 keys=[keys[0]], substitute=substitute, write_missing=write_missing 213 ) 214 if keys[0] not in single_key_config: 215 invalid_keys = True 216 else: 217 config[keys[0]] = single_key_config.get(keys[0], None) 218 if symlinks_key in single_key_config and keys[0] in single_key_config[symlinks_key]: 219 if symlinks_key not in config: 220 config[symlinks_key] = {} 221 config[symlinks_key][keys[0]] = single_key_config[symlinks_key][keys[0]] 222 223 if sync_files: 224 _sync_files(keys=[keys[0]]) 225 226 c = config 227 if len(keys) > 0: 228 for k in keys: 229 try: 230 c = c[k] 231 except Exception: 232 invalid_keys = True 233 break 234 if invalid_keys: 235 ### Check if the keys are in the default configuration. 236 from meerschaum.config._default import default_config 237 in_default = True 238 patched_default_config = ( 239 search_and_substitute_config(default_config) 240 if substitute else copy.deepcopy(default_config) 241 ) 242 _c = patched_default_config 243 for k in keys: 244 try: 245 _c = _c[k] 246 except Exception: 247 in_default = False 248 if in_default: 249 c = _c 250 invalid_keys = False 251 warning_msg = f"Invalid keys in config: {keys}" 252 if not in_default: 253 try: 254 if warn: 255 from meerschaum.utils.warnings import warn as _warn 256 _warn(warning_msg, stacklevel=3, color=False) 257 except Exception: 258 if warn: 259 print(warning_msg) 260 if as_tuple: 261 return False, None 262 return None 263 264 ### Don't write keys that we haven't yet loaded into memory. 265 not_loaded_keys = [k for k in patched_default_config if k not in config] 266 for k in not_loaded_keys: 267 patched_default_config.pop(k, None) 268 269 set_config( 270 apply_patch_to_config( 271 patched_default_config, 272 config, 273 ) 274 ) 275 if patch and keys[0] != symlinks_key: 276 if write_missing: 277 write_config(config, debug=debug) 278 279 if as_tuple: 280 return (not invalid_keys), c 281 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.
99def set_config(cf: Dict[str, Any]) -> Dict[str, Any]: 100 """ 101 Set the configuration dictionary. 102 """ 103 global config 104 if not isinstance(cf, dict): 105 from meerschaum.utils.warnings import error 106 error(f"Invalid value for config: {cf}") 107 with _locks['config']: 108 config = cf 109 return config
Set the configuration dictionary.
329@contextlib.contextmanager 330def replace_config(config_: Union[Dict[str, Any], None]): 331 """ 332 Temporarily override the Meerschaum config dictionary. 333 334 Parameters 335 ---------- 336 config_: Dict[str, Any] 337 The new config dictionary to temporarily replace the canonical `config`. 338 """ 339 if config_ is None: 340 try: 341 yield 342 except Exception: 343 pass 344 return 345 346 global _backup_config, _allow_write_missing 347 348 _backup_config = _config() 349 _allow_write_missing = False 350 set_config(config_) 351 352 try: 353 yield 354 finally: 355 set_config(_backup_config) 356 _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'])