meerschaum.config

Meerschaum v3.0.8

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

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

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

Write a plugin's configuration dictionary.

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

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

Parameters
  • keys (str:): List of strings to index.
  • patch (bool, default True): If True, patch missing default keys into the config directory. Defaults 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        from meerschaum.config._paths import CONFIG_DIR_PATH
106        directory = CONFIG_DIR_PATH
107
108    from meerschaum.config import _allow_write_missing
109    from meerschaum._internal.static import STATIC_CONFIG
110    from meerschaum.config._default import default_header_comment
111    from meerschaum.config._read_config import get_keyfile_path, revert_symlinks_config
112    from meerschaum.utils.yaml import yaml
113    from meerschaum.utils.misc import filter_keywords
114    import json
115    import os
116    if config_dict is None:
117        from meerschaum.config import _config
118        cf = _config(allow_replaced=False)
119        config_dict = cf
120
121    if not _allow_write_missing:
122        return False
123
124    default_filetype = STATIC_CONFIG['config']['default_filetype']
125    filetype_dumpers = {
126        'yml' : yaml.dump,
127        'yaml' : yaml.dump,
128        'json' : json.dump,
129    }
130
131    config_dict = revert_symlinks_config(config_dict)
132
133    def determine_filetype(k, v):
134        if k == 'meerschaum':
135            return 'yaml'
136        if isinstance(v, dict) and 'filetype' in v:
137            return v['filetype']
138        path = get_keyfile_path(k, create_new=False, directory=directory)
139        if path is None:
140            return default_filetype
141        filetype = path.suffix[1:]
142        if not isinstance(filetype, str) or filetype not in filetype_dumpers:
143            print(f"Invalid filetype '{filetype}' for '{k}'. Assuming {default_filetype}...")
144            filetype = default_filetype
145        return filetype
146
147    for k, v in config_dict.items():
148        filetype = determine_filetype(k, v)        
149        filename = str(k) + '.' + str(filetype)
150        filepath = os.path.join(directory, filename)
151        pathlib.Path(filepath).parent.mkdir(exist_ok=True)
152        with open(filepath, 'w+') as f:
153            try:
154                if k == 'meerschaum':
155                    f.write(default_header_comment)
156                filetype_dumpers[filetype](
157                    v, f,
158                    **filter_keywords(
159                        filetype_dumpers[filetype],
160                        sort_keys = False,
161                        indent = 2
162                    )
163                )
164                success = True
165            except Exception as e:
166                success = False
167                print("FAILED TO WRITE!")
168                print(e)
169                print(filter_keywords(
170                    filetype_dumpers[filetype],
171                    sort_keys=False,
172                    indent = 2
173                ))
174
175            if not success:
176                try:
177                    if os.path.exists(filepath):
178                        os.remove(filepath)
179                except Exception:
180                    print(f"Failed to write '{k}'")
181                return False
182
183    return True

Write YAML and JSON files to the configuration directory.

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

Edit the configuration files.

def set_config(cf: Dict[str, Any]) -> Dict[str, Any]:
102def set_config(cf: Dict[str, Any]) -> Dict[str, Any]:
103    """
104    Set the configuration dictionary.
105    """
106    global config
107    if not isinstance(cf, dict):
108        from meerschaum.utils.warnings import error
109        error(f"Invalid value for config: {cf}")
110    with _locks['config']:
111        config = cf
112    return config

Set the configuration dictionary.

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

Temporarily override the Meerschaum config dictionary.

