meerschaum.config

Meerschaum v3.2.3

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

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):
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)

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:
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

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]:
 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

Set the configuration dictionary.

@contextlib.contextmanager
def replace_config(config_: Optional[Dict[str, Any]]):
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

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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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': {'database', 'password', 'username', '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).'}}}