meerschaum.config
Meerschaum v2.1.7
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, shutil, sys, pathlib, copy 13from meerschaum.utils.typing import Any, Dict, Optional, Union 14from meerschaum.utils.threading import RLock 15from meerschaum.utils.warnings import warn 16 17from meerschaum.config._version import __version__ 18from meerschaum.config._edit import edit_config, write_config 19from meerschaum.config.static import STATIC_CONFIG 20 21from meerschaum.config._paths import ( 22 PERMANENT_PATCH_DIR_PATH, 23 CONFIG_DIR_PATH, 24 DEFAULT_CONFIG_DIR_PATH, 25) 26from meerschaum.config._patch import ( 27 apply_patch_to_config, 28) 29__all__ = ('get_plugin_config', 'write_plugin_config', 'get_config', 'write_config', 'set_config',) 30__pdoc__ = {'static': False, 'resources': False, 'stack': False, } 31_locks = {'config': RLock()} 32 33### apply config preprocessing (e.g. main to meta) 34config = {} 35def _config( 36 *keys: str, reload: bool = False, substitute: bool = True, 37 sync_files: bool = True, write_missing: bool = True, 38 ) -> Dict[str, Any]: 39 """ 40 Read and process the configuration file. 41 """ 42 global config 43 if config is None or reload: 44 with _locks['config']: 45 config = {} 46 47 if keys and keys[0] not in config: 48 from meerschaum.config._sync import sync_files as _sync_files 49 key_config = read_config( 50 keys = [keys[0]], 51 substitute = substitute, 52 write_missing = write_missing, 53 ) 54 if keys[0] in key_config: 55 config[keys[0]] = key_config[keys[0]] 56 if sync_files: 57 _sync_files(keys=[keys[0] if keys else None]) 58 return config 59 60 61def set_config(cf: Dict[str, Any]) -> Dict[str, Any]: 62 """ 63 Set the configuration dictionary. 64 """ 65 global config 66 if not isinstance(cf, dict): 67 from meerschaum.utils.warnings import error 68 error(f"Invalid value for config: {cf}") 69 with _locks['config']: 70 config = cf 71 return config 72 73 74def get_config( 75 *keys: str, 76 patch: bool = True, 77 substitute: bool = True, 78 sync_files: bool = True, 79 write_missing: bool = True, 80 as_tuple: bool = False, 81 warn: bool = True, 82 debug: bool = False 83 ) -> Any: 84 """ 85 Return the Meerschaum configuration dictionary. 86 If positional arguments are provided, index by the keys. 87 Raises a warning if invalid keys are provided. 88 89 Parameters 90 ---------- 91 keys: str: 92 List of strings to index. 93 94 patch: bool, default True 95 If `True`, patch missing default keys into the config directory. 96 Defaults to `True`. 97 98 sync_files: bool, default True 99 If `True`, sync files if needed. 100 Defaults to `True`. 101 102 write_missing: bool, default True 103 If `True`, write default values when the main config files are missing. 104 Defaults to `True`. 105 106 substitute: bool, default True 107 If `True`, subsitute 'MRSM{}' values. 108 Defaults to `True`. 109 110 as_tuple: bool, default False 111 If `True`, return a tuple of type (success, value). 112 Defaults to `False`. 113 114 Returns 115 ------- 116 The value in the configuration directory, indexed by the provided keys. 117 118 Examples 119 -------- 120 >>> get_config('meerschaum', 'instance') 121 'sql:main' 122 >>> get_config('does', 'not', 'exist') 123 UserWarning: Invalid keys in config: ('does', 'not', 'exist') 124 """ 125 import json 126 127 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 128 if debug: 129 from meerschaum.utils.debug import dprint 130 dprint(f"Indexing keys: {keys}", color=False) 131 132 if len(keys) == 0: 133 _rc = _config(substitute=substitute, sync_files=sync_files, write_missing=write_missing) 134 if as_tuple: 135 return True, _rc 136 return _rc 137 138 ### Weird threading issues, only import if substitute is True. 139 if substitute: 140 from meerschaum.config._read_config import search_and_substitute_config 141 ### Invalidate the cache if it was read before with substitute=False 142 ### but there still exist substitutions. 143 if ( 144 config is not None and substitute and keys[0] != symlinks_key 145 and 'MRSM{' in json.dumps(config.get(keys[0])) 146 ): 147 try: 148 _subbed = search_and_substitute_config({keys[0]: config[keys[0]]}) 149 except Exception as e: 150 import traceback 151 traceback.print_exc() 152 config[keys[0]] = _subbed[keys[0]] 153 if symlinks_key in _subbed: 154 if symlinks_key not in config: 155 config[symlinks_key] = {} 156 if keys[0] not in config[symlinks_key]: 157 config[symlinks_key][keys[0]] = {} 158 config[symlinks_key][keys[0]] = apply_patch_to_config( 159 _subbed, 160 config[symlinks_key][keys[0]] 161 ) 162 163 from meerschaum.config._sync import sync_files as _sync_files 164 if config is None: 165 _config(*keys, sync_files=sync_files) 166 167 invalid_keys = False 168 if keys[0] not in config and keys[0] != symlinks_key: 169 single_key_config = read_config( 170 keys=[keys[0]], substitute=substitute, write_missing=write_missing 171 ) 172 if keys[0] not in single_key_config: 173 invalid_keys = True 174 else: 175 config[keys[0]] = single_key_config.get(keys[0], None) 176 if symlinks_key in single_key_config and keys[0] in single_key_config[symlinks_key]: 177 if symlinks_key not in config: 178 config[symlinks_key] = {} 179 config[symlinks_key][keys[0]] = single_key_config[symlinks_key][keys[0]] 180 181 if sync_files: 182 _sync_files(keys=[keys[0]]) 183 184 c = config 185 if len(keys) > 0: 186 for k in keys: 187 try: 188 c = c[k] 189 except Exception as e: 190 invalid_keys = True 191 break 192 if invalid_keys: 193 ### Check if the keys are in the default configuration. 194 from meerschaum.config._default import default_config 195 in_default = True 196 patched_default_config = ( 197 search_and_substitute_config(default_config) 198 if substitute else copy.deepcopy(default_config) 199 ) 200 _c = patched_default_config 201 for k in keys: 202 try: 203 _c = _c[k] 204 except Exception as e: 205 in_default = False 206 if in_default: 207 c = _c 208 invalid_keys = False 209 warning_msg = f"Invalid keys in config: {keys}" 210 if not in_default: 211 try: 212 if warn: 213 from meerschaum.utils.warnings import warn as _warn 214 _warn(warning_msg, stacklevel=3, color=False) 215 except Exception as e: 216 if warn: 217 print(warning_msg) 218 if as_tuple: 219 return False, None 220 return None 221 222 ### Don't write keys that we haven't yet loaded into memory. 223 not_loaded_keys = [k for k in patched_default_config if k not in config] 224 for k in not_loaded_keys: 225 patched_default_config.pop(k, None) 226 227 set_config( 228 apply_patch_to_config( 229 patched_default_config, 230 config, 231 ) 232 ) 233 if patch and keys[0] != symlinks_key: 234 if write_missing: 235 write_config(config, debug=debug) 236 237 if as_tuple: 238 return (not invalid_keys), c 239 return c 240 241 242def get_plugin_config(*keys : str, **kw : Any) -> Optional[Any]: 243 """ 244 This may only be called from within a Meerschaum plugin. 245 See `meerschaum.config.get_config` for arguments. 246 """ 247 from meerschaum.utils.warnings import warn, error 248 from meerschaum.plugins import _get_parent_plugin 249 parent_plugin_name = _get_parent_plugin(2) 250 if parent_plugin_name is None: 251 error(f"You may only call `get_plugin_config()` from within a Meerschaum plugin.") 252 return get_config(*(['plugins', parent_plugin_name] + list(keys)), **kw) 253 254 255def write_plugin_config( 256 config_dict: Dict[str, Any], 257 **kw : Any 258 ): 259 """ 260 Write a plugin's configuration dictionary. 261 """ 262 from meerschaum.utils.warnings import warn, error 263 from meerschaum.plugins import _get_parent_plugin 264 parent_plugin_name = _get_parent_plugin(2) 265 if parent_plugin_name is None: 266 error(f"You may only call `get_plugin_config()` from within a Meerschaum plugin.") 267 plugins_cf = get_config('plugins', warn=False) 268 if plugins_cf is None: 269 plugins_cf = {} 270 plugins_cf.update({parent_plugin_name: config_dict}) 271 cf = {'plugins' : plugins_cf} 272 return write_config(cf, **kw) 273 274 275### This need to be below get_config to avoid a circular import. 276from meerschaum.config._read_config import read_config 277 278### If environment variable MRSM_CONFIG or MRSM_PATCH is set, patch config before anything else. 279from meerschaum.config._environment import apply_environment_patches, apply_environment_uris 280apply_environment_uris() 281apply_environment_patches() 282 283 284from meerschaum.config._paths import PATCH_DIR_PATH, PERMANENT_PATCH_DIR_PATH 285patch_config = None 286if PATCH_DIR_PATH.exists(): 287 from meerschaum.utils.yaml import yaml, _yaml 288 if _yaml is not None: 289 patch_config = read_config(directory=PATCH_DIR_PATH) 290 291permanent_patch_config = None 292if PERMANENT_PATCH_DIR_PATH.exists(): 293 from meerschaum.utils.yaml import yaml, _yaml 294 if _yaml is not None: 295 permanent_patch_config = read_config(directory=PERMANENT_PATCH_DIR_PATH) 296### If patches exist, apply to config. 297if patch_config is not None: 298 from meerschaum.config._paths import PATCH_DIR_PATH 299 set_config(apply_patch_to_config(_config(), patch_config)) 300 if PATCH_DIR_PATH.exists(): 301 shutil.rmtree(PATCH_DIR_PATH) 302 303### if permanent_patch.yaml exists, apply patch to config, write config, and delete patch 304if permanent_patch_config is not None and PERMANENT_PATCH_DIR_PATH.exists(): 305 print( 306 "Found permanent patch configuration. " + 307 "Updating main config and deleting permanent patch..." 308 ) 309 set_config(apply_patch_to_config(_config(), permanent_patch_config)) 310 write_config(_config()) 311 permanent_patch_config = None 312 if PERMANENT_PATCH_DIR_PATH.exists(): 313 shutil.rmtree(PERMANENT_PATCH_DIR_PATH) 314 if DEFAULT_CONFIG_DIR_PATH.exists(): 315 shutil.rmtree(DEFAULT_CONFIG_DIR_PATH) 316 317 318### Make sure readline is available for the portable version. 319environment_runtime = STATIC_CONFIG['environment']['runtime'] 320if environment_runtime in os.environ: 321 if os.environ[environment_runtime] == 'portable': 322 from meerschaum.utils.packages import ensure_readline 323 from meerschaum.config._paths import PORTABLE_CHECK_READLINE_PATH 324 if not PORTABLE_CHECK_READLINE_PATH.exists(): 325 ensure_readline() 326 PORTABLE_CHECK_READLINE_PATH.touch() 327 328 329### If interactive REPL, print welcome header. 330__doc__ = f"Meerschaum v{__version__}" 331try: 332 interactive = False 333 if sys.ps1: 334 interactive = True 335except AttributeError: 336 interactive = False 337if interactive: 338 msg = __doc__ 339 print(msg, file=sys.stderr)
def
get_plugin_config(*keys: str, **kw: Any) -> Optional[Any]:
243def get_plugin_config(*keys : str, **kw : Any) -> Optional[Any]: 244 """ 245 This may only be called from within a Meerschaum plugin. 246 See `meerschaum.config.get_config` for arguments. 247 """ 248 from meerschaum.utils.warnings import warn, error 249 from meerschaum.plugins import _get_parent_plugin 250 parent_plugin_name = _get_parent_plugin(2) 251 if parent_plugin_name is None: 252 error(f"You may only call `get_plugin_config()` from within a Meerschaum plugin.") 253 return get_config(*(['plugins', parent_plugin_name] + list(keys)), **kw)
This may only be called from within a Meerschaum plugin.
See get_config
for arguments.
def
write_plugin_config(config_dict: Dict[str, Any], **kw: Any):
256def write_plugin_config( 257 config_dict: Dict[str, Any], 258 **kw : Any 259 ): 260 """ 261 Write a plugin's configuration dictionary. 262 """ 263 from meerschaum.utils.warnings import warn, error 264 from meerschaum.plugins import _get_parent_plugin 265 parent_plugin_name = _get_parent_plugin(2) 266 if parent_plugin_name is None: 267 error(f"You may only call `get_plugin_config()` from within a Meerschaum plugin.") 268 plugins_cf = get_config('plugins', warn=False) 269 if plugins_cf is None: 270 plugins_cf = {} 271 plugins_cf.update({parent_plugin_name: config_dict}) 272 cf = {'plugins' : plugins_cf} 273 return write_config(cf, **kw)
Write a plugin's configuration dictionary.
def
get_config( *keys: str, patch: bool = True, substitute: bool = True, sync_files: bool = True, write_missing: bool = True, as_tuple: bool = False, warn: bool = True, debug: bool = False) -> Any:
75def get_config( 76 *keys: str, 77 patch: bool = True, 78 substitute: bool = True, 79 sync_files: bool = True, 80 write_missing: bool = True, 81 as_tuple: bool = False, 82 warn: bool = True, 83 debug: bool = False 84 ) -> Any: 85 """ 86 Return the Meerschaum configuration dictionary. 87 If positional arguments are provided, index by the keys. 88 Raises a warning if invalid keys are provided. 89 90 Parameters 91 ---------- 92 keys: str: 93 List of strings to index. 94 95 patch: bool, default True 96 If `True`, patch missing default keys into the config directory. 97 Defaults to `True`. 98 99 sync_files: bool, default True 100 If `True`, sync files if needed. 101 Defaults to `True`. 102 103 write_missing: bool, default True 104 If `True`, write default values when the main config files are missing. 105 Defaults to `True`. 106 107 substitute: bool, default True 108 If `True`, subsitute 'MRSM{}' values. 109 Defaults to `True`. 110 111 as_tuple: bool, default False 112 If `True`, return a tuple of type (success, value). 113 Defaults to `False`. 114 115 Returns 116 ------- 117 The value in the configuration directory, indexed by the provided keys. 118 119 Examples 120 -------- 121 >>> get_config('meerschaum', 'instance') 122 'sql:main' 123 >>> get_config('does', 'not', 'exist') 124 UserWarning: Invalid keys in config: ('does', 'not', 'exist') 125 """ 126 import json 127 128 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 129 if debug: 130 from meerschaum.utils.debug import dprint 131 dprint(f"Indexing keys: {keys}", color=False) 132 133 if len(keys) == 0: 134 _rc = _config(substitute=substitute, sync_files=sync_files, write_missing=write_missing) 135 if as_tuple: 136 return True, _rc 137 return _rc 138 139 ### Weird threading issues, only import if substitute is True. 140 if substitute: 141 from meerschaum.config._read_config import search_and_substitute_config 142 ### Invalidate the cache if it was read before with substitute=False 143 ### but there still exist substitutions. 144 if ( 145 config is not None and substitute and keys[0] != symlinks_key 146 and 'MRSM{' in json.dumps(config.get(keys[0])) 147 ): 148 try: 149 _subbed = search_and_substitute_config({keys[0]: config[keys[0]]}) 150 except Exception as e: 151 import traceback 152 traceback.print_exc() 153 config[keys[0]] = _subbed[keys[0]] 154 if symlinks_key in _subbed: 155 if symlinks_key not in config: 156 config[symlinks_key] = {} 157 if keys[0] not in config[symlinks_key]: 158 config[symlinks_key][keys[0]] = {} 159 config[symlinks_key][keys[0]] = apply_patch_to_config( 160 _subbed, 161 config[symlinks_key][keys[0]] 162 ) 163 164 from meerschaum.config._sync import sync_files as _sync_files 165 if config is None: 166 _config(*keys, sync_files=sync_files) 167 168 invalid_keys = False 169 if keys[0] not in config and keys[0] != symlinks_key: 170 single_key_config = read_config( 171 keys=[keys[0]], substitute=substitute, write_missing=write_missing 172 ) 173 if keys[0] not in single_key_config: 174 invalid_keys = True 175 else: 176 config[keys[0]] = single_key_config.get(keys[0], None) 177 if symlinks_key in single_key_config and keys[0] in single_key_config[symlinks_key]: 178 if symlinks_key not in config: 179 config[symlinks_key] = {} 180 config[symlinks_key][keys[0]] = single_key_config[symlinks_key][keys[0]] 181 182 if sync_files: 183 _sync_files(keys=[keys[0]]) 184 185 c = config 186 if len(keys) > 0: 187 for k in keys: 188 try: 189 c = c[k] 190 except Exception as e: 191 invalid_keys = True 192 break 193 if invalid_keys: 194 ### Check if the keys are in the default configuration. 195 from meerschaum.config._default import default_config 196 in_default = True 197 patched_default_config = ( 198 search_and_substitute_config(default_config) 199 if substitute else copy.deepcopy(default_config) 200 ) 201 _c = patched_default_config 202 for k in keys: 203 try: 204 _c = _c[k] 205 except Exception as e: 206 in_default = False 207 if in_default: 208 c = _c 209 invalid_keys = False 210 warning_msg = f"Invalid keys in config: {keys}" 211 if not in_default: 212 try: 213 if warn: 214 from meerschaum.utils.warnings import warn as _warn 215 _warn(warning_msg, stacklevel=3, color=False) 216 except Exception as e: 217 if warn: 218 print(warning_msg) 219 if as_tuple: 220 return False, None 221 return None 222 223 ### Don't write keys that we haven't yet loaded into memory. 224 not_loaded_keys = [k for k in patched_default_config if k not in config] 225 for k in not_loaded_keys: 226 patched_default_config.pop(k, None) 227 228 set_config( 229 apply_patch_to_config( 230 patched_default_config, 231 config, 232 ) 233 ) 234 if patch and keys[0] != symlinks_key: 235 if write_missing: 236 write_config(config, debug=debug) 237 238 if as_tuple: 239 return (not invalid_keys), c 240 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')
def
write_config( config_dict: Optional[Dict[str, Any]] = None, directory: Union[str, pathlib.Path, NoneType] = None, debug: bool = False, **kw: Any) -> bool:
43def write_config( 44 config_dict: Optional[Dict[str, Any]] = None, 45 directory: Union[str, pathlib.Path, None] = None, 46 debug: bool = False, 47 **kw : Any 48 ) -> bool: 49 """Write YAML and JSON files to the configuration directory. 50 51 Parameters 52 ---------- 53 config_dict: Optional[Dict[str, Any]], default None 54 A dictionary of keys to dictionaries of configuration. 55 Each key corresponds to a .yaml or .json config file. 56 Writing config to a directory with different keys 57 does not affect existing keys in that directory. 58 If not provided, use the currently loaded config dictionary. 59 60 directory: Union[str, pathlib.Path, None], default None 61 The directory to which the keys are written. 62 If not provided, use the default config path (`~/.config/meerschaum/config/`). 63 64 Returns 65 ------- 66 A bool indicating success. 67 68 """ 69 if directory is None: 70 from meerschaum.config._paths import CONFIG_DIR_PATH 71 directory = CONFIG_DIR_PATH 72 from meerschaum.config.static import STATIC_CONFIG 73 from meerschaum.config._default import default_header_comment 74 from meerschaum.config._patch import apply_patch_to_config 75 from meerschaum.config._read_config import get_keyfile_path 76 from meerschaum.utils.debug import dprint 77 from meerschaum.utils.yaml import yaml 78 from meerschaum.utils.misc import filter_keywords 79 import json, os 80 if config_dict is None: 81 from meerschaum.config import _config 82 cf = _config() 83 config_dict = cf 84 85 default_filetype = STATIC_CONFIG['config']['default_filetype'] 86 filetype_dumpers = { 87 'yml' : yaml.dump, 88 'yaml' : yaml.dump, 89 'json' : json.dump, 90 } 91 92 symlinks_key = STATIC_CONFIG['config']['symlinks_key'] 93 symlinks = config_dict.pop(symlinks_key) if symlinks_key in config_dict else {} 94 config_dict = apply_patch_to_config(config_dict, symlinks) 95 96 def determine_filetype(k, v): 97 if k == 'meerschaum': 98 return 'yaml' 99 if isinstance(v, dict) and 'filetype' in v: 100 return v['filetype'] 101 path = get_keyfile_path(k, create_new=False, directory=directory) 102 if path is None: 103 return default_filetype 104 filetype = path.suffix[1:] 105 if not isinstance(filetype, str) or filetype not in filetype_dumpers: 106 print(f"Invalid filetype '{filetype}' for '{k}'. Assuming {default_filetype}...") 107 filetype = default_filetype 108 return filetype 109 110 for k, v in config_dict.items(): 111 filetype = determine_filetype(k, v) 112 filename = str(k) + '.' + str(filetype) 113 filepath = os.path.join(directory, filename) 114 pathlib.Path(filepath).parent.mkdir(exist_ok=True) 115 with open(filepath, 'w+') as f: 116 try: 117 if k == 'meerschaum': 118 f.write(default_header_comment) 119 filetype_dumpers[filetype]( 120 v, f, 121 **filter_keywords( 122 filetype_dumpers[filetype], 123 sort_keys = False, 124 indent = 2 125 ) 126 ) 127 success = True 128 except Exception as e: 129 success = False 130 print(f"FAILED TO WRITE!") 131 print(e) 132 print(filter_keywords( 133 filetype_dumpers[filetype], 134 sort_keys=False, 135 indent = 2 136 )) 137 138 if not success: 139 try: 140 if os.path.exists(filepath): 141 os.remove(filepath) 142 except Exception as e: 143 print(f"Failed to write '{k}'") 144 return False 145 146 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.
def
set_config(cf: Dict[str, Any]) -> Dict[str, Any]:
62def set_config(cf: Dict[str, Any]) -> Dict[str, Any]: 63 """ 64 Set the configuration dictionary. 65 """ 66 global config 67 if not isinstance(cf, dict): 68 from meerschaum.utils.warnings import error 69 error(f"Invalid value for config: {cf}") 70 with _locks['config']: 71 config = cf 72 return config
Set the configuration dictionary.