meerschaum.config

Meerschaum v3.3.1

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

This may only be called from within a Meerschaum plugin. See meerschaum.config.get_config for arguments.

def write_plugin_config(config_dict: Dict[str, Any], **kw: Any):
309def write_plugin_config(
310    config_dict: Dict[str, Any],
311    **kw: Any
312):
313    """
314    Write a plugin's configuration dictionary.
315    """
316    from meerschaum.utils.warnings import error
317    from meerschaum.plugins import _get_parent_plugin
318    parent_plugin_name = _get_parent_plugin(2)
319    if parent_plugin_name is None:
320        error("You may only call `get_plugin_config()` from within a Meerschaum plugin.")
321    plugins_cf = get_config('plugins', warn=False)
322    if plugins_cf is None:
323        plugins_cf = {}
324    plugins_cf.update({parent_plugin_name: config_dict})
325    cf = {'plugins' : plugins_cf}
326    return write_config(cf, **kw)

Write a plugin's configuration dictionary.

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:
112def get_config(
113    *keys: str,
114    patch: bool = True,
115    substitute: bool = True,
116    sync_files: bool = True,
117    write_missing: bool = True,
118    as_tuple: bool = False,
119    warn: bool = True,
120    debug: bool = False
121) -> Any:
122    """
123    Return the Meerschaum configuration dictionary.
124    If positional arguments are provided, index by the keys.
125    Raises a warning if invalid keys are provided.
126
127    Parameters
128    ----------
129    keys: str:
130        List of strings to index.
131
132    patch: bool, default True
133        If `True`, patch missing default keys into the config directory.
134        Defaults to `True`.
135
136    sync_files: bool, default True
137        If `True`, sync files if needed.
138        Defaults to `True`.
139
140    write_missing: bool, default True
141        If `True`, write default values when the main config files are missing.
142        Defaults to `True`.
143
144    substitute: bool, default True
145        If `True`, subsitute 'MRSM{}' values.
146        Defaults to `True`.
147
148    as_tuple: bool, default False
149        If `True`, return a tuple of type (success, value).
150        Defaults to `False`.
151        
152    Returns
153    -------
154    The value in the configuration directory, indexed by the provided keys.
155
156    Examples
157    --------
158    >>> get_config('meerschaum', 'instance')
159    'sql:main'
160    >>> get_config('does', 'not', 'exist')
161    UserWarning: Invalid keys in config: ('does', 'not', 'exist')
162    """
163    import json
164
165    symlinks_key = STATIC_CONFIG['config']['symlinks_key']
166    if debug:
167        from meerschaum.utils.debug import dprint
168        dprint(f"Indexing keys: {keys}", color=False)
169
170    if len(keys) == 0:
171        _rc = _config(
172            substitute=substitute,
173            sync_files=sync_files,
174            write_missing=(write_missing and _allow_write_missing),
175        )
176        if as_tuple:
177            return True, _rc 
178        return _rc
179    
180    ### Weird threading issues, only import if substitute is True.
181    if substitute:
182        from meerschaum.config._read_config import search_and_substitute_config
183    ### Invalidate the cache if it was read before with substitute=False
184    ### but there still exist substitutions.
185    if (
186        config is not None and substitute and keys[0] != symlinks_key
187        and 'MRSM{' in json.dumps(config.get(keys[0]))
188    ):
189        try:
190            _subbed = search_and_substitute_config({keys[0]: config[keys[0]]})
191        except Exception:
192            import traceback
193            traceback.print_exc()
194            _subbed = {keys[0]: config[keys[0]]}
195
196        config[keys[0]] = _subbed[keys[0]]
197        if symlinks_key in _subbed:
198            if symlinks_key not in config:
199                config[symlinks_key] = {}
200            config[symlinks_key] = apply_patch_to_config(
201                _subbed.get(symlinks_key, {}),
202                config.get(symlinks_key, {}),
203            )
204
205    from meerschaum.config._sync import sync_files as _sync_files
206    if config is None:
207        _config(*keys, sync_files=sync_files)
208
209    invalid_keys = False
210    if keys[0] not in config and keys[0] != symlinks_key:
211        single_key_config = read_config(
212            keys=[keys[0]], substitute=substitute, write_missing=write_missing
213        )
214        if keys[0] not in single_key_config:
215            invalid_keys = True
216        else:
217            config[keys[0]] = single_key_config.get(keys[0], None)
218            if symlinks_key in single_key_config and keys[0] in single_key_config[symlinks_key]:
219                if symlinks_key not in config:
220                    config[symlinks_key] = {}
221                config[symlinks_key][keys[0]] = single_key_config[symlinks_key][keys[0]]
222
223            if sync_files:
224                _sync_files(keys=[keys[0]])
225
226    c = config
227    if len(keys) > 0:
228        for k in keys:
229            try:
230                c = c[k]
231            except Exception:
232                invalid_keys = True
233                break
234        if invalid_keys:
235            ### Check if the keys are in the default configuration.
236            from meerschaum.config._default import default_config
237            in_default = True
238            patched_default_config = (
239                search_and_substitute_config(default_config)
240                if substitute else copy.deepcopy(default_config)
241            )
242            _c = patched_default_config
243            for k in keys:
244                try:
245                    _c = _c[k]
246                except Exception:
247                    in_default = False
248            if in_default:
249                c = _c
250                invalid_keys = False
251            warning_msg = f"Invalid keys in config: {keys}"
252            if not in_default:
253                try:
254                    if warn:
255                        from meerschaum.utils.warnings import warn as _warn
256                        _warn(warning_msg, stacklevel=3, color=False)
257                except Exception:
258                    if warn:
259                        print(warning_msg)
260                if as_tuple:
261                    return False, None
262                return None
263
264            ### Don't write keys that we haven't yet loaded into memory.
265            not_loaded_keys = [k for k in patched_default_config if k not in config]
266            for k in not_loaded_keys:
267                patched_default_config.pop(k, None)
268
269            set_config(
270                apply_patch_to_config(
271                    patched_default_config,
272                    config,
273                )
274            )
275            if patch and keys[0] != symlinks_key:
276                if write_missing:
277                    write_config(config, debug=debug)
278
279    if as_tuple:
280        return (not invalid_keys), c
281    return c