Parameters
  • config_ (Dict[str, Any]): The new config dictionary to temporarily replace the canonical config.
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]:
217def search_and_substitute_config(
218    config: Dict[str, Any],
219    leading_key: str = "MRSM",
220    delimiter: str = ":",
221    begin_key: str = "{",
222    end_key: str = "}",
223    literal_key: str = '!',
224    keep_symlinks: bool = True,
225) -> Dict[str, Any]:
226    """
227    Search the config for Meerschaum substitution syntax and substite with value of keys.
228
229    Parameters
230    ----------
231    config: Dict[str, Any]
232        The Meerschaum configuration dictionary to search through.
233
234    leading_key: str, default 'MRSM'
235        The string with which to start the search.
236
237    begin_key: str, default '{'
238        The string to start the keys list.
239
240    end_key: str, default '}'
241        The string to end the keys list.
242
243    literal_key: str, default '!'
244        The string to force an literal interpretation of a value.
245        When the string is isolated, a literal interpreation is assumed and the surrounding
246        quotes are replaced.
247        
248        E.g. Suppose a:b:c produces a dictionary {'d': 1}.
249        - 'MRSM{a:b:c}'    => {'d': 1}        : isolated
250        - ' MRSM{a:b:c} '  => ' "{\'d\': 1}"' : not isolated
251        - ' MRSM{!a:b:c} ' => ' {"d": 1}'     : literal
252
253    keep_symlinks: bool, default True
254        If True, include the symlinks under the top-level key '_symlinks' (never written to a file).
255        Defaults to True.
256        
257        Example:
258
259        ```
260        MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host']
261        ``` 
262
263    Returns
264    -------
265    The configuration dictionary with `MRSM{}` symlinks replaced with
266    the values from the current configuration.
267    """
268    from meerschaum.config import get_config
269
270    _links = []
271    def _find_symlinks(d, _keys: Optional[List[str]] = None):
272        if _keys is None:
273            _keys = []
274        if not isinstance(d, dict):
275            return
276        for k, v in d.items():
277            if isinstance(v, dict):
278                _find_symlinks(v, _keys + [k])
279            elif (leading_key + begin_key) in str(v):
280                _links.append((_keys + [k], v))
281
282    _find_symlinks(config)
283
284    import json
285    needle = leading_key + begin_key
286    haystack = json.dumps(config, separators=(',', ':'))
287
288    patterns = {}
289    isolated_patterns = {}
290    literal_patterns = {}
291
292    begin, end, floor = 0, 0, 0
293    while needle in haystack[floor:]:
294        ### extract the keys
295        hs = haystack[floor:]
296
297        ### the first character of the keys
298        ### MRSM{key1:key2}
299        ###      ^
300        begin = hs.find(needle) + len(needle)
301
302        ### The character behind the needle.
303        ### "MRSM{key1:key2}"
304        ### ^
305        prior = haystack[(floor + begin) - (len(needle) + 1)]
306
307        ### number of characters to end of keys
308        ### (really it's the index of the beginning of the end_key relative to the beginning
309        ###     but the math works out)
310        ### MRSM{key1}
311        ###      ^   ^  => 4
312        length = hs[begin:].find(end_key)
313
314        ### index of the end_key (end of `length` characters)
315        end = begin + length
316
317        ### The character after the end_key.
318        after = haystack[floor + end + 1]
319
320        ### advance the floor to find the next leading key
321        floor += end + len(end_key)
322        pattern_keys = hs[begin:end].split(delimiter)
323
324        ### Check for isolation key and empty keys (MRSM{}).
325        force_literal = False
326        keys = [k for k in pattern_keys]
327        if str(keys[0]).startswith(literal_key):
328            keys[0] = str(keys[0])[len(literal_key):]
329            force_literal = True
330        if len(keys) == 1 and keys[0] == '':
331            keys = []
332
333        ### Evaluate the parsed keys to extract the referenced value.
334        ### TODO This needs to be recursive for chaining symlinks together.
335        try:
336            valid, value = get_config(
337                *keys,
338                substitute=False,
339                as_tuple=True,
340                write_missing=False,
341                sync_files=False,
342            )
343        except Exception:
344            import traceback
345            traceback.print_exc()
346            valid = False
347        if not valid:
348            continue
349
350        ### pattern to search and replace
351        pattern = leading_key + begin_key + delimiter.join(pattern_keys) + end_key
352
353        ### store patterns and values
354        patterns[pattern] = value
355
356        ### Determine whether the pattern occured inside a string or is an isolated, direct symlink.
357        isolated_patterns[pattern] = (prior == '"' and after == '"')
358
359        literal_patterns[pattern] = force_literal
360
361    ### replace the patterns with the values
362    for pattern, value in patterns.items():
363        if isolated_patterns[pattern]:
364            haystack = haystack.replace(
365                json.dumps(pattern),
366                json.dumps(value),
367            )
368        elif literal_patterns[pattern]:
369            haystack = haystack.replace(
370                pattern,
371                (
372                    json.dumps(value)
373                    .replace("\\", "\\\\")
374                    .replace('"', '\\"')
375                    .replace("'", "\\'")
376                )
377            )
378        else:
379            haystack = haystack.replace(pattern, str(value))
380
381    ### parse back into dict
382    parsed_config = json.loads(haystack) or {}
383
384    symlinks = {}
385    if keep_symlinks:
386        ### Keep track of symlinks for writing back to a file.
387        for _keys, _pattern in _links:
388            s = symlinks
389            for k in _keys[:-1]:
390                if k not in s:
391                    s[k] = {}
392                s = s[k]
393            s[_keys[-1]] = _pattern
394
395        from meerschaum.config._patch import apply_patch_to_config
396        symlinks_key = STATIC_CONFIG['config']['symlinks_key']
397        if symlinks:
398            if symlinks_key not in parsed_config:
399                parsed_config[symlinks_key] = symlinks
400            else:
401                parsed_config[symlinks_key] = apply_patch_to_config(
402                    parsed_config[symlinks_key],
403                    symlinks,
404                    warn=False,
405                )
406
407    return parsed_config

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

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

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

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

    Example:

    MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host']
    
