meerschaum.config

Meerschaum v3.4.1

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

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

Write a plugin's configuration dictionary.

def get_config( *keys: str, patch: bool = True, substitute: bool = True, sync_files: bool = True, write_missing: bool = True, as_tuple: bool = False, warn: bool = True, debug: bool = False) -> Any:
112def get_config(
113    *keys: str,
114    patch: bool = True,
115    substitute: bool = True,
116    sync_files: bool = True,
117    write_missing: bool = True,
118    as_tuple: bool = False,
119    warn: bool = True,
120    debug: bool = False
121) -> Any:
122    """
123    Return the Meerschaum configuration dictionary.
124    If positional arguments are provided, index by the keys.
125    Raises a warning if invalid keys are provided.
126
127    Parameters
128    ----------
129    keys: str:
130        List of strings to index.
131
132    patch: bool, default True
133        If `True`, patch missing default keys into the config directory.
134        Defaults to `True`.
135
136    sync_files: bool, default True
137        If `True`, sync files if needed.
138        Defaults to `True`.
139
140    write_missing: bool, default True
141        If `True`, write default values when the main config files are missing.
142        Defaults to `True`.
143
144    substitute: bool, default True
145        If `True`, subsitute 'MRSM{}' values.
146        Defaults to `True`.
147
148    as_tuple: bool, default False
149        If `True`, return a tuple of type (success, value).
150        Defaults to `False`.
151        
152    Returns
153    -------
154    The value in the configuration directory, indexed by the provided keys.
155
156    Examples
157    --------
158    >>> get_config('meerschaum', 'instance')
159    'sql:main'
160    >>> get_config('does', 'not', 'exist')
161    UserWarning: Invalid keys in config: ('does', 'not', 'exist')
162    """
163    import json
164
165    symlinks_key = STATIC_CONFIG['config']['symlinks_key']
166    if debug:
167        from meerschaum.utils.debug import dprint
168        dprint(f"Indexing keys: {keys}", color=False)
169
170    if len(keys) == 0:
171        _rc = _config(
172            substitute=substitute,
173            sync_files=sync_files,
174            write_missing=(write_missing and _allow_write_missing),
175        )
176        if as_tuple:
177            return True, _rc 
178        return _rc
179    
180    ### Weird threading issues, only import if substitute is True.
181    if substitute:
182        from meerschaum.config._read_config import search_and_substitute_config
183    ### Invalidate the cache if it was read before with substitute=False
184    ### but there still exist substitutions.
185    if (
186        config is not None and substitute and keys[0] != symlinks_key
187        and 'MRSM{' in json.dumps(config.get(keys[0]))
188    ):
189        try:
190            _subbed = search_and_substitute_config({keys[0]: config[keys[0]]})
191        except Exception:
192            import traceback
193            traceback.print_exc()
194            _subbed = {keys[0]: config[keys[0]]}
195
196        config[keys[0]] = _subbed[keys[0]]
197        if symlinks_key in _subbed:
198            if symlinks_key not in config:
199                config[symlinks_key] = {}
200            config[symlinks_key] = apply_patch_to_config(
201                _subbed.get(symlinks_key, {}),
202                config.get(symlinks_key, {}),
203            )
204
205    from meerschaum.config._sync import sync_files as _sync_files
206    if config is None:
207        _config(*keys, sync_files=sync_files)
208
209    invalid_keys = False
210    if keys[0] not in config and keys[0] != symlinks_key:
211        single_key_config = read_config(
212            keys=[keys[0]], substitute=substitute, write_missing=write_missing
213        )
214        if keys[0] not in single_key_config:
215            invalid_keys = True
216        else:
217            config[keys[0]] = single_key_config.get(keys[0], None)
218            if symlinks_key in single_key_config and keys[0] in single_key_config[symlinks_key]:
219                if symlinks_key not in config:
220                    config[symlinks_key] = {}
221                config[symlinks_key][keys[0]] = single_key_config[symlinks_key][keys[0]]
222
223            if sync_files:
224                _sync_files(keys=[keys[0]])
225
226    c = config
227    if len(keys) > 0:
228        for k in keys:
229            try:
230                c = c[k]
231            except Exception:
232                invalid_keys = True
233                break
234        if invalid_keys:
235            ### Check if the keys are in the default configuration.
236            from meerschaum.config._default import default_config
237            in_default = True
238            patched_default_config = (
239                search_and_substitute_config(default_config)
240                if substitute else copy.deepcopy(default_config)
241            )
242            _c = patched_default_config
243            for k in keys:
244                try:
245                    _c = _c[k]
246                except Exception:
247                    in_default = False
248            if in_default:
249                c = _c
250                invalid_keys = False
251            warning_msg = f"Invalid keys in config: {keys}"
252            if not in_default:
253                try:
254                    if warn:
255                        from meerschaum.utils.warnings import warn as _warn
256                        _warn(warning_msg, stacklevel=3, color=False)
257                except Exception:
258                    if warn:
259                        print(warning_msg)
260                if as_tuple:
261                    return False, None
262                return None
263
264            ### Don't write keys that we haven't yet loaded into memory.
265            not_loaded_keys = [k for k in patched_default_config if k not in config]
266            for k in not_loaded_keys:
267                patched_default_config.pop(k, None)
268
269            set_config(
270                apply_patch_to_config(
271                    patched_default_config,
272                    config,
273                )
274            )
275            if patch and keys[0] != symlinks_key and write_missing:
276                ### Only persist defaults when the key's file is genuinely absent.
277                ### Never overwrite an existing file (e.g. one that failed to parse) ─
278                ### doing so would clobber the user's config with default values.
279                ### Brand-new config files are still created by `read_config()`.
280                from meerschaum.config._read_config import get_keyfile_path
281                keyfile_exists = get_keyfile_path(keys[0], create_new=False) is not None
282                if not keyfile_exists:
283                    write_config(config, debug=debug)
284
285    if as_tuple:
286        return (not invalid_keys), c
287    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:
 99def write_config(
100    config_dict: Optional[Dict[str, Any]] = None,
101    directory: Union[str, pathlib.Path, None] = None,
102    debug: bool = False,
103    **kw: Any
104) -> bool:
105    """Write YAML and JSON files to the configuration directory.
106
107    Parameters
108    ----------
109    config_dict: Optional[Dict[str, Any]], default None
110        A dictionary of keys to dictionaries of configuration.
111        Each key corresponds to a .yaml or .json config file.
112        Writing config to a directory with different keys
113        does not affect existing keys in that directory.
114        If not provided, use the currently loaded config dictionary.
115
116    directory: Union[str, pathlib.Path, None], default None
117        The directory to which the keys are written.
118        If not provided, use the default config path (`~/.config/meerschaum/config/`).
119
120    Returns
121    -------
122    A bool indicating success.
123
124    """
125    if directory is None:
126        import meerschaum.config.paths as paths
127        directory = paths.CONFIG_DIR_PATH
128
129    from meerschaum.config import _allow_write_missing
130    from meerschaum._internal.static import STATIC_CONFIG
131    from meerschaum.config._default import default_header_comment
132    from meerschaum.config._read_config import get_keyfile_path, revert_symlinks_config
133    from meerschaum.utils.yaml import yaml
134    from meerschaum.utils.misc import filter_keywords
135    import json
136    import os
137    if config_dict is None:
138        from meerschaum.config import _config
139        cf = _config(allow_replaced=False)
140        config_dict = cf
141
142    if not _allow_write_missing:
143        return False
144
145    default_filetype = STATIC_CONFIG['config']['default_filetype']
146    filetype_dumpers = {
147        'yml' : yaml.dump,
148        'yaml' : yaml.dump,
149        'json' : json.dump,
150    }
151
152    config_dict = revert_symlinks_config(config_dict)
153
154    def determine_filetype(k, v):
155        if k == 'meerschaum':
156            return 'yaml'
157        if isinstance(v, dict) and 'filetype' in v:
158            return v['filetype']
159        path = get_keyfile_path(k, create_new=False, directory=directory)
160        if path is None:
161            return default_filetype
162        filetype = path.suffix[1:]
163        if not isinstance(filetype, str) or filetype not in filetype_dumpers:
164            print(f"Invalid filetype '{filetype}' for '{k}'. Assuming {default_filetype}...")
165            filetype = default_filetype
166        return filetype
167
168    for k, v in config_dict.items():
169        filetype = determine_filetype(k, v)        
170        filename = str(k) + '.' + str(filetype)
171        filepath = os.path.join(directory, filename)
172        pathlib.Path(filepath).parent.mkdir(exist_ok=True)
173        with open(filepath, 'w+') as f:
174            try:
175                if k == 'meerschaum':
176                    f.write(default_header_comment)
177                filetype_dumpers[filetype](
178                    v, f,
179                    **filter_keywords(
180                        filetype_dumpers[filetype],
181                        sort_keys = False,
182                        indent = 2
183                    )
184                )
185                success = True
186            except Exception as e:
187                success = False
188                print("FAILED TO WRITE!")
189                print(e)
190                print(filter_keywords(
191                    filetype_dumpers[filetype],
192                    sort_keys=False,
193                    indent = 2
194                ))
195
196            if not success:
197                try:
198                    if os.path.exists(filepath):
199                        os.remove(filepath)
200                except Exception:
201                    print(f"Failed to write '{k}'")
202                return False
203
204    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.config._backup import backup_config_key, restore_config_backup
27    from meerschaum._internal.static import STATIC_CONFIG
28    from meerschaum.utils.packages import reload_meerschaum
29    from meerschaum.utils.misc import edit_file
30    from meerschaum.utils.warnings import warn, info
31    from meerschaum.utils.prompt import prompt, yes_no
32
33    if keys is None:
34        keys = []
35
36    symlinks_key = STATIC_CONFIG['config']['symlinks_key']
37    def _edit_key(key: str):
38        new_key_config = None
39        ### Snapshot the pristine file before any edits or default-merging so it
40        ### can be rolled back if the edit corrupts it.
41        backup_path = backup_config_key(key, debug=debug)
42        while True:
43            ### If defined in default, create the config file.
44            symlinks_key_config = config.get(symlinks_key, {}).get(key, {})
45            key_config = revert_symlinks_config({
46                key: config.pop(key, {}),
47                symlinks_key: {key: symlinks_key_config},
48            })
49            keyfile_path = get_keyfile_path(key, create_new=True)
50            get_config(key, write_missing=True, warn=False)
51
52            edit_file(get_keyfile_path(key, create_new=True))
53
54            ### TODO: verify that the file is valid. Retry if not.
55            try:
56                new_key_config = read_config(
57                    paths.CONFIG_DIR_PATH,
58                    [key],
59                    write_missing=False,
60                    raise_parsing_errors=True,
61                )
62            except Exception:
63                if key_config:
64                    config[key] = key_config
65                warn(f"Could not parse key '{key}'.", stack=False)
66                if backup_path is not None and yes_no(
67                    f"Restore '{key}' from the backup taken before editing?",
68                    default='n',
69                ):
70                    restore_success, restore_msg = restore_config_backup(
71                        key, backup_path=backup_path, reload=False, debug=debug,
72                    )
73                    if restore_success:
74                        info(restore_msg)
75                        new_key_config = read_config(
76                            paths.CONFIG_DIR_PATH,
77                            [key],
78                            write_missing=False,
79                            raise_parsing_errors=False,
80                        )
81                        break
82                    warn(restore_msg, stack=False)
83                _ = prompt(f"Press [Enter] to edit '{keyfile_path}', [CTRL+C] to exit.")
84                continue
85
86            if new_key_config:
87                break
88
89    try:
90        for k in keys:
91            _edit_key(k)
92    except KeyboardInterrupt:
93        return False, ""
94
95    reload_meerschaum(debug=debug)
96    return (True, "Success")