Return the Meerschaum configuration dictionary. If positional arguments are provided, index by the keys. Raises a warning if invalid keys are provided.

Parameters
  • keys (str:): List of strings to index.
  • patch (bool, default True): If True, patch missing default keys into the config directory. Defaults to True.
  • sync_files (bool, default True): If True, sync files if needed. Defaults to True.
  • write_missing (bool, default True): If True, write default values when the main config files are missing. Defaults to True.
  • substitute (bool, default True): If True, subsitute 'MRSM{}' values. Defaults to True.
  • as_tuple (bool, default False): If True, return a tuple of type (success, value). Defaults to False.
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:
 78def write_config(
 79    config_dict: Optional[Dict[str, Any]] = None,
 80    directory: Union[str, pathlib.Path, None] = None,
 81    debug: bool = False,
 82    **kw: Any
 83) -> bool:
 84    """Write YAML and JSON files to the configuration directory.
 85
 86    Parameters
 87    ----------
 88    config_dict: Optional[Dict[str, Any]], default None
 89        A dictionary of keys to dictionaries of configuration.
 90        Each key corresponds to a .yaml or .json config file.
 91        Writing config to a directory with different keys
 92        does not affect existing keys in that directory.
 93        If not provided, use the currently loaded config dictionary.
 94
 95    directory: Union[str, pathlib.Path, None], default None
 96        The directory to which the keys are written.
 97        If not provided, use the default config path (`~/.config/meerschaum/config/`).
 98
 99    Returns
100    -------
101    A bool indicating success.
102
103    """
104    if directory is None:
105        import meerschaum.config.paths as paths
106        directory = paths.CONFIG_DIR_PATH
107
108    from meerschaum.config import _allow_write_missing
109    from meerschaum._internal.static import STATIC_CONFIG
110    from meerschaum.config._default import default_header_comment
111    from meerschaum.config._read_config import get_keyfile_path, revert_symlinks_config
112    from meerschaum.utils.yaml import yaml
113    from meerschaum.utils.misc import filter_keywords
114    import json
115    import os
116    if config_dict is None:
117        from meerschaum.config import _config
118        cf = _config(allow_replaced=False)
119        config_dict = cf
120
121    if not _allow_write_missing:
122        return False
123
124    default_filetype = STATIC_CONFIG['config']['default_filetype']
125    filetype_dumpers = {
126        'yml' : yaml.dump,
127        'yaml' : yaml.dump,
128        'json' : json.dump,
129    }
130
131    config_dict = revert_symlinks_config(config_dict)
132
133    def determine_filetype(k, v):
134        if k == 'meerschaum':
135            return 'yaml'
136        if isinstance(v, dict) and 'filetype' in v:
137            return v['filetype']
138        path = get_keyfile_path(k, create_new=False, directory=directory)
139        if path is None:
140            return default_filetype
141        filetype = path.suffix[1:]
142        if not isinstance(filetype, str) or filetype not in filetype_dumpers:
143            print(f"Invalid filetype '{filetype}' for '{k}'. Assuming {default_filetype}...")
144            filetype = default_filetype
145        return filetype
146
147    for k, v in config_dict.items():
148        filetype = determine_filetype(k, v)        
149        filename = str(k) + '.' + str(filetype)
150        filepath = os.path.join(directory, filename)
151        pathlib.Path(filepath).parent.mkdir(exist_ok=True)
152        with open(filepath, 'w+') as f:
153            try:
154                if k == 'meerschaum':
155                    f.write(default_header_comment)
156                filetype_dumpers[filetype](
157                    v, f,
158                    **filter_keywords(
159                        filetype_dumpers[filetype],
160                        sort_keys = False,
161                        indent = 2
162                    )
163                )
164                success = True
165            except Exception as e:
166                success = False
167                print("FAILED TO WRITE!")
168                print(e)
169                print(filter_keywords(
170                    filetype_dumpers[filetype],
171                    sort_keys=False,
172                    indent = 2
173                ))
174
175            if not success:
176                try:
177                    if os.path.exists(filepath):
178                        os.remove(filepath)
179                except Exception:
180                    print(f"Failed to write '{k}'")
181                return False
182
183    return True