Returns
  • The configuration dictionary with MRSM{} symlinks replaced with
  • the values from the current configuration.
def get_possible_keys() -> List[str]:
445def get_possible_keys() -> List[str]:
446    """
447    Return a list of possible top-level keys.
448    """
449    import os
450    from meerschaum.config._paths import CONFIG_DIR_PATH
451    from meerschaum.config._default import default_config
452    keys = set()
453    for key in default_config:
454        keys.add(key)
455    for filename in os.listdir(CONFIG_DIR_PATH):
456        keys.add('.'.join(filename.split('.')[:-1]))
457    return sorted(list(keys))

Return a list of possible top-level keys.

def get_keyfile_path( key: str, create_new: bool = False, directory: Union[pathlib.Path, str, NoneType] = None) -> Optional[pathlib.Path]:
460def get_keyfile_path(
461    key: str,
462    create_new: bool = False,
463    directory: Union[pathlib.Path, str, None] = None,
464) -> Union[pathlib.Path, None]:
465    """Determine a key's file path."""
466    import os
467    import pathlib
468    if directory is None:
469        from meerschaum.config._paths import CONFIG_DIR_PATH
470        directory = CONFIG_DIR_PATH
471
472    try:
473        return pathlib.Path(
474            os.path.join(
475                directory,
476                read_config(
477                    keys=[key],
478                    with_filenames=True,
479                    write_missing=False,
480                    substitute=False,
481                )[1][0]
482            )
483        )
484    except IndexError:
485        if create_new:
486            default_filetype = STATIC_CONFIG['config']['default_filetype']
487            return pathlib.Path(os.path.join(directory, key + '.' + default_filetype))
488        return None

Determine a key's file path.

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    _base = copy.deepcopy(config) if isinstance(config, dict) else {}
22    if not isinstance(patch, dict):
23        return config
24
25    def update_dict(base, patch):
26        if base is None:
27            return {}
28        if not isinstance(base, dict):
29            if warn:
30                _warn(f"Overwriting the value {base} with a dictionary:\n{patch}")
31            base = {}
32        for key, value in patch.items():
33            if isinstance(value, dict):
34                base[key] = update_dict(base.get(key, {}), value)
35            else:
36                base[key] = value
37        return base
38
39    return update_dict(_base, patch)

Patch the config dict with a new dict (cascade patching).

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

Read the configuration directory.

Parameters
  • directory (Union[pathlib.Path, str, None], default None): The directory with configuration files (.json and .yaml).
  • keys (Optional[List[str]], default None): Which configuration files to read.
  • write_missing (bool, default True): If a keyfile does not exist but is defined in the default configuration, write the file to disk.
  • substitute (bool, default True): Replace MRSM{} syntax with configuration values.
  • with_filename (bool, default False): If True, return a tuple of the configuration dictionary with a list of read filenames.
  • raise_parsing_errors (bool, default False): If True, re-raise parsing exceptions.
Examples
>>> read_config(keys=['meerschaum'], with_filename=True)
>>> ({...}, ['meerschaum.yaml'])
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': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'create_engine_flavors': {'timescaledb': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'host', 'username', 'database'}, 'defaults': {'port': 5432}}, 'timescaledb-ha': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'host', 'username', 'database'}, 'defaults': {'port': 5432}}, 'postgresql': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'host', 'username', 'database'}, 'defaults': {'port': 5432}}, 'postgis': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'host', 'username', 'database'}, 'defaults': {'port': 5432}}, 'citus': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'host', 'username', 'database'}, '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', 'host', 'username', 'database'}, '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': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'password', 'host', 'username', 'database'}, 'defaults': {'port': 3306}}, 'mariadb': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'password', 'host', 'username', 'database'}, 'defaults': {'port': 3306}}, 'oracle': {'engine': 'oracle+oracledb', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'password', 'host', 'username', 'database'}, 'defaults': {'port': 1521}}, 'sqlite': {'engine': 'sqlite', 'create_engine': {'pool_size': 4, 'max_overflow': 4, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database'}, 'defaults': {}}, 'geopackage': {'engine': 'sqlite', 'create_engine': {'pool_size': 4, 'max_overflow': 4, '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': 4, 'max_overflow': 4, '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).'}}}