meerschaum.config

Meerschaum v5.0.2

  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
 19import types
 20from meerschaum.utils.typing import Any, Dict, Optional, Union
 21from meerschaum.utils.threading import RLock
 22
 23from meerschaum.config._version import __version__
 24from meerschaum.config._store import get_store, set_store, reset_store
 25from meerschaum.config._edit import edit_config, write_config
 26from meerschaum.config._read_config import (
 27    search_and_substitute_config,
 28    revert_symlinks_config,
 29    get_possible_keys,
 30    get_keyfile_path,
 31    read_config,
 32)
 33from meerschaum._internal.static import STATIC_CONFIG
 34
 35from meerschaum.config._patch import (
 36    apply_patch_to_config,
 37)
 38from meerschaum import _core
 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    'environment',
 55    'paths',
 56    'STATIC_CONFIG',
 57)
 58__pdoc__ = {'static': False, 'resources': False, 'stack': False, }
 59_locks = {'config': RLock()}
 60
 61### The live config dictionary is `meerschaum.config.config` (see `_store.py`): the config registry
 62### owns the object, so it is resolved on access (`from meerschaum.config import config` gives the
 63### current dictionary, as v4's module global did), and reloading this module starts it over.
 64reset_store()
 65_backup_config = None
 66_allow_write_missing: bool = True
 67
 68
 69def _config(
 70    *keys: str,
 71    reload: bool = False,
 72    substitute: bool = True,
 73    sync_files: bool = True,
 74    allow_replaced: bool = True,
 75    write_missing: bool = True,
 76) -> Dict[str, Any]:
 77    """
 78    Read and process the configuration file.
 79    """
 80    if reload:
 81        with _locks['config']:
 82            reset_store()
 83
 84    config = get_store()
 85    if keys and keys[0] not in config:
 86        from meerschaum.config._sync import sync_files as _sync_files
 87        key_config = read_config(
 88            keys = [keys[0]],
 89            substitute = substitute,
 90            write_missing = write_missing and _allow_write_missing,
 91        )
 92        if keys[0] in key_config:
 93            config[keys[0]] = key_config[keys[0]]
 94            if sync_files and _allow_write_missing:
 95                _sync_files(keys=[keys[0] if keys else None])
 96
 97    if not allow_replaced:
 98        return _backup_config if _backup_config is not None else config
 99
