meerschaum.config
Meerschaum v3.4.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 and write_missing: 275 ### Only persist defaults when the key's file is genuinely absent. 276 ### Never overwrite an existing file (e.g. one that failed to parse) ─ 277 ### doing so would clobber the user's config with default values. 278 ### Brand-new config files are still created by `read_config()`. 279 from meerschaum.config._read_config import get_keyfile_path 280 keyfile_exists = get_keyfile_path(keys[0], create_new=False) is not None 281 if not keyfile_exists: 282 write_config(config, debug=debug) 283 284 if as_tuple: 285 return (not invalid_keys), c 286 return c 287 288 289def get_plugin_config( 290 *keys: str, 291 warn: bool = False, 292 **kw: Any 293) -> Optional[Any]: 294 """ 295 This may only be called from within a Meerschaum plugin. 296 See `meerschaum.config.get_config` for arguments. 297 """ 298 from meerschaum.utils.warnings import error 299 from meerschaum.plugins import _get_parent_plugin 300 parent_plugin_name = _get_parent_plugin(2) 301 if parent_plugin_name is None: 302 error( 303 "You may only call `get_plugin_config()` " 304 "from within a Meerschaum plugin." 305 ) 306 307 return get_config( 308 *(['plugins', parent_plugin_name] + list(keys)), 309 warn=warn, 310 **kw 311 ) 312 313 314def write_plugin_config( 315 config_dict: Dict[str, Any], 316 **kw: Any 317): 318 """ 319 Write a plugin's configuration dictionary. 320 """ 321 from meerschaum.utils.warnings import error 322 from meerschaum.plugins import _get_parent_plugin 323 parent_plugin_name = _get_parent_plugin(2) 324 if parent_plugin_name is None: 325 error("You may only call `get_plugin_config()` from within a Meerschaum plugin.") 326 plugins_cf = get_config('plugins', warn=False) 327 if plugins_cf is None: 328 plugins_cf = {} 329 plugins_cf.update({parent_plugin_name: config_dict}) 330 cf = {'plugins' : plugins_cf} 331 return write_config(cf, **kw) 332 333 334@contextlib.contextmanager 335def replace_config(config_: Union[Dict[str, Any], None]): 336 """ 337 Temporarily override the Meerschaum config dictionary. 338 339 Parameters 340 ---------- 341 config_: Dict[str, Any] 342 The new config dictionary to temporarily replace the canonical `config`. 343 """ 344 if config_ is None: 345 try: 346 yield 347 except Exception: 348 pass 349 return 350 351 global _backup_config, _allow_write_missing 352 353 _backup_config = _config() 354 _allow_write_missing = False 355 set_config(config_) 356 357 try: 358 yield 359 finally: 360 set_config(_backup_config) 361 _allow_write_missing = True 362 363### This need to be below get_config to avoid a circular import. 364from meerschaum.config._read_config import read_config 365 366### If environment variable MRSM_CONFIG or MRSM_PATCH is set, patch config before anything else. 367from meerschaum.config.environment import ( 368 apply_environment_patches as _apply_environment_patches, 369 apply_environment_uris as _apply_environment_uris, 370) 371_apply_environment_uris() 372_apply_environment_patches() 373 374patch_config = None 375if paths.PATCH_DIR_PATH.exists(): 376 from meerschaum.utils.yaml import yaml, _yaml 377 if _yaml is not None: 378 patch_config = read_config(directory=paths.PATCH_DIR_PATH) 379 380permanent_patch_config = None 381if paths.PERMANENT_PATCH_DIR_PATH.exists(): 382 from meerschaum.utils.yaml import yaml, _yaml 383 if _yaml is not None: 384 permanent_patch_config = read_config(directory=paths.PERMANENT_PATCH_DIR_PATH) 385### If patches exist, apply to config. 386if patch_config is not None: 387 set_config(apply_patch_to_config(_config(), patch_config)) 388 if paths.PATCH_DIR_PATH.exists(): 389 shutil.rmtree(paths.PATCH_DIR_PATH) 390 391### if permanent_patch.yaml exists, apply patch to config, write config, and delete patch 392if permanent_patch_config is not None and paths.PERMANENT_PATCH_DIR_PATH.exists(): 393 print( 394 "Found permanent patch configuration. " + 395 "Updating main config and deleting permanent patch..." 396 ) 397 set_config(apply_patch_to_config(_config(), permanent_patch_config)) 398 write_config(_config()) 399 permanent_patch_config = None 400 if paths.PERMANENT_PATCH_DIR_PATH.exists(): 401 shutil.rmtree(paths.PERMANENT_PATCH_DIR_PATH) 402 if paths.DEFAULT_CONFIG_DIR_PATH.exists(): 403 shutil.rmtree(paths.DEFAULT_CONFIG_DIR_PATH) 404 405 406### Make sure readline is available for the portable version. 407environment_runtime = STATIC_CONFIG['environment']['runtime'] 408if environment_runtime in os.environ: 409 if os.environ[environment_runtime] == 'portable': 410 from meerschaum.utils.packages import ensure_readline 411 if not paths.PORTABLE_CHECK_READLINE_PATH.exists(): 412 ensure_readline() 413 paths.PORTABLE_CHECK_READLINE_PATH.touch() 414 415 416### If interactive REPL, print welcome header. 417__doc__ = f"Meerschaum v{__version__}" 418try: 419 interactive = False 420 if sys.ps1: 421 interactive = True 422except AttributeError: 423 interactive = False 424if interactive: 425 msg = __doc__ 426 print(msg, file=sys.stderr)
290def get_plugin_config( 291 *keys: str, 292 warn: bool = False, 293 **kw: Any 294) -> Optional[Any]: 295 """ 296 This may only be called from within a Meerschaum plugin. 297 See `meerschaum.config.get_config` for arguments. 298 """ 299 from meerschaum.utils.warnings import error 300 from meerschaum.plugins import _get_parent_plugin 301 parent_plugin_name = _get_parent_plugin(2) 302 if parent_plugin_name is None: 303 error( 304 "You may only call `get_plugin_config()` " 305 "from within a Meerschaum plugin." 306 ) 307 308 return get_config( 309 *(['plugins', parent_plugin_name] + list(keys)), 310 warn=warn, 311 **kw 312 )
This may only be called from within a Meerschaum plugin.
See meerschaum.config.get_config for arguments.
315def write_plugin_config( 316 config_dict: Dict[str, Any], 317 **kw: Any 318): 319 """ 320 Write a plugin's configuration dictionary. 321 """ 322 from meerschaum.utils.warnings import error 323 from meerschaum.plugins import _get_parent_plugin 324 parent_plugin_name = _get_parent_plugin(2) 325 if parent_plugin_name is None: 326 error("You may only call `get_plugin_config()` from within a Meerschaum plugin.") 327 plugins_cf = get_config('plugins', warn=False) 328 if plugins_cf is None: 329 plugins_cf = {} 330 plugins_cf.update({parent_plugin_name: config_dict}) 331 cf = {'plugins' : plugins_cf} 332 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 and write_missing: 276 ### Only persist defaults when the key's file is genuinely absent. 277 ### Never overwrite an existing file (e.g. one that failed to parse) ─ 278 ### doing so would clobber the user's config with default values. 279 ### Brand-new config files are still created by `read_config()`. 280 from meerschaum.config._read_config import get_keyfile_path 281 keyfile_exists = get_keyfile_path(keys[0], create_new=False) is not None 282 if not keyfile_exists: 283 write_config(config, debug=debug) 284 285 if as_tuple: 286 return (not invalid_keys), c 287 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')
99def write_config( 100 config_dict: Optional[Dict[str, Any]] = None, 101 directory: Union[str, pathlib.Path, None] = None, 102 debug: bool = False, 103 **kw: Any 104) -> bool: 105 """Write YAML and JSON files to the configuration directory. 106 107 Parameters 108 ---------- 109 config_dict: Optional[Dict[str, Any]], default None 110 A dictionary of keys to dictionaries of configuration. 111 Each key corresponds to a .yaml or .json config file. 112 Writing config to a directory with different keys 113 does not affect existing keys in that directory. 114 If not provided, use the currently loaded config dictionary. 115 116 directory: Union[str, pathlib.Path, None], default None 117 The directory to which the keys are written. 118 If not provided, use the default config path (`~/.config/meerschaum/config/`). 119 120 Returns 121 ------- 122 A bool indicating success. 123 124 """ 125 if directory is None: 126 import meerschaum.config.paths as paths 127 directory = paths.CONFIG_DIR_PATH 128 129 from meerschaum.config import _allow_write_missing 130 from meerschaum._internal.static import STATIC_CONFIG 131 from meerschaum.config._default import default_header_comment 132 from meerschaum.config._read_config import get_keyfile_path, revert_symlinks_config 133 from meerschaum.utils.yaml import yaml 134 from meerschaum.utils.misc import filter_keywords 135 import json 136 import os 137 if config_dict is None: 138 from meerschaum.config import _config 139 cf = _config(allow_replaced=False) 140 config_dict = cf 141 142 if not _allow_write_missing: 143 return False 144 145 default_filetype = STATIC_CONFIG['config']['default_filetype'] 146 filetype_dumpers = { 147 'yml' : yaml.dump, 148 'yaml' : yaml.dump, 149 'json' : json.dump, 150 } 151 152 config_dict = revert_symlinks_config(config_dict) 153 154 def determine_filetype(k, v): 155 if k == 'meerschaum': 156 return 'yaml' 157 if isinstance(v, dict) and 'filetype' in v: 158 return v['filetype'] 159 path = get_keyfile_path(k, create_new=False, directory=directory) 160 if path is None: 161 return default_filetype 162 filetype = path.suffix[1:] 163 if not isinstance(filetype, str) or filetype not in filetype_dumpers: 164 print(f"Invalid filetype '{filetype}' for '{k}'. Assuming {default_filetype}...") 165 filetype = default_filetype 166 return filetype 167 168 for k, v in config_dict.items(): 169 filetype = determine_filetype(k, v) 170 filename = str(k) + '.' + str(filetype) 171 filepath = os.path.join(directory, filename) 172 pathlib.Path(filepath).parent.mkdir(exist_ok=True) 173 with open(filepath, 'w+') as f: 174 try: 175 if k == 'meerschaum': 176 f.write(default_header_comment) 177 filetype_dumpers[filetype]( 178 v, f, 179 **filter_keywords( 180 filetype_dumpers[filetype], 181 sort_keys = False, 182 indent = 2 183 ) 184 ) 185 success = True 186 except Exception as e: 187 success = False 188 print("FAILED TO WRITE!") 189 print(e) 190 print(filter_keywords( 191 filetype_dumpers[filetype], 192 sort_keys=False, 193 indent = 2 194 )) 195 196 if not success: 197 try: 198 if os.path.exists(filepath): 199 os.remove(filepath) 200 except Exception: 201 print(f"Failed to write '{k}'") 202 return False 203 204 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.config._backup import backup_config_key, restore_config_backup 27 from meerschaum._internal.static import STATIC_CONFIG 28 from meerschaum.utils.packages import reload_meerschaum 29 from meerschaum.utils.misc import edit_file 30 from meerschaum.utils.warnings import warn, info 31 from meerschaum.utils.prompt import prompt, yes_no 32 33 if keys is None: 34 keys = [] 35 36 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 37 def _edit_key(key: str): 38 new_key_config = None 39 ### Snapshot the pristine file before any edits or default-merging so it 40 ### can be rolled back if the edit corrupts it. 41 backup_path = backup_config_key(key, debug=debug) 42 while True: 43 ### If defined in default, create the config file. 44 symlinks_key_config = config.get(symlinks_key, {}).get(key, {}) 45 key_config = revert_symlinks_config({ 46 key: config.pop(key, {}), 47 symlinks_key: {key: symlinks_key_config}, 48 }) 49 keyfile_path = get_keyfile_path(key, create_new=True) 50 get_config(key, write_missing=True, warn=False) 51 52 edit_file(get_keyfile_path(key, create_new=True)) 53 54 ### TODO: verify that the file is valid. Retry if not. 55 try: 56 new_key_config = read_config( 57 paths.CONFIG_DIR_PATH, 58 [key], 59 write_missing=False, 60 raise_parsing_errors=True, 61 ) 62 except Exception: 63 if key_config: 64 config[key] = key_config 65 warn(f"Could not parse key '{key}'.", stack=False) 66 if backup_path is not None and yes_no( 67 f"Restore '{key}' from the backup taken before editing?", 68 default='n', 69 ): 70 restore_success, restore_msg = restore_config_backup( 71 key, backup_path=backup_path, reload=False, debug=debug, 72 ) 73 if restore_success: 74 info(restore_msg) 75 new_key_config = read_config( 76 paths.CONFIG_DIR_PATH, 77 [key], 78 write_missing=False, 79 raise_parsing_errors=False, 80 ) 81 break 82 warn(restore_msg, stack=False) 83 _ = prompt(f"Press [Enter] to edit '{keyfile_path}', [CTRL+C] to exit.") 84 continue 85 86 if new_key_config: 87 break 88 89 try: 90 for k in keys: 91 _edit_key(k) 92 except KeyboardInterrupt: 93 return False, "" 94 95 reload_meerschaum(debug=debug) 96 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.
335@contextlib.contextmanager 336def replace_config(config_: Union[Dict[str, Any], None]): 337 """ 338 Temporarily override the Meerschaum config dictionary. 339 340 Parameters 341 ---------- 342 config_: Dict[str, Any] 343 The new config dictionary to temporarily replace the canonical `config`. 344 """ 345 if config_ is None: 346 try: 347 yield 348 except Exception: 349 pass 350 return 351 352 global _backup_config, _allow_write_missing 353 354 _backup_config = _config() 355 _allow_write_missing = False 356 set_config(config_) 357 358 try: 359 yield 360 finally: 361 set_config(_backup_config) 362 _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.
227def search_and_substitute_config( 228 config: Dict[str, Any], 229 leading_key: str = "MRSM", 230 delimiter: str = ":", 231 begin_key: str = "{", 232 end_key: str = "}", 233 literal_key: str = '!', 234 keep_symlinks: bool = True, 235) -> Dict[str, Any]: 236 """ 237 Search the config for Meerschaum substitution syntax and substite with value of keys. 238 239 Parameters 240 ---------- 241 config: Dict[str, Any] 242 The Meerschaum configuration dictionary to search through. 243 244 leading_key: str, default 'MRSM' 245 The string with which to start the search. 246 247 begin_key: str, default '{' 248 The string to start the keys list. 249 250 end_key: str, default '}' 251 The string to end the keys list. 252 253 literal_key: str, default '!' 254 The string to force an literal interpretation of a value. 255 When the string is isolated, a literal interpreation is assumed and the surrounding 256 quotes are replaced. 257 258 E.g. Suppose a:b:c produces a dictionary {'d': 1}. 259 - 'MRSM{a:b:c}' => {'d': 1} : isolated 260 - ' MRSM{a:b:c} ' => ' "{\'d\': 1}"' : not isolated 261 - ' MRSM{!a:b:c} ' => ' {"d": 1}' : literal 262 263 keep_symlinks: bool, default True 264 If True, include the symlinks under the top-level key '_symlinks' (never written to a file). 265 Defaults to True. 266 267 Example: 268 269 ``` 270 MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host'] 271 ``` 272 273 Returns 274 ------- 275 The configuration dictionary with `MRSM{}` symlinks replaced with 276 the values from the current configuration. 277 """ 278 import re 279 import json 280 from meerschaum.config import get_config 281 needle = leading_key + begin_key 282 283 _links = [] 284 if keep_symlinks: 285 def _find_symlinks(d, _keys: Optional[List[str]] = None): 286 if _keys is None: 287 _keys = [] 288 if not isinstance(d, dict): 289 return 290 for k, v in d.items(): 291 if isinstance(v, dict): 292 _find_symlinks(v, _keys + [k]) 293 elif isinstance(v, str) and needle in v: 294 _links.append((_keys + [k], v)) 295 _find_symlinks(config) 296 297 haystack = json.dumps(config, separators=(',', ':')) 298 if needle not in haystack: 299 parsed_config = config 300 else: 301 patterns = {} 302 isolated_patterns = {} 303 literal_patterns = {} 304 memo = {} 305 306 pattern_re = re.compile( 307 re.escape(needle) + r'(?P<pattern_keys>.*?)' + re.escape(end_key) 308 ) 309 310 for match in pattern_re.finditer(haystack): 311 pattern = match.group(0) 312 pattern_keys_raw = match.group('pattern_keys') 313 pattern_keys = pattern_keys_raw.split(delimiter) 314 315 start, end = match.span() 316 prior = haystack[start - 1] if start > 0 else None 317 after = haystack[end] if end < len(haystack) else None 318 319 force_literal = False 320 keys = [k for k in pattern_keys] 321 if keys and str(keys[0]).startswith(literal_key): 322 keys[0] = str(keys[0])[len(literal_key):] 323 force_literal = True 324 if len(keys) == 1 and keys[0] == '': 325 keys = [] 326 327 cache_key = tuple(keys) 328 if cache_key in memo: 329 valid, value = memo[cache_key] 330 else: 331 try: 332 valid, value = get_config( 333 *keys, 334 substitute=False, 335 as_tuple=True, 336 write_missing=False, 337 sync_files=False, 338 ) 339 except Exception: 340 valid, value = False, None 341 memo[cache_key] = (valid, value) 342 343 if not valid: 344 continue 345 346 patterns[pattern] = value 347 isolated_patterns[pattern] = (prior == '"' and after == '"') 348 literal_patterns[pattern] = force_literal 349 350 for pattern, value in patterns.items(): 351 if isolated_patterns[pattern]: 352 haystack = haystack.replace( 353 json.dumps(pattern), 354 json.dumps(value), 355 ) 356 elif literal_patterns[pattern]: 357 haystack = haystack.replace( 358 pattern, 359 ( 360 json.dumps(value) 361 .replace("\\", "\\\\") 362 .replace('"', '\\"') 363 .replace("'", "\\'") 364 ) 365 ) 366 else: 367 haystack = haystack.replace(pattern, str(value)) 368 369 parsed_config = json.loads(haystack) or {} 370 371 symlinks = {} 372 if keep_symlinks: 373 for _keys, _pattern in _links: 374 s = symlinks 375 for k in _keys[:-1]: 376 if k not in s: 377 s[k] = {} 378 s = s[k] 379 s[_keys[-1]] = _pattern 380 381 from meerschaum.config._patch import apply_patch_to_config 382 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 383 if symlinks: 384 if symlinks_key not in parsed_config: 385 parsed_config[symlinks_key] = symlinks 386 else: 387 parsed_config[symlinks_key] = apply_patch_to_config( 388 parsed_config[symlinks_key], 389 symlinks, 390 warn=False, 391 ) 392 393 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.
396def revert_symlinks_config(config: Dict[str, Any]) -> Dict[str, Any]: 397 """ 398 Given a configuration dictionary, re-apply the values from the 399 accompanying `_symlinks` dictionary. 400 401 Parameters 402 ---------- 403 config: Dict[str, Any] 404 The configuration dictionary containing a `_symlinks` dictionary. 405 406 Returns 407 ------- 408 A configuration dictionary with `_symlinks` re-applied. 409 """ 410 import copy 411 from meerschaum._internal.static import STATIC_CONFIG 412 413 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 414 if symlinks_key not in config: 415 return config 416 417 reverted_config = copy.deepcopy(config) 418 symlinks_config = reverted_config.pop(symlinks_key) 419 420 def deep_patch(target_dict, patch_dict): 421 for key, value in patch_dict.items(): 422 if isinstance(value, dict) and key in target_dict and isinstance(target_dict[key], dict): 423 deep_patch(target_dict[key], value) 424 else: 425 target_dict[key] = value 426 427 deep_patch(reverted_config, symlinks_config) 428 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.
431def get_possible_keys() -> List[str]: 432 """ 433 Return a list of possible top-level keys. 434 """ 435 import os 436 import meerschaum.config.paths as paths 437 from meerschaum.config._default import default_config 438 keys = set() 439 for key in default_config: 440 keys.add(key) 441 for filename in os.listdir(paths.CONFIG_DIR_PATH): 442 keys.add('.'.join(filename.split('.')[:-1])) 443 return sorted(list(keys))
Return a list of possible top-level keys.
446def get_keyfile_path( 447 key: str, 448 create_new: bool = False, 449 directory: Union[pathlib.Path, str, None] = None, 450) -> Union[pathlib.Path, None]: 451 """Determine a key's file path.""" 452 import os 453 import pathlib 454 if directory is None: 455 import meerschaum.config.paths as paths 456 directory = paths.CONFIG_DIR_PATH 457 458 try: 459 return pathlib.Path( 460 os.path.join( 461 directory, 462 read_config( 463 keys=[key], 464 with_filenames=True, 465 write_missing=False, 466 substitute=False, 467 )[1][0] 468 ) 469 ) 470 except IndexError: 471 if create_new: 472 default_filetype = STATIC_CONFIG['config']['default_filetype'] 473 return pathlib.Path(os.path.join(directory, key + '.' + default_filetype)) 474 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 if raise_parsing_errors: 183 raise e 184 ### The file exists but is malformed (e.g. a bad manual edit). 185 ### Fall back to default values in memory so the app keeps 186 ### working, but leave the file on disk untouched so it is 187 ### never overwritten and can be fixed or rolled back. 188 import copy 189 from meerschaum.config._default import default_config 190 from meerschaum.utils.warnings import warn as _warn 191 _warn( 192 f"Could not parse '{filename}':\n {e}\n" 193 "Using default values in memory; the file on disk is unchanged.\n" 194 f"Fix it manually or run `edit config {key} --rollback` " 195 "to restore a backup.", 196 stack=False, 197 ) 198 _config_key = copy.deepcopy(default_config.get(key, {})) 199 _single_key_config = ( 200 search_and_substitute_config({key: _config_key}) if substitute 201 else {key: _config_key} 202 ) 203 config[key] = _single_key_config[key] 204 if ( 205 symlinks_key in _single_key_config 206 and key in _single_key_config[symlinks_key] 207 ): 208 if symlinks_key not in config: 209 config[symlinks_key] = {} 210 config[symlinks_key][key] = _single_key_config[symlinks_key][key] 211 break 212 except Exception as e: 213 if raise_parsing_errors: 214 raise e 215 print(f"Unable to parse {filename}!") 216 import traceback 217 traceback.print_exc() 218 input(f"Press [Enter] to open '{filename}' and fix formatting errors.") 219 from meerschaum.utils.misc import edit_file 220 edit_file(filepath) 221 222 if with_filenames: 223 return config, filekeys 224 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'])