Write YAML and JSON files to the configuration directory.

Parameters
  • config_dict (Optional[Dict[str, Any]], default None): A dictionary of keys to dictionaries of configuration. Each key corresponds to a .yaml or .json config file. Writing config to a directory with different keys does not affect existing keys in that directory. If not provided, use the currently loaded config dictionary.
  • directory (Union[str, pathlib.Path, None], default None): The directory to which the keys are written. If not provided, use the default config path (~/.config/meerschaum/config/).
Returns
  • A bool indicating success.
def edit_config( keys: Optional[List[str]] = None, params: Optional[Dict[str, Any]] = None, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
16def edit_config(
17    keys: Optional[List[str]] = None,
18    params: Optional[Dict[str, Any]] = None,
19    debug: bool = False,
20    **kw: Any
21) -> SuccessTuple:
22    """Edit the configuration files."""
23    import meerschaum.config.paths as paths
24    from meerschaum.config import get_config, config
25    from meerschaum.config._read_config import get_keyfile_path, read_config, revert_symlinks_config
26    from meerschaum._internal.static import STATIC_CONFIG
27    from meerschaum.utils.packages import reload_meerschaum
28    from meerschaum.utils.misc import edit_file
29    from meerschaum.utils.warnings import warn
30    from meerschaum.utils.prompt import prompt
31
32    if keys is None:
33        keys = []
34
35    symlinks_key = STATIC_CONFIG['config']['symlinks_key']
36    def _edit_key(key: str):
37        new_key_config = None
38        while True:
39            ### If defined in default, create the config file.
40            symlinks_key_config = config.get(symlinks_key, {}).get(key, {})
41            key_config = revert_symlinks_config({
42                key: config.pop(key, {}),
43                symlinks_key: {key: symlinks_key_config},
44            })
45            keyfile_path = get_keyfile_path(key, create_new=True)
46            get_config(key, write_missing=True, warn=False)
47
48            edit_file(get_keyfile_path(key, create_new=True))
49
50            ### TODO: verify that the file is valid. Retry if not.
51            try:
52                new_key_config = read_config(
53                    paths.CONFIG_DIR_PATH,
54                    [key],
55                    write_missing=False,
56                    raise_parsing_errors=True,
57                )
58            except Exception:
59                if key_config:
60                    config[key] = key_config
61                warn(f"Could not parse key '{key}'.", stack=False)
62                _ = prompt(f"Press [Enter] to edit '{keyfile_path}', [CTRL+C] to exit.")
63                continue
64
65            if new_key_config:
66                break
67
68    try:
69        for k in keys:
70            _edit_key(k)
71    except KeyboardInterrupt:
72        return False, ""
73
74    reload_meerschaum(debug=debug)
75    return (True, "Success")

Edit the configuration files.

def set_config(cf: Dict[str, Any]) -> Dict[str, Any]:
 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.

@contextlib.contextmanager
def replace_config(config_: Optional[Dict[str, Any]]):
329@contextlib.contextmanager
330def replace_config(config_: Union[Dict[str, Any], None]):
331    """
332    Temporarily override the Meerschaum config dictionary.
333
334    Parameters
335    ----------
336    config_: Dict[str, Any]
337        The new config dictionary to temporarily replace the canonical `config`.
338    """
339    if config_ is None:
340        try:
341            yield
342        except Exception:
343            pass
344        return
345
346    global _backup_config, _allow_write_missing
347
348    _backup_config = _config()
349    _allow_write_missing = False
350    set_config(config_)
351
352    try:
353        yield
354    finally:
355        set_config(_backup_config)
356        _allow_write_missing = True

Temporarily override the Meerschaum config dictionary.

Parameters
  • config_ (Dict[str, Any]): The new config dictionary to temporarily replace the canonical config.
def search_and_substitute_config( config: Dict[str, Any], leading_key: str = 'MRSM', delimiter: str = ':', begin_key: str = '{', end_key: str = '}', literal_key: str = '!', keep_symlinks: bool = True) -> Dict[str, Any]:
216def search_and_substitute_config(
217    config: Dict[str, Any],
218    leading_key: str = "MRSM",
219    delimiter: str = ":",
220    begin_key: str = "{",
221    end_key: str = "}",
222    literal_key: str = '!',
223    keep_symlinks: bool = True,
224) -> Dict[str, Any]:
225    """
226    Search the config for Meerschaum substitution syntax and substite with value of keys.
227
228    Parameters
229    ----------
230    config: Dict[str, Any]
231        The Meerschaum configuration dictionary to search through.
232
233    leading_key: str, default 'MRSM'
234        The string with which to start the search.
235
236    begin_key: str, default '{'
237        The string to start the keys list.
238
239    end_key: str, default '}'
240        The string to end the keys list.
241
242    literal_key: str, default '!'
243        The string to force an literal interpretation of a value.
244        When the string is isolated, a literal interpreation is assumed and the surrounding
245        quotes are replaced.
246        
247        E.g. Suppose a:b:c produces a dictionary {'d': 1}.
248        - 'MRSM{a:b:c}'    => {'d': 1}        : isolated
249        - ' MRSM{a:b:c} '  => ' "{\'d\': 1}"' : not isolated
250        - ' MRSM{!a:b:c} ' => ' {"d": 1}'     : literal
251
252    keep_symlinks: bool, default True
253        If True, include the symlinks under the top-level key '_symlinks' (never written to a file).
254        Defaults to True.
255        
256        Example:
257
258        ```
259        MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host']
260        ``` 
261
262    Returns
263    -------
264    The configuration dictionary with `MRSM{}` symlinks replaced with
265    the values from the current configuration.
266    """
267    import re
268    import json
269    from meerschaum.config import get_config
270    needle = leading_key + begin_key
271
272    _links = []
273    if keep_symlinks:
274        def _find_symlinks(d, _keys: Optional[List[str]] = None):
275            if _keys is None:
276                _keys = []
277            if not isinstance(d, dict):
278                return
279            for k, v in d.items():
280                if isinstance(v, dict):
281                    _find_symlinks(v, _keys + [k])
282                elif isinstance(v, str) and needle in v:
283                    _links.append((_keys + [k], v))
284        _find_symlinks(config)
285
286    haystack = json.dumps(config, separators=(',', ':'))
287    if needle not in haystack:
288        parsed_config = config
289    else:
290        patterns = {}
291        isolated_patterns = {}
292        literal_patterns = {}
293        memo = {}
294
295        pattern_re = re.compile(
296            re.escape(needle) + r'(?P<pattern_keys>.*?)' + re.escape(end_key)
297        )
298
299        for match in pattern_re.finditer(haystack):
300            pattern = match.group(0)
301            pattern_keys_raw = match.group('pattern_keys')
302            pattern_keys = pattern_keys_raw.split(delimiter)
303
304            start, end = match.span()
305            prior = haystack[start - 1] if start > 0 else None
306            after = haystack[end] if end < len(haystack) else None
307
308            force_literal = False
309            keys = [k for k in pattern_keys]
310            if keys and str(keys[0]).startswith(literal_key):
311                keys[0] = str(keys[0])[len(literal_key):]
312                force_literal = True
313            if len(keys) == 1 and keys[0] == '':
314                keys = []
315
316            cache_key = tuple(keys)
317            if cache_key in memo:
318                valid, value = memo[cache_key]
319            else:
320                try:
321                    valid, value = get_config(
322                        *keys,
323                        substitute=False,
324                        as_tuple=True,
325                        write_missing=False,
326                        sync_files=False,
327                    )
328                except Exception:
329                    valid, value = False, None
330                memo[cache_key] = (valid, value)
331
332            if not valid:
333                continue
334
335            patterns[pattern] = value
336            isolated_patterns[pattern] = (prior == '"' and after == '"')
337            literal_patterns[pattern] = force_literal
338
339        for pattern, value in patterns.items():
340            if isolated_patterns[pattern]:
341                haystack = haystack.replace(
342                    json.dumps(pattern),
343                    json.dumps(value),
344                )
345            elif literal_patterns[pattern]:
346                haystack = haystack.replace(
347                    pattern,
348                    (
349                        json.dumps(value)
350                        .replace("\\", "\\\\")
351                        .replace('"', '\\"')
352                        .replace("'", "\\'")
353                    )
354                )
355            else:
356                haystack = haystack.replace(pattern, str(value))
357
358        parsed_config = json.loads(haystack) or {}
359
360    symlinks = {}
361    if keep_symlinks:
362        for _keys, _pattern in _links:
363            s = symlinks
364            for k in _keys[:-1]:
365                if k not in s:
366                    s[k] = {}
367                s = s[k]
368            s[_keys[-1]] = _pattern
369
370        from meerschaum.config._patch import apply_patch_to_config
371        symlinks_key = STATIC_CONFIG['config']['symlinks_key']
372        if symlinks:
373            if symlinks_key not in parsed_config:
374                parsed_config[symlinks_key] = symlinks
375            else:
376                parsed_config[symlinks_key] = apply_patch_to_config(
377                    parsed_config[symlinks_key],
378                    symlinks,
379                    warn=False,
380                )
381
382    return parsed_config

Search the config for Meerschaum substitution syntax and substite with value of keys.

Parameters
  • config (Dict[str, Any]): The Meerschaum configuration dictionary to search through.
  • leading_key (str, default 'MRSM'): The string with which to start the search.
  • begin_key (str, default '{'): The string to start the keys list.
  • end_key (str, default '}'): The string to end the keys list.
  • literal_key (str, default '!'): The string to force an literal interpretation of a value. When the string is isolated, a literal interpreation is assumed and the surrounding quotes are replaced.

    E.g. Suppose a:b:c produces a dictionary {'d': 1}.

    • 'MRSM{a:b:c}' => {'d': 1} : isolated
    • ' MRSM{a:b:c} ' => ' "{'d': 1}"' : not isolated
    • ' MRSM{!a:b:c} ' => ' {"d": 1}' : literal
  • keep_symlinks (bool, default True): If True, include the symlinks under the top-level key '_symlinks' (never written to a file). Defaults to True.

    Example:

    MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host']
    
Returns
  • The configuration dictionary with MRSM{} symlinks replaced with
  • the values from the current configuration.
def get_possible_keys() -> List[str]:
420def get_possible_keys() -> List[str]:
421    """
422    Return a list of possible top-level keys.
423    """
424    import os
425    import meerschaum.config.paths as paths
426    from meerschaum.config._default import default_config
427    keys = set()
428    for key in default_config:
429        keys.add(key)
430    for filename in os.listdir(paths.CONFIG_DIR_PATH):
431        keys.add('.'.join(filename.split('.')[:-1]))
432    return sorted(list(keys))

Return a list of possible top-level keys.

def get_keyfile_path( key: str, create_new: bool = False, directory: Union[pathlib.Path, str, NoneType] = None) -> Optional[pathlib.Path]:
435def get_keyfile_path(
436    key: str,
437    create_new: bool = False,
438    directory: Union[pathlib.Path, str, None] = None,
439) -> Union[pathlib.Path, None]:
440    """Determine a key's file path."""
441    import os
442    import pathlib
443    if directory is None:
444        import meerschaum.config.paths as paths
445        directory = paths.CONFIG_DIR_PATH
446
447    try:
448        return pathlib.Path(
449            os.path.join(
450                directory,
451                read_config(
452                    keys=[key],
453                    with_filenames=True,
454                    write_missing=False,
455                    substitute=False,
456                )[1][0]
457            )
458        )
459    except IndexError:
460        if create_new:
461            default_filetype = STATIC_CONFIG['config']['default_filetype']
462            return pathlib.Path(os.path.join(directory, key + '.' + default_filetype))
463        return None

Determine a key's file path.

def apply_patch_to_config( config: Dict[str, Any], patch: Dict[str, Any], warn: bool = False) -> Dict[str, Any]:
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).