100    return config
101
102
103def set_config(cf: Dict[str, Any]) -> Dict[str, Any]:
104    """
105    Set the configuration dictionary.
106    """
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        set_store(cf)
112    return cf
113
114
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    return _core.get_config(
167        *keys, patch=patch, substitute=substitute, sync_files=sync_files,
168        write_missing=write_missing, as_tuple=as_tuple, warn=warn, debug=debug,
169    )
170
171
172def get_plugin_config(
173    *keys: str,
174    warn: bool = False,
175    **kw: Any
176) -> Optional[Any]:
177    """
178    This may only be called from within a Meerschaum plugin.
179    See `meerschaum.config.get_config` for arguments.
180    """
181    from meerschaum.utils.warnings import error
182    from meerschaum.plugins import _get_parent_plugin
183    parent_plugin_name = _get_parent_plugin(2)
184    if parent_plugin_name is None:
185        error(
186            "You may only call `get_plugin_config()` "
187            "from within a Meerschaum plugin."
188        )
189
190    return _core.get_plugin_config(parent_plugin_name, *keys, warn=warn, **kw)
191
192
193def write_plugin_config(
194    config_dict: Dict[str, Any],
195    **kw: Any
196):
197    """
198    Write a plugin's configuration dictionary.
199    """
200    from meerschaum.utils.warnings import error
201    from meerschaum.plugins import _get_parent_plugin
202    parent_plugin_name = _get_parent_plugin(2)
203    if parent_plugin_name is None:
204        error("You may only call `get_plugin_config()` from within a Meerschaum plugin.")
205    return _core.write_plugin_config(parent_plugin_name, config_dict, **kw)
206
207
208@contextlib.contextmanager
209def replace_config(config_: Union[Dict[str, Any], None]):
210    """
211    Temporarily override the Meerschaum config dictionary.
212
213    Parameters
214    ----------
215    config_: Dict[str, Any]
216        The new config dictionary to temporarily replace the canonical `config`.
217    """
218    if config_ is None:
219        yield
220        return
221
222    global _backup_config, _allow_write_missing
223
224    previous_config = _config()
225    previous_backup_config = _backup_config
226    previous_allow_write_missing = _allow_write_missing
227    _backup_config = (
228        previous_backup_config
229        if previous_backup_config is not None
230        else previous_config
231    )
232    _allow_write_missing = False
233    set_config(config_)
234
235    try:
236        yield
237    finally:
238        set_config(previous_config)
239        _backup_config = previous_backup_config
240        _allow_write_missing = previous_allow_write_missing
241
242### This need to be below get_config to avoid a circular import.
243from meerschaum.config._read_config import read_config
244
245### If environment variable MRSM_CONFIG or MRSM_PATCH is set, patch config before anything else.
246from meerschaum.config.environment import (
247    apply_environment_patches as _apply_environment_patches,
248    apply_environment_uris as _apply_environment_uris,
249)
250_apply_environment_uris()
251_apply_environment_patches()
252
253patch_config = None
254if paths.PATCH_DIR_PATH.exists():
255    from meerschaum.utils.yaml import yaml, _yaml
256    if _yaml is not None:
257        patch_config = read_config(directory=paths.PATCH_DIR_PATH)
258
259permanent_patch_config = None
260if paths.PERMANENT_PATCH_DIR_PATH.exists():
261    from meerschaum.utils.yaml import yaml, _yaml
262    if _yaml is not None:
263        permanent_patch_config = read_config(directory=paths.PERMANENT_PATCH_DIR_PATH)
264### If patches exist, apply to config.
265if patch_config is not None:
266    set_config(apply_patch_to_config(_config(), patch_config))
267    if paths.PATCH_DIR_PATH.exists():
268        shutil.rmtree(paths.PATCH_DIR_PATH)
269
270### if permanent_patch.yaml exists, apply patch to config, write config, and delete patch
271if permanent_patch_config is not None and paths.PERMANENT_PATCH_DIR_PATH.exists():
272    print(
273        "Found permanent patch configuration. " +
274        "Updating main config and deleting permanent patch..."
275    )
276    set_config(apply_patch_to_config(_config(), permanent_patch_config))
277    write_config(_config())
278    permanent_patch_config = None
279    if paths.PERMANENT_PATCH_DIR_PATH.exists():
280        shutil.rmtree(paths.PERMANENT_PATCH_DIR_PATH)
281    if paths.DEFAULT_CONFIG_DIR_PATH.exists():
282        shutil.rmtree(paths.DEFAULT_CONFIG_DIR_PATH)
283
284
285### Make sure readline is available for the portable version.
286environment_runtime = STATIC_CONFIG['environment']['runtime']
287if environment_runtime in os.environ:
288    if os.environ[environment_runtime] == 'portable':
289        from meerschaum.utils.packages import ensure_readline
290        if not paths.PORTABLE_CHECK_READLINE_PATH.exists():
291            ensure_readline()
292            paths.PORTABLE_CHECK_READLINE_PATH.touch()
293
294
295### If interactive REPL, print welcome header.
296__doc__ = f"Meerschaum v{__version__}"
297try:
298    interactive = False
299    if sys.ps1:
300        interactive = True
301except AttributeError:
302    interactive = False
303if interactive:
304    msg = __doc__
305    print(msg, file=sys.stderr)
306
307
308class _ConfigModule(types.ModuleType):
309    """
310    The `meerschaum.config` module type: `config` is the live dictionary of the registry, so
311    `meerschaum.config.config = {...}` replaces it (as `set_config()` does) instead of creating
312    a second attribute that `get_config()` never sees.
313    """
314
315    @property
316    def config(self) -> Dict[str, Any]:
317        return get_store()
318
319    @config.setter
320    def config(self, cf: Dict[str, Any]) -> None:
321        set_config(cf)
322
323
324sys.modules[__name__].__class__ = _ConfigModule
def get_plugin_config(*keys: str, warn: bool = False, **kw: Any) -> Any | None:
173def get_plugin_config(
174    *keys: str,
175    warn: bool = False,
176    **kw: Any
177) -> Optional[Any]:
178    """
179    This may only be called from within a Meerschaum plugin.
180    See `meerschaum.config.get_config` for arguments.
181    """
182    from meerschaum.utils.warnings import error
183    from meerschaum.plugins import _get_parent_plugin
184    parent_plugin_name = _get_parent_plugin(2)
185    if parent_plugin_name is None:
186        error(
187            "You may only call `get_plugin_config()` "
188            "from within a Meerschaum plugin."
189        )
190
191    return _core.get_plugin_config(parent_plugin_name, *keys, warn=warn, **kw)

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):
194def write_plugin_config(
195    config_dict: Dict[str, Any],
196    **kw: Any
197):
198    """
199    Write a plugin's configuration dictionary.
200    """
201    from meerschaum.utils.warnings import error
202    from meerschaum.plugins import _get_parent_plugin
203    parent_plugin_name = _get_parent_plugin(2)
204    if parent_plugin_name is None:
205        error("You may only call `get_plugin_config()` from within a Meerschaum plugin.")
206    return _core.write_plugin_config(parent_plugin_name, config_dict, **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:
116def get_config(
117    *keys: str,
118    patch: bool = True,
119    substitute: bool = True,
120    sync_files: bool = True,
121    write_missing: bool = True,
122    as_tuple: bool = False,
123    warn: bool = True,
124    debug: bool = False
125) -> Any:
126    """
127    Return the Meerschaum configuration dictionary.
128    If positional arguments are provided, index by the keys.
129    Raises a warning if invalid keys are provided.
130
131    Parameters
132    ----------
133    keys: str:
134        List of strings to index.
135
136    patch: bool, default True
137        If `True`, patch missing default keys into the config directory.
138        Defaults to `True`.
139
140    sync_files: bool, default True
141        If `True`, sync files if needed.
142        Defaults to `True`.
143
144    write_missing: bool, default True
145        If `True`, write default values when the main config files are missing.
146        Defaults to `True`.
147
148    substitute: bool, default True
149        If `True`, subsitute 'MRSM{}' values.
150        Defaults to `True`.
151
152    as_tuple: bool, default False
153        If `True`, return a tuple of type (success, value).
154        Defaults to `False`.
155        
156    Returns
157    -------
158    The value in the configuration directory, indexed by the provided keys.
159
160    Examples
161    --------
162    >>> get_config('meerschaum', 'instance')
163    'sql:main'
164    >>> get_config('does', 'not', 'exist')
165    UserWarning: Invalid keys in config: ('does', 'not', 'exist')
166    """
167    return _core.get_config(
168        *keys, patch=patch, substitute=substitute, sync_files=sync_files,
169        write_missing=write_missing, as_tuple=as_tuple, warn=warn, debug=debug,
170    )

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: Dict[str, Any] | None = None, directory: str | pathlib.Path | None = None, debug: bool = False, **kw: Any) -> bool:
101def write_config(
102    config_dict: Optional[Dict[str, Any]] = None,
103    directory: Union[str, pathlib.Path, None] = None,
104    debug: bool = False,
105    **kw: Any
106) -> bool:
107    """Write YAML and JSON files to the configuration directory.
108
109    Parameters
110    ----------
111    config_dict: Optional[Dict[str, Any]], default None
112        A dictionary of keys to dictionaries of configuration.
113        Each key corresponds to a .yaml or .json config file.
114        Writing config to a directory with different keys
115        does not affect existing keys in that directory.
116        If not provided, use the currently loaded config dictionary.
117
118    directory: Union[str, pathlib.Path, None], default None
119        The directory to which the keys are written.
120        If not provided, use the default config path (`~/.config/meerschaum/config/`).
121
122    Returns
123    -------
124    A bool indicating success.
125
126    """
127    _rust = _core.write_config
128    return _rust(config_dict, directory=directory, debug=debug)

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: List[str] | None = None, params: Dict[str, Any] | None = None, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
17def edit_config(
18    keys: Optional[List[str]] = None,
19    params: Optional[Dict[str, Any]] = None,
20    debug: bool = False,
21    **kw: Any
22) -> SuccessTuple:
23    """Edit the configuration files."""
24    import meerschaum.config.paths as paths
25    from meerschaum.config import get_config, _config
26    from meerschaum.config._read_config import get_keyfile_path, read_config, revert_symlinks_config
27    from meerschaum.config._backup import backup_config_key, restore_config_backup
28    from meerschaum._internal.static import STATIC_CONFIG
29    from meerschaum.utils.packages import reload_meerschaum
30    from meerschaum.utils.misc import edit_file
31    from meerschaum.utils.warnings import warn, info
32    from meerschaum.utils.prompt import prompt, yes_no
33
34    if keys is None:
35        keys = []
36
37    symlinks_key = STATIC_CONFIG['config']['symlinks_key']
38    def _edit_key(key: str):
39        new_key_config = None
40        ### Snapshot the pristine file before any edits or default-merging so it
41        ### can be rolled back if the edit corrupts it.
42        backup_path = backup_config_key(key, debug=debug)
43        while True:
44            ### If defined in default, create the config file.
45            config = _config()
46            symlinks_key_config = config.get(symlinks_key, {}).get(key, {})
47            key_config = revert_symlinks_config({
48                key: config.pop(key, {}),
49                symlinks_key: {key: symlinks_key_config},
50            })
51            keyfile_path = get_keyfile_path(key, create_new=True)
52            get_config(key, write_missing=True, warn=False)
53
54            edit_file(get_keyfile_path(key, create_new=True))
55
56            ### Parse the edited file; offer the backup when it does not parse.
57            try:
58                new_key_config = read_config(
59                    paths.CONFIG_DIR_PATH,
60                    [key],
61                    write_missing=False,
62                    raise_parsing_errors=True,
63                )
64            except Exception:
65                if key_config:
66                    _config()[key] = key_config
67                warn(f"Could not parse key '{key}'.", stack=False)
68                if backup_path is not None and yes_no(
69                    f"Restore '{key}' from the backup taken before editing?",
70                    default='n',
71                ):
72                    restore_success, restore_msg = restore_config_backup(
73                        key, backup_path=backup_path, reload=False, debug=debug,
74                    )
75                    if restore_success:
76                        info(restore_msg)
77                        new_key_config = read_config(
78                            paths.CONFIG_DIR_PATH,
79                            [key],
80                            write_missing=False,
81                            raise_parsing_errors=False,
82                        )
83                        break
84                    warn(restore_msg, stack=False)
85                _ = prompt(f"Press [Enter] to edit '{keyfile_path}', [CTRL+C] to exit.")
86                continue
87
88            if new_key_config:
89                break
90
91    try:
92        for k in keys:
93            _edit_key(k)
94    except KeyboardInterrupt:
95        return False, ""
96
97    reload_meerschaum(debug=debug)
98    return (True, "Success")

