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