def read_config( directory: Union[pathlib.Path, str, NoneType] = None, keys: Optional[List[str]] = None, write_missing: bool = True, substitute: bool = True, with_filenames: bool = False, raise_parsing_errors: bool = False) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[str]]]:
 16def read_config(
 17    directory: Union[pathlib.Path, str, None] = None,
 18    keys: Optional[List[str]] = None,
 19    write_missing: bool = True,
 20    substitute: bool = True,
 21    with_filenames: bool = False,
 22    raise_parsing_errors: bool = False,
 23) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[str]]]:
 24    """
 25    Read the configuration directory.
 26
 27    Parameters
 28    ----------
 29    directory: Union[pathlib.Path, str, None], default None
 30        The directory with configuration files (.json and .yaml).
 31
 32    keys: Optional[List[str]], default None
 33        Which configuration files to read.
 34
 35    write_missing: bool, default True
 36        If a keyfile does not exist but is defined in the default configuration,
 37        write the file to disk.
 38
 39    substitute: bool, default True
 40        Replace `MRSM{}` syntax with configuration values.
 41
 42    with_filename: bool, default False
 43        If `True`, return a tuple of the configuration dictionary with a list of read filenames.
 44
 45    raise_parsing_errors: bool, default False
 46        If `True`, re-raise parsing exceptions.
 47
 48    Examples
 49    --------
 50    >>> read_config(keys=['meerschaum'], with_filename=True)
 51    >>> ({...}, ['meerschaum.yaml'])
 52    """
 53    import os
 54    import json
 55    import itertools
 56    import meerschaum.config.paths as paths
 57    from meerschaum.utils.yaml import yaml, _yaml
 58    from meerschaum.config._patch import apply_patch_to_config
 59    if directory is None:
 60        directory = paths.CONFIG_DIR_PATH
 61
 62    if _yaml is None:
 63        print('Could not import YAML! Reverting to default configuration.')
 64        from meerschaum.config._default import default_config
 65        return default_config
 66
 67    ### Each key corresponds to a YAML or JSON file.
 68    symlinks_key = STATIC_CONFIG['config']['symlinks_key']
 69    config = {}
 70    config_to_write = {}
 71
 72    default_filetype = STATIC_CONFIG['config']['default_filetype']
 73    filetype_loaders = {
 74        'yml': yaml.load,
 75        'yaml': yaml.load,
 76        'json': json.load,
 77    }
 78
 79    ### Construct filekeys (files to parse).
 80    filekeys = []
 81    filenames = os.listdir(directory)
 82    missing_keys, found_keys = set(), set()
 83    if keys is None:
 84        _filekeys = filenames
 85    else:
 86        _filekeys = []
 87        for k in keys:
 88            for ft in filetype_loaders:
 89                if str(k) + '.' + str(ft) in filenames:
 90                    _filekeys.append(str(k) + '.' + str(ft))
 91                    found_keys.add(k)
 92                    if k in missing_keys:
 93                        missing_keys.remove(k)
 94                elif k not in found_keys:
 95                    missing_keys.add(k)
 96
 97    ### Check for missing files with default keys.
 98    if len(missing_keys) > 0:
 99        from meerschaum.config._default import default_config