Edit the configuration files.

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

Set the configuration dictionary.

@contextlib.contextmanager
def replace_config(config_: Dict[str, Any] | None):
209@contextlib.contextmanager
210def replace_config(config_: Union[Dict[str, Any], None]):
211    """
212    Temporarily override the Meerschaum config dictionary.
213
214    Parameters
215    ----------
216    config_: Dict[str, Any]
217        The new config dictionary to temporarily replace the canonical `config`.
218    """
219    if config_ is None:
220        yield
221        return
222
223    global _backup_config, _allow_write_missing
224
225    previous_config = _config()
226    previous_backup_config = _backup_config
227    previous_allow_write_missing = _allow_write_missing
228    _backup_config = (
229        previous_backup_config
230        if previous_backup_config is not None
231        else previous_config
232    )
233    _allow_write_missing = False
234    set_config(config_)
235
236    try:
237        yield
238    finally:
239        set_config(previous_config)
240        _backup_config = previous_backup_config
241        _allow_write_missing = previous_allow_write_missing

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]:
 65def search_and_substitute_config(
 66    config: Dict[str, Any],
 67    leading_key: str = "MRSM",
 68    delimiter: str = ":",
 69    begin_key: str = "{",
 70    end_key: str = "}",
 71    literal_key: str = '!',
 72    keep_symlinks: bool = True,
 73) -> Dict[str, Any]:
 74    """
 75    Search the config for Meerschaum substitution syntax and substite with value of keys.
 76
 77    Parameters
 78    ----------
 79    config: Dict[str, Any]
 80        The Meerschaum configuration dictionary to search through.
 81
 82    leading_key: str, default 'MRSM'
 83        The string with which to start the search.
 84
 85    begin_key: str, default '{'
 86        The string to start the keys list.
 87
 88    end_key: str, default '}'
 89        The string to end the keys list.
 90
 91    literal_key: str, default '!'
 92        The string to force an literal interpretation of a value.
 93        When the string is isolated, a literal interpreation is assumed and the surrounding
 94        quotes are replaced.
 95        
 96        E.g. Suppose a:b:c produces a dictionary {'d': 1}.
 97        - 'MRSM{a:b:c}'    => {'d': 1}        : isolated
 98        - ' MRSM{a:b:c} '  => ' "{\'d\': 1}"' : not isolated
 99        - ' MRSM{!a:b:c} ' => ' {"d": 1}'     : literal
100
101    keep_symlinks: bool, default True
102        If True, include the symlinks under the top-level key '_symlinks' (never written to a file).
103        Defaults to True.
104        
105        Example:
106
107        ```
108        MRSM{meerschaum:connectors:main:host} => cf['meerschaum']['connectors']['main']['host']
109        ``` 
110
111    Returns
112    -------
113    The configuration dictionary with `MRSM{}` symlinks replaced with
114    the values from the current configuration.
115    """
116    _rust = _core.search_and_substitute_config
117    return _rust(
118        config,
119        leading_key=leading_key,
120        delimiter=delimiter,
121        begin_key=begin_key,
122        end_key=end_key,
123        literal_key=literal_key,
124        keep_symlinks=keep_symlinks,
125    )

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]:
146def get_possible_keys() -> List[str]:
147    """
148    Return a list of possible top-level keys.
149    """
150    import os
151    import meerschaum.config.paths as paths
152    from meerschaum.config._default import default_config
153    keys = set()
154    for key in default_config:
155        keys.add(key)
156    for filename in os.listdir(paths.CONFIG_DIR_PATH):
157        keys.add('.'.join(filename.split('.')[:-1]))
158    return sorted(list(keys))