Edit the configuration files.

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

Set the configuration dictionary.

@contextlib.contextmanager
def replace_config(config_: Optional[Dict[str, Any]]):
335@contextlib.contextmanager
336def replace_config(config_: Union[Dict[str, Any], None]):
337    """
338    Temporarily override the Meerschaum config dictionary.
339
340    Parameters
341    ----------
342    config_: Dict[str, Any]
343        The new config dictionary to temporarily replace the canonical `config`.
344    """
345    if config_ is None:
346        try:
347            yield
348        except Exception:
349            pass
350        return
351
352    global _backup_config, _allow_write_missing
353
354    _backup_config = _config()
355    _allow_write_missing = False
356    set_config(config_)
357
358    try:
359        yield
360    finally:
361        set_config(_backup_config)
362        _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]:
227def search_and_substitute_config(
228    config: Dict[str, Any],
229    leading_key: str = "MRSM",
230    delimiter: str = ":",
231    begin_key: str = "{",
232    end_key: str = "}",
233    literal_key: str = '!',
234    keep_symlinks: bool = True,
235) -> Dict[str, Any]:
236    """
237    Search the config for Meerschaum substitution syntax and substite with value of keys.
238
239    Parameters
240    ----------
241    config: Dict[str, Any]
242        The Meerschaum configuration dictionary to search through.
243
244    leading_key: str, default 'MRSM'
245        The string with which to start the search.
246
247    begin_key: str, default '{'
248        The string to start the keys list.
249
250    end_key: str, default '}'
251        The string to end the keys list.
252
253    literal_key: str, default '!'
254        The string to force an literal interpretation of a value.
255        When the string is isolated, a literal interpreation is assumed and the surrounding
256        quotes are replaced.
257        
258        E.g. Suppose a:b:c produces a dictionary {'d': 1}.
259        - 'MRSM{a:b:c}'    => {'d': 1}        : isolated
260        - ' MRSM{a:b:c} '  => ' "{\'d\': 1}"' : not isolated
261        - ' MRSM{!a:b:c} ' => ' {"d": 1}'     : literal
262
263    keep_symlinks: bool, default True
264        If True, include the symlinks under the top-level key '_symlinks' (never written to a file).
265        Defaults to True.
266        
267        Example:
268
269        ```
270        MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host']
271        ``` 
272
273    Returns
274    -------
275    The configuration dictionary with `MRSM{}` symlinks replaced with
276    the values from the current configuration.
277    """
278    import re
279    import json
280    from meerschaum.config import get_config
281    needle = leading_key + begin_key
282
283    _links = []
284    if keep_symlinks:
285        def _find_symlinks(d, _keys: Optional[List[str]] = None):
286            if _keys is None:
287                _keys = []
288            if not isinstance(d, dict):
289                return
290            for k, v in d.items():
291                if isinstance(v, dict):
292                    _find_symlinks(v, _keys + [k])
293                elif isinstance(v, str) and needle in v:
294                    _links.append((_keys + [k], v))
295        _find_symlinks(config)
296
297    haystack = json.dumps(config, separators=(',', ':'))
298    if needle not in haystack:
299        parsed_config = config
300    else:
301        patterns = {}
302        isolated_patterns = {}
303        literal_patterns = {}
304        memo = {}
305
306        pattern_re = re.compile(
307            re.escape(needle) + r'(?P<pattern_keys>.*?)' + re.escape(end_key)
308        )
309
310        for match in pattern_re.finditer(haystack):
311            pattern = match.group(0)
312            pattern_keys_raw = match.group('pattern_keys')
313            pattern_keys = pattern_keys_raw.split(delimiter)
314
315            start, end = match.span()
316            prior = haystack[start - 1] if start > 0 else None
317            after = haystack[end] if end < len(haystack) else None
318
319            force_literal = False
320            keys = [k for k in pattern_keys]
321            if keys and str(keys[0]).startswith(literal_key):
322                keys[0] = str(keys[0])[len(literal_key):]
323                force_literal = True
324            if len(keys) == 1 and keys[0] == '':
325                keys = []
326
327            cache_key = tuple(keys)
328            if cache_key in memo:
329                valid, value = memo[cache_key]
330            else:
331                try:
332                    valid, value = get_config(
333                        *keys,
334                        substitute=False,
335                        as_tuple=True,
336                        write_missing=False,
337                        sync_files=False,
338                    )
339                except Exception:
340                    valid, value = False, None
341                memo[cache_key] = (valid, value)
342
343            if not valid:
344                continue
345
346            patterns[pattern] = value
347            isolated_patterns[pattern] = (prior == '"' and after == '"')
348            literal_patterns[pattern] = force_literal
349
350        for pattern, value in patterns.items():
351            if isolated_patterns[pattern]:
352                haystack = haystack.replace(
353                    json.dumps(pattern),
354                    json.dumps(value),
355                )
356            elif literal_patterns[pattern]:
357                haystack = haystack.replace(
358                    pattern,
359                    (
360                        json.dumps(value)
361                        .replace("\\", "\\\\")
362                        .replace('"', '\\"')
363                        .replace("'", "\\'")
364                    )
365                )
366            else:
367                haystack = haystack.replace(pattern, str(value))
368
369        parsed_config = json.loads(haystack) or {}
370
371    symlinks = {}
372    if keep_symlinks:
373        for _keys, _pattern in _links:
374            s = symlinks
375            for k in _keys[:-1]:
376                if k not in s:
377                    s[k] = {}
378                s = s[k]
379            s[_keys[-1]] = _pattern
380
381        from meerschaum.config._patch import apply_patch_to_config
382        symlinks_key = STATIC_CONFIG['config']['symlinks_key']
383        if symlinks:
384            if symlinks_key not in parsed_config:
385                parsed_config[symlinks_key] = symlinks
386            else:
387                parsed_config[symlinks_key] = apply_patch_to_config(
388                    parsed_config[symlinks_key],
389                    symlinks,
390                    warn=False,
391                )
392
393    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]:
431def get_possible_keys() -> List[str]:
432    """
433    Return a list of possible top-level keys.
434    """
435    import os
436    import meerschaum.config.paths as paths
437    from meerschaum.config._default import default_config
438    keys = set()
439    for key in default_config:
440        keys.add(key)
441    for filename in os.listdir(paths.CONFIG_DIR_PATH):
442        keys.add('.'.join(filename.split('.')[:-1]))
443    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]:
446def get_keyfile_path(
447    key: str,
448    create_new: bool = False,
449    directory: Union[pathlib.Path, str, None] = None,
450) -> Union[pathlib.Path, None]:
451    """Determine a key's file path."""
452    import os
453    import pathlib
454    if directory is None:
455        import meerschaum.config.paths as paths
456        directory = paths.CONFIG_DIR_PATH
457
458    try:
459        return pathlib.Path(
460            os.path.join(
461                directory,
462                read_config(
463                    keys=[key],
464                    with_filenames=True,
465                    write_missing=False,
466                    substitute=False,
467                )[1][0]
468            )
469        )
470    except IndexError:
471        if create_new:
472            default_filetype = STATIC_CONFIG['config']['default_filetype']
473            return pathlib.Path(os.path.join(directory, key + '.' + default_filetype))
474        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                        if raise_parsing_errors:
183                            raise e
184                        ### The file exists but is malformed (e.g. a bad manual edit).
185                        ### Fall back to default values in memory so the app keeps
186                        ### working, but leave the file on disk untouched so it is
187                        ### never overwritten and can be fixed or rolled back.
188                        import copy
189                        from meerschaum.config._default import default_config
190                        from meerschaum.utils.warnings import warn as _warn
191                        _warn(
192                            f"Could not parse '{filename}':\n    {e}\n"
193                            "Using default values in memory; the file on disk is unchanged.\n"
194                            f"Fix it manually or run `edit config {key} --rollback` "
195                            "to restore a backup.",
196                            stack=False,
197                        )
198                        _config_key = copy.deepcopy(default_config.get(key, {}))
199                _single_key_config = (
200                    search_and_substitute_config({key: _config_key}) if substitute
201                    else {key: _config_key}
202                )
203                config[key] = _single_key_config[key]
204                if (
205                    symlinks_key in _single_key_config
206                    and key in _single_key_config[symlinks_key]
207                ):
208                    if symlinks_key not in config:
209                        config[symlinks_key] = {}
210                    config[symlinks_key][key] = _single_key_config[symlinks_key][key]
211                break
212            except Exception as e:
213                if raise_parsing_errors:
214                    raise e
215                print(f"Unable to parse {filename}!")
216                import traceback
217                traceback.print_exc()
218                input(f"Press [Enter] to open '{filename}' and fix formatting errors.")
219                from meerschaum.utils.misc import edit_file
220                edit_file(filepath)
221
222    if with_filenames:
223        return config, filekeys
224    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': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 5432}}, 'timescaledb-ha': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 5432}}, 'postgresql': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 5432}}, 'postgis': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 5432}}, 'citus': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 5432}}, 'mssql': {'engine': 'mssql+pyodbc', 'create_engine': {'fast_executemany': True, 'use_insertmanyvalues': False, 'isolation_level': 'AUTOCOMMIT', 'use_setinputsizes': False, 'pool_pre_ping': True, 'ignore_no_transaction_on_rollback': True}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 1433, 'options': 'driver=ODBC Driver 18 for SQL Server&UseFMTONLY=Yes&TrustServerCertificate=yes&Encrypt=no&MARS_Connection=yes'}}, 'mysql': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 3306}}, 'mariadb': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 3306}}, 'oracle': {'engine': 'oracle+oracledb', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'username', 'password', 'database', 'host'}, 'defaults': {'port': 1521}}, 'sqlite': {'engine': 'sqlite', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database'}, 'defaults': {}}, 'geopackage': {'engine': 'sqlite', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database'}, 'defaults': {}}, 'duckdb': {'engine': 'duckdb', 'create_engine': {}, 'omit_create_engine': {'ALL'}, 'to_sql': {'method': 'multi'}, 'requirements': '', 'defaults': {}}, 'cockroachdb': {'engine': 'cockroachdb', 'omit_create_engine': {'method'}, 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'to_sql': {'method': 'multi'}, 'requirements': {'host'}, 'defaults': {'port': 26257, 'database': 'defaultdb', 'username': 'root', 'password': 'admin'}}}}, 'valkey': {'colon': '-_'}, 'environment': {'config': 'MRSM_CONFIG', 'config_dir': 'MRSM_CONFIG_DIR', 'patch': 'MRSM_PATCH', 'root': 'MRSM_ROOT_DIR', 'plugins': 'MRSM_PLUGINS_DIR', 'venvs': 'MRSM_VENVS_DIR', 'runtime': 'MRSM_RUNTIME', 'work_dir': 'MRSM_WORK_DIR', 'user': 'MRSM_USER', 'dep_group': 'MRSM_DEP_GROUP', 'home': 'MRSM_HOME', 'src': 'MRSM_SRC', 'uid': 'MRSM_UID', 'gid': 'MRSM_GID', 'noask': 'MRSM_NOASK', 'noninteractive': 'MRSM_NONINTERACTIVE', 'id': 'MRSM_SERVER_ID', 'test_flavors': 'MRSM_TEST_FLAVORS', 'daemon_id': 'MRSM_DAEMON_ID', 'systemd_log_path': 'MRSM_SYSTEMD_LOG_PATH', 'systemd_stdin_path': 'MRSM_SYSTEMD_STDIN_PATH', 'systemd_result_path': 'MRSM_SYSTEMD_RESULT_PATH', 'systemd_delete_job': 'MRSM_SYSTEMD_DELETE_JOB', 'uri_regex': 'MRSM_([a-zA-Z0-9]*)_(\\d*[a-zA-Z][a-zA-Z0-9-_+]*$)', 'prefix': 'MRSM_'}, 'config': {'default_filetype': 'json', 'symlinks_key': '_symlinks', 'max_backups_per_key': 10}, '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).'}}}