100        for mk in missing_keys:
101            if mk not in default_config:
102                continue
103            _default_dict = (
104                search_and_substitute_config(default_config) if substitute
105                else default_config
106            )
107            ### If default config contains symlinks, add them to the config to write.
108            try:
109                _default_symlinks = _default_dict[symlinks_key][mk]
110            except KeyError:
111                _default_symlinks = {}
112
113            config[mk] = _default_dict[mk]
114            if _default_symlinks:
115                if symlinks_key not in config:
116                    config[symlinks_key] = {}
117                if symlinks_key not in config_to_write:
118                    config_to_write[symlinks_key] = {}
119
120                if mk not in config[symlinks_key]:
121                    config[symlinks_key][mk] = {}
122                if mk not in config_to_write[symlinks_key]:
123                    config_to_write[symlinks_key][mk] = {}
124
125                config[symlinks_key][mk] = apply_patch_to_config(
126                    config[symlinks_key][mk], 
127                    _default_symlinks
128                )
129                config_to_write[symlinks_key][mk] = config[symlinks_key][mk]
130
131            ### Write the default key.
132            config_to_write[mk] = config[mk]
133
134    ### Write missing keys if necessary.
135    if len(config_to_write) > 0 and write_missing:
136        from meerschaum.config._edit import write_config
137        write_config(config_to_write, directory)
138
139    ### Check for duplicate files.
140    ### Found help on StackOverflow:
141    ### https://stackoverflow.com/questions/26618688/python-iterate-over-a-list-
142    ### of-files-finding-same-filenames-but-different-exten
143    keygroups = {
144        key: list(value)
145        for key, value in itertools.groupby(
146            sorted(_filekeys, key = lambda e: os.path.splitext(e)[0]),
147            key = lambda e: os.path.splitext(e)[0]
148        )
149    }
150    for k, v in keygroups.items():
151        fn = v[0]
152        if len(v) > 1:
153            if k + '.' + default_filetype in v:
154                fn = k + '.' + default_filetype
155            print(
156                f"Found multiple config files named '{k}'. " +
157                f"Will attempt to parse '{fn}' for key '{k}'."
158            )
159        filekeys.append(fn)
160
161    _seen_keys = []
162    for filename in filekeys:
163        filepath = os.path.join(directory, filename)
164        _parts = filename.split('.')
165        _type = _parts[-1]
166        key = '.'.join(_parts[:-1])
167        ### Check if we've seen this key before (e.g. test.yaml, test.yml, test.json).
168        if key in _seen_keys:
169            print(
170                f"Multiple files with the name '{key}' found in '{str(directory)}'. " +
171                f"Reading from '{filename}'."
172            )
173        if len(_parts) < 2 or _type not in filetype_loaders:
174            print(f"Unknown file '{filename}' in '{str(directory)}'. Skipping...")
175
176        while True:
177            try:
178                with open(filepath, 'r', encoding='utf-8') as f:
179                    try:
180                        _config_key = filetype_loaders[_type](f)
181                    except Exception as e:
182                        print(f"Error processing file: {filepath}")
183                        if raise_parsing_errors:
184                            raise e
185                        import traceback
186                        traceback.print_exc()
187                        _config_key = {}
188                _single_key_config = (
189                    search_and_substitute_config({key: _config_key}) if substitute
190                    else {key: _config_key}
191                )
192                config[key] = _single_key_config[key]
193                if (
194                    symlinks_key in _single_key_config
195                    and key in _single_key_config[symlinks_key]
196                ):
197                    if symlinks_key not in config:
198                        config[symlinks_key] = {}
199                    config[symlinks_key][key] = _single_key_config[symlinks_key][key]
200                break
201            except Exception as e:
202                if raise_parsing_errors:
203                    raise e
204                print(f"Unable to parse {filename}!")
205                import traceback
206                traceback.print_exc()
207                input(f"Press [Enter] to open '{filename}' and fix formatting errors.")
208                from meerschaum.utils.misc import edit_file
209                edit_file(filepath)
210
211    if with_filenames:
212        return config, filekeys
213    return config