Return a list of possible top-level keys.

def get_keyfile_path( key: str, create_new: bool = False, directory: pathlib.Path | str | None = None) -> pathlib.Path | None:
161def get_keyfile_path(
162    key: str,
163    create_new: bool = False,
164    directory: Union[pathlib.Path, str, None] = None,
165) -> Union[pathlib.Path, None]:
166    """Determine a key's file path."""
167    _rust = _core.get_keyfile_path
168    return _rust(key, create_new=create_new, directory=directory)

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: pathlib.Path | str | None = None, keys: List[str] | None = None, write_missing: bool = True, substitute: bool = True, with_filenames: bool = False, raise_parsing_errors: bool = False) -> 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    _rust = _core.read_config
55    return _rust(
56        directory=directory,
57        keys=keys,
58        write_missing=write_missing,
59        substitute=substitute,
60        with_filenames=with_filenames,
61        raise_parsing_errors=raise_parsing_errors,
62    )

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', 'mcp': '/mcp', 'webterm': '/webterm/{session_id}', 'webterm_websocket': '/websocket/{session_id}', 'info': '/info', 'healthcheck': '/healthcheck', 'docs': '/docs', 'redoc': '/redoc', 'openapi': '/openapi.json'}, 'oauth': {'token_expires_minutes': 720}, 'webterm_job_name': '_webterm', 'default_timeout': 600, 'jobs': {'stdin_message': 'MRSM_STDIN', 'stop_message': 'MRSM_STOP', 'metadata_cache_seconds': 5, 'temp_prefix': '.api-temp-'}}, 'sql': {'internal_schema': '_mrsm_internal', 'instance_schema': 'mrsm', 'default_create_engine_args': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'create_engine_flavors': {'timescaledb': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'timescaledb-ha': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'postgresql': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'postgis': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'citus': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 5432}}, 'mssql': {'engine': 'mssql+pyodbc', 'create_engine': {'fast_executemany': True, 'use_insertmanyvalues': False, 'isolation_level': 'AUTOCOMMIT', 'use_setinputsizes': False, 'pool_pre_ping': True, 'ignore_no_transaction_on_rollback': True}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 1433, 'options': 'driver=ODBC Driver 18 for SQL Server&UseFMTONLY=Yes&TrustServerCertificate=yes&Encrypt=no&MARS_Connection=yes'}}, 'mysql': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 3306}}, 'mariadb': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 3306}}, 'oracle': {'engine': 'oracle+oracledb', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'password', 'username', 'database', 'host'}, 'defaults': {'port': 1521}}, 'sqlite': {'engine': 'sqlite', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database'}, 'defaults': {}}, 'geopackage': {'engine': 'sqlite', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database'}, 'defaults': {}}, 'duckdb': {'engine': 'duckdb', 'create_engine': {}, 'omit_create_engine': {'ALL'}, 'to_sql': {'method': 'multi'}, 'requirements': '', 'defaults': {}}, 'cockroachdb': {'engine': 'cockroachdb', 'omit_create_engine': {'method'}, 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'to_sql': {'method': 'multi'}, 'requirements': {'host'}, 'defaults': {'port': 26257, 'database': 'defaultdb', 'username': 'root', 'password': 'admin'}}}}, 'valkey': {'colon': '-_'}, 'environment': {'config': 'MRSM_CONFIG', 'config_dir': 'MRSM_CONFIG_DIR', 'patch': 'MRSM_PATCH', 'root': 'MRSM_ROOT_DIR', 'plugins': 'MRSM_PLUGINS_DIR', 'venvs': 'MRSM_VENVS_DIR', 'runtime': 'MRSM_RUNTIME', 'work_dir': 'MRSM_WORK_DIR', 'user': 'MRSM_USER', 'dep_group': 'MRSM_DEP_GROUP', 'home': 'MRSM_HOME', 'src': 'MRSM_SRC', 'uid': 'MRSM_UID', 'gid': 'MRSM_GID', 'noask': 'MRSM_NOASK', 'noninteractive': 'MRSM_NONINTERACTIVE', 'no_auto_install': 'MRSM_NO_AUTO_INSTALL', 'dataframe': 'MRSM_DATAFRAME', 'id': 'MRSM_SERVER_ID', 'test_flavors': 'MRSM_TEST_FLAVORS', 'daemon_id': 'MRSM_DAEMON_ID', 'compose_project': 'MRSM__COMPOSE_PROJECT', 'compose_root': 'MRSM__COMPOSE_ROOT', '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, 'origins_filename': '.mrsm_origins.json'}, '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.', 'sql:read': 'Execute read-only SQL queries against SQL 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).', 'pipes:register': 'Register new pipes.', 'pipes:define_sql': "Set or change pipes' SQL definitions and the fetch parameters of `sql:` connectors.", 'webterm:use': 'Use the web terminal.'}}}