Read the configuration directory.

Parameters
  • directory (Union[pathlib.Path, str, None], default None): The directory with configuration files (.json and .yaml).
  • keys (Optional[List[str]], default None): Which configuration files to read.
  • write_missing (bool, default True): If a keyfile does not exist but is defined in the default configuration, write the file to disk.
  • substitute (bool, default True): Replace MRSM{} syntax with configuration values.
  • with_filename (bool, default False): If True, return a tuple of the configuration dictionary with a list of read filenames.
  • raise_parsing_errors (bool, default False): If True, re-raise parsing exceptions.
Examples
>>> read_config(keys=['meerschaum'], with_filename=True)
>>> ({...}, ['meerschaum.yaml'])
STATIC_CONFIG = {'api': {'endpoints': {'index': '/', 'favicon': '/favicon.ico', 'plugins': '/plugins', 'pipes': '/pipes', 'metadata': '/metadata', 'actions': '/actions', 'jobs': '/jobs', 'logs': '/logs', 'users': '/users', 'tokens': '/tokens', 'login': '/login', 'connectors': '/connectors', 'version': '/version', 'chaining': '/chaining', 'websocket': '/ws', 'dash': '/dash', 'webterm': '/webterm/{session_id}', 'webterm_websocket': '/websocket/{session_id}', 'info': '/info', 'healthcheck': '/healthcheck', 'docs': '/docs', 'redoc': '/redoc', 'openapi': '/openapi.json'}, 'oauth': {'token_expires_minutes': 720}, 'webterm_job_name': '_webterm', 'default_timeout': 600, 'jobs': {'stdin_message': 'MRSM_STDIN', 'stop_message': 'MRSM_STOP', 'metadata_cache_seconds': 5, 'temp_prefix': '.api-temp-'}}, 'sql': {'internal_schema': '_mrsm_internal', 'instance_schema': 'mrsm', 'default_create_engine_args': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'create_engine_flavors': {'timescaledb': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'timescaledb-ha': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'postgresql': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'postgis': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'citus': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'mssql': {'engine': 'mssql+pyodbc', 'create_engine': {'fast_executemany': True, 'use_insertmanyvalues': False, 'isolation_level': 'AUTOCOMMIT', 'use_setinputsizes': False, 'pool_pre_ping': True, 'ignore_no_transaction_on_rollback': True}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 1433, 'options': 'driver=ODBC Driver 18 for SQL Server&UseFMTONLY=Yes&TrustServerCertificate=yes&Encrypt=no&MARS_Connection=yes'}}, 'mysql': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 3306}}, 'mariadb': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 3306}}, 'oracle': {'engine': 'oracle+oracledb', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 1521}}, 'sqlite': {'engine': 'sqlite', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database'}, 'defaults': {}}, 'geopackage': {'engine': 'sqlite', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database'}, 'defaults': {}}, 'duckdb': {'engine': 'duckdb', 'create_engine': {}, 'omit_create_engine': {'ALL'}, 'to_sql': {'method': 'multi'}, 'requirements': '', 'defaults': {}}, 'cockroachdb': {'engine': 'cockroachdb', 'omit_create_engine': {'method'}, 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'to_sql': {'method': 'multi'}, 'requirements': {'host'}, 'defaults': {'port': 26257, 'database': 'defaultdb', 'username': 'root', 'password': 'admin'}}}}, 'valkey': {'colon': '-_'}, 'environment': {'config': 'MRSM_CONFIG', 'config_dir': 'MRSM_CONFIG_DIR', 'patch': 'MRSM_PATCH', 'root': 'MRSM_ROOT_DIR', 'plugins': 'MRSM_PLUGINS_DIR', 'venvs': 'MRSM_VENVS_DIR', 'runtime': 'MRSM_RUNTIME', 'work_dir': 'MRSM_WORK_DIR', 'user': 'MRSM_USER', 'dep_group': 'MRSM_DEP_GROUP', 'home': 'MRSM_HOME', 'src': 'MRSM_SRC', 'uid': 'MRSM_UID', 'gid': 'MRSM_GID', 'noask': 'MRSM_NOASK', 'noninteractive': 'MRSM_NONINTERACTIVE', 'id': 'MRSM_SERVER_ID', 'test_flavors': 'MRSM_TEST_FLAVORS', 'daemon_id': 'MRSM_DAEMON_ID', 'systemd_log_path': 'MRSM_SYSTEMD_LOG_PATH', 'systemd_stdin_path': 'MRSM_SYSTEMD_STDIN_PATH', 'systemd_result_path': 'MRSM_SYSTEMD_RESULT_PATH', 'systemd_delete_job': 'MRSM_SYSTEMD_DELETE_JOB', 'uri_regex': 'MRSM_([a-zA-Z0-9]*)_(\\d*[a-zA-Z][a-zA-Z0-9-_+]*$)', 'prefix': 'MRSM_'}, 'config': {'default_filetype': 'json', 'symlinks_key': '_symlinks'}, 'system': {'arguments': {'sub_decorators': ('[', ']'), 'underscore_standin': '<UNDERSCORE>', 'failure_key': '_argparse_exception', 'and_key': '+', 'escaped_and_key': '++', 'pipeline_key': ':', 'escaped_pipeline_key': '::'}, 'urls': {'get-pip.py': 'https://bootstrap.pypa.io/get-pip.py'}, 'success': {'ignore': ('Success', 'successSucceeded', '', None)}, 'prompt': {'web': False}, 'fetch_pipes_keys': {'negation_prefix': '_'}}, 'connectors': {'default_label': 'main'}, 'dtypes': {'datetime': {'default_precision_unit': 'microsecond'}}, 'stack': {'dollar_standin': '<DOLLAR>'}, 'users': {'password_hash': {'algorithm_name': 'sha256', 'salt_bytes': 16, 'schemes': ['pbkdf2_sha256'], 'default': 'pbkdf2_sha256', 'pbkdf2_sha256__default_rounds': 1000000}, 'min_username_length': 1, 'max_username_length': 60, 'min_password_length': 5}, 'plugins': {'repo_separator': '@', 'lock_sleep_total': 1.0, 'lock_sleep_increment': 0.1}, 'pipes': {'dtypes': {'min_ratio_columns_changed_for_full_astype': 0.5}, 'max_bound_time_days': 36525}, 'jobs': {'check_restart_seconds': 1.0, 'stop_token': '<------- MRSM_STOP_TOKEN ------->', 'clear_token': '<------- MRSM_CLEAR_TOKEN ------->', 'flush_token': '<------- MRSM_FLUSH_TOKEN ------->\n'}, 'tokens': {'minimum_length': 24, 'maximum_length': 32, 'hash_rounds': 100000, 'scopes': {'pipes:read': "Read pipes' parameters and the contents of target tables.", 'pipes:write': "Update pipes' parameters and sync to target tables.", 'pipes:drop': 'Drop target tables.', 'pipes:delete': "Delete pipes' parameters and drop target tables.", 'actions:execute': 'Execute arbitrary actions.', 'connectors:read': 'Read the available connectors.', 'jobs:read': "Read jobs' properties", 'jobs:write': "Write jobs' properties", 'jobs:execute': 'Run jobs.', 'jobs:delete': 'Delete jobs.', 'logs:read': "Read jobs' logs.", 'jobs:stop': 'Stop running jobs.', 'jobs:pause': 'Pause running jobs.', 'instance:read': "Read an instance's system-level metadata.", 'instance:chain': 'Allow chaining API instances using the associated credentials.', 'plugins:write': "Register and update plugins' metadata.", 'plugins:read': 'Read attributes of registered plugins.', 'plugins:delete': 'Delete plugins (owned by user) from the repository.', 'users:read': 'Read metadata about the associated account.', 'users:write': 'Write metadata for the associated account.', 'users:register': 'Register new user accounts.', 'users:delete': 'Delete the associated user account (or other users for admins).'}}}