meerschaum.connectors

Create connectors with meerschaum.connectors.get_connector(). For ease of use, you can also import from the root meerschaum module:

>>> from meerschaum import get_connector
>>> conn = get_connector()
  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3# vim:fenc=utf-8
  4
  5"""
  6Create connectors with `meerschaum.connectors.get_connector()`.
  7For ease of use, you can also import from the root `meerschaum` module:
  8```
  9>>> from meerschaum import get_connector
 10>>> conn = get_connector()
 11```
 12"""
 13
 14from __future__ import annotations
 15
 16import meerschaum as mrsm
 17from meerschaum.utils.typing import Any, Union, List, Dict, Optional
 18from meerschaum.utils.threading import RLock
 19from meerschaum.utils.warnings import warn
 20
 21from meerschaum.connectors._Connector import Connector, InvalidAttributesError
 22from meerschaum.connectors.instance._InstanceConnector import InstanceConnector
 23from meerschaum.connectors.sql._SQLConnector import SQLConnector
 24from meerschaum.connectors.api._APIConnector import APIConnector
 25
 26__all__ = (
 27    "make_connector",
 28    "Connector",
 29    "InstanceConnector",
 30    "SQLConnector",
 31    "APIConnector",
 32    "get_connector",
 33    "is_connected",
 34    "allow_protected",
 35    "poll",
 36    "api",
 37    "sql",
 38    "valkey",
 39    "parse",
 40)
 41
 42### store connectors partitioned by
 43### type, label for reuse
 44connectors: Dict[str, Dict[str, Connector]] = {
 45    'api'    : {},
 46    'sql'    : {},
 47    'plugin' : {},
 48    'valkey' : {},
 49}
 50instance_types: List[str] = ['sql', 'api']
 51_locks: Dict[str, RLock] = {
 52    'connectors'               : RLock(),
 53    'types'                    : RLock(),
 54    'custom_types'             : RLock(),
 55    'plugins_types'            : RLock(),
 56    '_loaded_plugin_connectors': RLock(),
 57    'instance_types'           : RLock(),
 58    'native_types'             : RLock(),
 59}
 60
 61### Fill this with objects only when connectors are first referenced.
 62types: Dict[str, Any] = {}
 63custom_types: set = set()
 64plugins_types: Dict[str, List[str]] = {}
 65_known_custom_types: set = set()
 66_loaded_plugin_connectors: bool = False
 67
 68### Connector types declared by native (Rust) plugins: `{type: info}` from the core, read on the
 69### first lookup of a type Python does not know, and the plugin of each type registered here.
 70_native_types: Optional[Dict[str, Dict[str, Any]]] = None
 71_native_types_refresh: bool = False
 72_native_type_plugins: Dict[str, str] = {}
 73
 74
 75def get_connector(
 76    type: str = None,
 77    label: str = None,
 78    refresh: bool = False,
 79    debug: bool = False,
 80    _load_plugins: bool = True,
 81    _warn_undefined: bool = True,
 82    **kw: Any
 83) -> Connector:
 84    """
 85    Return existing connector or create new connection and store for reuse.
 86    
 87    You can create new connectors if enough parameters are provided for the given type and flavor.
 88
 89    Parameters
 90    ----------
 91    type: Optional[str], default None
 92        Connector type (sql, api, etc.).
 93        Defaults to the type of the configured `instance_connector`.
 94
 95    label: Optional[str], default None
 96        Connector label (e.g. main). Defaults to `'main'`.
 97
 98    refresh: bool, default False
 99        Refresh the Connector instance / construct new object. Defaults to `False`.
100
101    kw: Any
102        Other arguments to pass to the Connector constructor.
103        If the Connector has already been constructed and new arguments are provided,
104        `refresh` is set to `True` and the old Connector is replaced.
105
106    Returns
107    -------
108    A new Meerschaum connector (e.g. `meerschaum.connectors.api.APIConnector`,
109    `meerschaum.connectors.sql.SQLConnector`).
110    
111    Examples
112    --------
113    The following parameters would create a new
114    `meerschaum.connectors.sql.SQLConnector` that isn't in the configuration file.
115
116    ```
117    >>> conn = get_connector(
118    ...     type = 'sql',
119    ...     label = 'newlabel',
120    ...     flavor = 'sqlite',
121    ...     database = '/file/path/to/database.db'
122    ... )
123    >>>
124    ```
125
126    """
127    from meerschaum.connectors.parse import parse_instance_keys
128    from meerschaum.config import get_config
129    from meerschaum._internal.static import STATIC_CONFIG
130    from meerschaum.utils.warnings import warn
131    global _loaded_plugin_connectors
132    if isinstance(type, str) and not label and ':' in type:
133        type, label = type.split(':', maxsplit=1)
134
135    if _load_plugins:
136        with _locks['_loaded_plugin_connectors']:
137            if not _loaded_plugin_connectors:
138                load_plugin_connectors()
139                _load_builtin_custom_connectors()
140                _loaded_plugin_connectors = True
141
142    if type is None and label is None:
143        default_instance_keys = get_config('meerschaum', 'instance', patch=True)
144        ### recursive call to get_connector
145        return parse_instance_keys(default_instance_keys)
146
147    ### NOTE: the default instance connector may not be main.
148    ### Only fall back to 'main' if the type is provided by the label is omitted.
149    label = label if label is not None else STATIC_CONFIG['connectors']['default_label']
150
151    ### A type declared by a native (Rust) plugin is registered on first use.
152    if type not in connectors and _load_plugins and isinstance(type, str):
153        _load_native_connector_type(type)
154
155    ### type might actually be a label. Check if so and raise a warning.
156    if type not in connectors:
157        possibilities, poss_msg = [], ""
158        for _type in get_config('meerschaum', 'connectors'):
159            if type in get_config('meerschaum', 'connectors', _type):
160                possibilities.append(f"{_type}:{type}")
161        if len(possibilities) > 0:
162            poss_msg = " Did you mean"
163            for poss in possibilities[:-1]:
164                poss_msg += f" '{poss}',"
165            if poss_msg.endswith(','):
166                poss_msg = poss_msg[:-1]
167            if len(possibilities) > 1:
168                poss_msg += " or"
169            poss_msg += f" '{possibilities[-1]}'?"
170
171        warn(f"Cannot create Connector of type '{type}'." + poss_msg, stack=False)
172        return None
173
174    if 'sql' not in types:
175        from meerschaum.connectors.plugin import PluginConnector
176        from meerschaum.connectors.valkey import ValkeyConnector
177        with _locks['types']:
178            types.update({
179                'api': APIConnector,
180                'sql': SQLConnector,
181                'plugin': PluginConnector,
182                'valkey': ValkeyConnector,
183            })
184
185    ### determine if we need to call the constructor
186    if not refresh:
187        ### see if any user-supplied arguments differ from the existing instance
188        if label in connectors[type]:
189            warning_message = None
190            for attribute, value in kw.items():
191                if attribute not in connectors[type][label].meta:
192                    import inspect
193                    cls = connectors[type][label].__class__
194                    cls_init_signature = inspect.signature(cls)
195                    cls_init_params = cls_init_signature.parameters
196                    if attribute not in cls_init_params:
197                        warning_message = (
198                            f"Received new attribute '{attribute}' not present in connector " +
199                            f"{connectors[type][label]}.\n"
200                        )
201                elif connectors[type][label].__dict__[attribute] != value:
202                    warning_message = (
203                        f"Mismatched values for attribute '{attribute}' in connector "
204                        + f"'{connectors[type][label]}'.\n" +
205                        f"  - Keyword value: '{value}'\n" +
206                        f"  - Existing value: '{connectors[type][label].__dict__[attribute]}'\n"
207                    )
208            if warning_message is not None:
209                warning_message += (
210                    "\nSetting `refresh` to True and recreating connector with type:"
211                    + f" '{type}' and label '{label}'."
212                )
213                refresh = True
214                warn(warning_message)
215        else: ### connector doesn't yet exist
216            refresh = True
217
218    ### only create an object if refresh is True
219    ### (can be manually specified, otherwise determined above)
220    if refresh:
221        ### A connector defined nowhere: say where connectors come from (not "Missing flavor").
222        undefined = _undefined_connector_message(type, label, kw)
223        if undefined is not None:
224            if _warn_undefined:
225                warn(undefined, stack=False)
226            return None
227        with _locks['connectors']:
228            try:
229                ### will raise an error if configuration is incorrect / missing
230                conn = types[type](label=label, **kw)
231                connectors[type][label] = conn
232            except InvalidAttributesError as ie:
233                warn(
234                    f"Incorrect attributes for connector '{type}:{label}'.\n"
235                    + str(ie),
236                    stack = False,
237                )
238                conn = None
239            except Exception as e:
240                from meerschaum.utils.formatting import print_exception
241                print_exception()
242                warn(
243                    f"Exception when creating connector '{type}:{label}'.\n" + str(e),
244                    stack = False,
245                )
246                conn = None
247        if conn is None:
248            return None
249
250    return connectors[type][label]
251
252
253def _undefined_connector_message(
254    type: Optional[str],
255    label: Optional[str],
256    attributes: Optional[Dict[str, Any]] = None,
257) -> Optional[str]:
258    """
259    The message for a `sql:`, `api:` or `valkey:` connector defined nowhere: no attributes were
260    passed and the config has no section for it (which includes the `MRSM_<TYPE>_<LABEL>`
261    environment connectors). `None` when the connector is defined. The text is the CLI's
262    (`undefined_connector_message` in `crates/meerschaum-cli/src/native/connectors.rs`).
263    """
264    from meerschaum.config import get_config
265    if type not in ('sql', 'api', 'valkey') or not label or attributes:
266        return None
267    if get_config('meerschaum', 'connectors', type, label, warn=False):
268        return None
269    return (
270        f"Connector '{type}:{label}' is not defined. Add it to the config "
271        f"(`mrsm bootstrap connectors`), set `MRSM_{type.upper()}_{label.upper()}`, "
272        "or install the plugin that provides it."
273    )
274
275
276class allow_protected:
277    """
278    Allow writes to protected connectors (`protected: true`) inside a `with` block.
279
280    Every mutation the core makes through a protected connector (sync, clear, drop, delete,
281    register, edit, `verify schema`, `SQLConnector.exec()` / `to_sql()`) is refused unless writes
282    are allowed: by `--allow-protected` on the command line, by `MRSM_ALLOW_PROTECTED=1` in the
283    environment, or inside this context manager. The allowance is process-wide while the block
284    runs (other threads see it too); child processes need `MRSM_ALLOW_PROTECTED=1`.
285
286    Examples
287    --------
288    >>> import meerschaum as mrsm
289    >>> from meerschaum.connectors import allow_protected
290    >>> pipe = mrsm.Pipe('plugin:noaa', 'weather', instance='sql:prod')
291    >>> with allow_protected():
292    ...     success, msg = pipe.sync(df)
293    """
294
295    def __enter__(self) -> 'allow_protected':
296        from meerschaum import _core
297        _core.allow_protected_enter()
298        return self
299
300    def __exit__(self, *exc: Any) -> None:
301        from meerschaum import _core
302        _core.allow_protected_exit()
303
304
305def is_protected_value(value: Any) -> bool:
306    """
307    Whether a connector attribute value marks the connector protected
308    (`True`, a non-zero number, or a string like `'true'`, `'1'`, `'yes'`).
309    """
310    if isinstance(value, bool):
311        return value
312    if isinstance(value, (int, float)):
313        return value != 0
314    if isinstance(value, str):
315        return value.strip().lower() in ('1', 'true', 'yes', 'on', 'y', 't')
316    return False
317
318
319def uri_marks_protected(uri: Any) -> bool:
320    """
321    Whether a connector URI carries `?protected=true` (any truthy value), which marks the
322    connector protected, e.g. `MRSM_VALKEY_PROD='valkey://host:6379/0?protected=true'`.
323    """
324    if not isinstance(uri, str) or '?' not in uri:
325        return False
326    from meerschaum import _core
327    return _core.uri_marks_protected(uri)
328
329
330def protected_refusal(connector: Any, op: str, pipe: Any = None) -> Optional[str]:
331    """
332    The refusal message for a write `op` through `connector` when it is protected and writes
333    are not allowed, else `None`. Meerschaum's own internal pipes (connector keys `mrsm`) are
334    never refused.
335    """
336    if pipe is not None and str(getattr(pipe, 'connector_keys', '')) == 'mrsm':
337        return None
338    if not is_protected_value(getattr(connector, 'protected', False)):
339        return None
340    from meerschaum import _core
341    if _core.protected_allowed():
342        return None
343    what = f"{op} {pipe}" if pipe is not None else op
344    return _core.protected_refusal_message(what, str(connector))
345
346
347def is_connected(keys: str, **kw) -> bool:
348    """
349    Check if the connector keys correspond to an active connection.
350    If the connector has not been created, it will immediately return `False`.
351    If the connector exists but cannot communicate with the source, return `False`.
352    
353    **NOTE:** Only works with instance connectors (`SQLConnectors` and `APIConnectors`).
354    Keyword arguments are passed to `meerschaum.connectors.poll.retry_connect`.
355
356    Parameters
357    ----------
358    keys:
359        The keys to the connector (e.g. `'sql:main'`).
360        
361    Returns
362    -------
363    A `bool` corresponding to whether a successful connection may be made.
364
365    """
366    import warnings
367    if ':' not in keys:
368        warn(f"Invalid connector keys '{keys}'")
369
370    try:
371        typ, label = keys.split(':')
372    except Exception:
373        return False
374    if typ not in instance_types:
375        return False
376    if label not in connectors.get(typ, {}):
377        return False
378
379    from meerschaum.connectors.parse import parse_instance_keys
380    conn = parse_instance_keys(keys)
381    try:
382        with warnings.catch_warnings():
383            warnings.filterwarnings('ignore')
384            return conn.test_connection(**kw)
385    except Exception:
386        return False
387
388
389def make_connector(cls, _is_executor: bool = False):
390    """
391    Register a class as a `Connector`.
392    The `type` will be the lower case of the class name, without the suffix `connector`.
393
394    Parameters
395    ----------
396    instance: bool, default False
397        If `True`, make this connector type an instance connector.
398        This requires implementing the various pipes functions and lots of testing.
399
400    Examples
401    --------
402    >>> import meerschaum as mrsm
403    >>> from meerschaum.connectors import make_connector, Connector
404    >>> 
405    >>> @make_connector
406    >>> class FooConnector(Connector):
407    ...     REQUIRED_ATTRIBUTES: list[str] = ['username', 'password']
408    ... 
409    >>> conn = mrsm.get_connector('foo:bar', username='dog', password='cat')
410    >>> print(conn.username, conn.password)
411    dog cat
412    >>> 
413    """
414    import re
415    from meerschaum.plugins import _get_parent_plugin
416    suffix_regex = (
417        r'connector$'
418        if not _is_executor
419        else r'executor$'
420    )
421    plugin_name = _get_parent_plugin(2)
422    typ = re.sub(suffix_regex, '', cls.__name__.lower())
423    with _locks['types']:
424        types[typ] = cls
425    with _locks['custom_types']:
426        custom_types.add(typ)
427    if plugin_name:
428        with _locks['plugins_types']:
429            if plugin_name not in plugins_types:
430                plugins_types[plugin_name] = []
431            plugins_types[plugin_name].append(typ)
432    with _locks['connectors']:
433        if typ not in connectors:
434            connectors[typ] = {}
435    if getattr(cls, 'IS_INSTANCE', False):
436        with _locks['instance_types']:
437            if typ not in instance_types:
438                instance_types.append(typ)
439
440    return cls
441
442
443def load_plugin_connectors():
444    """
445    If a plugin makes use of the `make_connector` decorator,
446    load its module.
447    """
448    import os
449    from meerschaum.plugins import get_plugins, import_plugins
450    to_import = []
451    for plugin in get_plugins():
452        if plugin is None:
453            continue
454
455        plugin_file = plugin.__file__
456        if not plugin_file:
457            continue
458
459        files_to_scan = []
460        plugin_dir = os.path.dirname(plugin_file)
461        if os.path.basename(plugin_file) == '__init__.py':
462            for root, _, files in os.walk(plugin_dir):
463                for fname in files:
464                    if fname.endswith('.py'):
465                        files_to_scan.append(os.path.join(root, fname))
466        else:
467            files_to_scan.append(plugin_file)
468
469        found = False
470        for fpath in files_to_scan:
471            try:
472                with open(fpath, encoding='utf-8') as f:
473                    text = f.read()
474            except (OSError, UnicodeDecodeError):
475                continue
476            if 'make_connector' in text or 'Connector' in text:
477                found = True
478                break
479
480        if found:
481            to_import.append(plugin.name)
482
483    if not to_import:
484        return
485
486    import_plugins(*to_import)
487
488
489def unload_plugin_connectors(
490    plugin_names: Optional[List[str]] = None,
491    debug: bool = False,
492) -> None:
493    """
494    Unload custom connectors added by plugins.
495    """
496    from meerschaum.plugins import get_plugins_names
497    global custom_types, _known_custom_types, types, plugins_types, connectors, _loaded_plugin_connectors
498
499    plugin_names = plugin_names or get_plugins_names()
500
501    for plugin_name in plugin_names:
502        plugin_types = plugins_types.get(plugin_name, [])
503        for typ in plugin_types:
504            _ = types.pop(typ, None)
505            _ = connectors.pop(typ, None)
506            if typ in instance_types:
507                instance_types.remove(typ)
508
509    custom_types.clear()
510    custom_types.update(_known_custom_types)
511    _loaded_plugin_connectors = False
512    _unload_native_connector_types()
513
514
515def get_connector_plugin(
516    connector: Connector,
517) -> Union[str, None, mrsm.Plugin]:
518    """
519    Determine the plugin for a connector.
520    This is useful for handling virtual environments for custom instance connectors.
521
522    Parameters
523    ----------
524    connector: Connector
525        The connector which may require a virtual environment.
526
527    Returns
528    -------
529    A Plugin, 'mrsm', or None.
530    """
531    if not hasattr(connector, 'type'):
532        return None
533    ### A native plugin has no virtual environment.
534    if getattr(connector, 'IS_NATIVE', False):
535        return None
536    plugin_name = (
537        connector.__module__.replace('plugins.', '').split('.')[0]
538        if connector.type in custom_types else (
539            connector.label
540            if connector.type == 'plugin'
541            else 'mrsm'
542        )
543    )
544    plugin = mrsm.Plugin(plugin_name)
545    return plugin if plugin.is_installed() else None
546
547
548def _load_builtin_custom_connectors():
549    """
550    Import custom connectors decorated with `@make_connector` or `@make_executor`.
551    """
552    import meerschaum.jobs.systemd
553    import meerschaum.connectors.valkey
554    _known_custom_types.add('valkey')
555    _known_custom_types.add('systemd')
556
557
558def get_native_connector_types(refresh: bool = False) -> Dict[str, Dict[str, Any]]:
559    """
560    Return the connector types declared by native (Rust) plugins in the plugins directories,
561    as `{type: {'plugin', 'is_instance', 'is_streaming', 'is_thread_safe', 'methods'}}`.
562    Read once per process (like Python plugins' connector types) until
563    `unload_plugin_connectors()` or `refresh=True`.
564    """
565    global _native_types, _native_types_refresh
566    with _locks['native_types']:
567        if _native_types is not None and not refresh:
568            return _native_types
569        from meerschaum import _core
570        try:
571            _native_types = {
572                str(typ): dict(info)
573                for typ, info in _core.native_connector_types(
574                    refresh or _native_types_refresh
575                ).items()
576            }
577        except Exception as e:
578            from meerschaum.utils.debug import dprint
579            dprint(f"Could not read the native plugins' connector types: {e}")
580            _native_types = {}
581        _native_types_refresh = False
582        return _native_types
583
584
585def _load_native_connector_type(typ: str) -> bool:
586    """
587    Register the native connector type `typ` (a `NativeConnector` class) if a native plugin
588    declares it. Return whether `typ` is now a known type.
589    """
590    info = get_native_connector_types().get(typ, None)
591    if not info:
592        return False
593    from meerschaum.connectors.native import make_native_connector_class
594    cls = make_native_connector_class(typ, info)
595    plugin_name = str(info.get('plugin', ''))
596    with _locks['types']:
597        if typ in types:
598            return True
599        types[typ] = cls
600    with _locks['custom_types']:
601        custom_types.add(typ)
602    with _locks['plugins_types']:
603        plugins_types.setdefault(plugin_name, []).append(typ)
604    _native_type_plugins[typ] = plugin_name
605    with _locks['connectors']:
606        if typ not in connectors:
607            connectors[typ] = {}
608    if getattr(cls, 'IS_INSTANCE', False):
609        with _locks['instance_types']:
610            if typ not in instance_types:
611                instance_types.append(typ)
612    return True
613
614
615def _unload_native_connector_types() -> None:
616    """
617    Forget the native connector types (re-read from the core on the next unknown type).
618    """
619    global _native_types, _native_types_refresh
620    with _locks['native_types']:
621        for typ, plugin_name in list(_native_type_plugins.items()):
622            _ = types.pop(typ, None)
623            _ = connectors.pop(typ, None)
624            custom_types.discard(typ)
625            if typ in instance_types:
626                instance_types.remove(typ)
627            plugin_types = plugins_types.get(plugin_name, [])
628            if typ in plugin_types:
629                plugin_types.remove(typ)
630            _ = _native_type_plugins.pop(typ, None)
631        _native_types = None
632        _native_types_refresh = True
def make_connector(cls, _is_executor: bool = False):
390def make_connector(cls, _is_executor: bool = False):
391    """
392    Register a class as a `Connector`.
393    The `type` will be the lower case of the class name, without the suffix `connector`.
394
395    Parameters
396    ----------
397    instance: bool, default False
398        If `True`, make this connector type an instance connector.
399        This requires implementing the various pipes functions and lots of testing.
400
401    Examples
402    --------
403    >>> import meerschaum as mrsm
404    >>> from meerschaum.connectors import make_connector, Connector
405    >>> 
406    >>> @make_connector
407    >>> class FooConnector(Connector):
408    ...     REQUIRED_ATTRIBUTES: list[str] = ['username', 'password']
409    ... 
410    >>> conn = mrsm.get_connector('foo:bar', username='dog', password='cat')
411    >>> print(conn.username, conn.password)
412    dog cat
413    >>> 
414    """
415    import re
416    from meerschaum.plugins import _get_parent_plugin
417    suffix_regex = (
418        r'connector$'
419        if not _is_executor
420        else r'executor$'
421    )
422    plugin_name = _get_parent_plugin(2)
423    typ = re.sub(suffix_regex, '', cls.__name__.lower())
424    with _locks['types']:
425        types[typ] = cls
426    with _locks['custom_types']:
427        custom_types.add(typ)
428    if plugin_name:
429        with _locks['plugins_types']:
430            if plugin_name not in plugins_types:
431                plugins_types[plugin_name] = []
432            plugins_types[plugin_name].append(typ)
433    with _locks['connectors']:
434        if typ not in connectors:
435            connectors[typ] = {}
436    if getattr(cls, 'IS_INSTANCE', False):
437        with _locks['instance_types']:
438            if typ not in instance_types:
439                instance_types.append(typ)
440
441    return cls

Register a class as a Connector. The type will be the lower case of the class name, without the suffix connector.

Parameters
  • instance (bool, default False): If True, make this connector type an instance connector. This requires implementing the various pipes functions and lots of testing.
Examples
>>> import meerschaum as mrsm
>>> from meerschaum.connectors import make_connector, Connector
>>> 
>>> @make_connector
>>> class FooConnector(Connector):
...     REQUIRED_ATTRIBUTES: list[str] = ['username', 'password']
... 
>>> conn = mrsm.get_connector('foo:bar', username='dog', password='cat')
>>> print(conn.username, conn.password)
dog cat
>>>
class Connector:
 22class Connector(metaclass=abc.ABCMeta):
 23    """
 24    The base connector class to hold connection attributes.
 25    """
 26
 27    IS_INSTANCE: bool = False
 28
 29    def __init__(
 30        self,
 31        type: Optional[str] = None,
 32        label: Optional[str] = None,
 33        **kw: Any
 34    ):
 35        """
 36        Set the given keyword arguments as attributes.
 37
 38        Parameters
 39        ----------
 40        type: str
 41            The `type` of the connector (e.g. `sql`, `api`, `plugin`).
 42
 43        label: str
 44            The `label` for the connector.
 45
 46
 47        Examples
 48        --------
 49        Run `mrsm edit config` and to edit connectors in the YAML file:
 50
 51        ```yaml
 52        meerschaum:
 53            connections:
 54                {type}:
 55                    {label}:
 56                        ### attributes go here
 57        ```
 58
 59        """
 60        self._original_dict = copy.deepcopy(self.__dict__)
 61        self._set_attributes(type=type, label=label, **kw)
 62
 63        ### NOTE: Override `REQUIRED_ATTRIBUTES` if `uri` is set.
 64        self.verify_attributes(
 65            ['uri']
 66            if 'uri' in self.__dict__
 67            else getattr(self, 'REQUIRED_ATTRIBUTES', None)
 68        )
 69
 70    def _reset_attributes(self):
 71        self.__dict__ = self._original_dict
 72
 73    def _set_attributes(
 74        self,
 75        *args,
 76        inherit_default: bool = True,
 77        **kw: Any
 78    ):
 79        from meerschaum._internal.static import STATIC_CONFIG
 80        from meerschaum.utils.warnings import error
 81
 82        self._attributes = {}
 83
 84        default_label = STATIC_CONFIG['connectors']['default_label']
 85
 86        ### NOTE: Support the legacy method of explicitly passing the type.
 87        label = kw.get('label', None)
 88        if label is None:
 89            if len(args) == 2:
 90                label = args[1]
 91            elif len(args) == 0:
 92                label = None
 93            else:
 94                label = args[0]
 95
 96        if label == 'default':
 97            error(
 98                f"Label cannot be 'default'. Did you mean '{default_label}'?",
 99                InvalidAttributesError,
100            )
101        self.__dict__['label'] = label
102
103        from meerschaum.config import get_config
104        conn_configs = copy.deepcopy(get_config('meerschaum', 'connectors'))
105        connector_config = copy.deepcopy(get_config('system', 'connectors'))
106
107        ### inherit attributes from 'default' if exists
108        if inherit_default:
109            inherit_from = 'default'
110            if self.type in conn_configs and inherit_from in conn_configs[self.type]:
111                _inherit_dict = copy.deepcopy(conn_configs[self.type][inherit_from])
112                self._attributes.update(_inherit_dict)
113
114        ### load user config into self._attributes
115        if self.type in conn_configs and self.label in conn_configs[self.type]:
116            self._attributes.update(conn_configs[self.type][self.label] or {})
117
118        ### load system config into self._sys_config
119        ### (deep copy so future Connectors don't inherit changes)
120        if self.type in connector_config:
121            self._sys_config = copy.deepcopy(connector_config[self.type])
122
123        ### add additional arguments or override configuration
124        self._attributes.update(kw)
125
126        ### Remember what the caller passed: a connector built with its own attributes
127        ### (`get_connector('sql', 'x', flavor=...)`) overrides the configured `sql:x` in the
128        ### actions this process runs (`meerschaum._internal.entry.adhoc_context`), as in v4.
129        self.__dict__['_explicit_attributes'] = {
130            key: value
131            for key, value in kw.items()
132            if key not in ('type', 'label')
133        }
134
135        ### finally, update __dict__ with _attributes.
136        self.__dict__.update(self._attributes)
137
138    def verify_attributes(
139        self,
140        required_attributes: Optional[List[str]] = None,
141        debug: bool = False,
142    ) -> None:
143        """
144        Ensure that the required attributes have been met.
145        
146        The Connector base class checks the minimum requirements.
147        Child classes may enforce additional requirements.
148
149        Parameters
150        ----------
151        required_attributes: Optional[List[str]], default None
152            Attributes to be verified. If `None`, default to `['label']`.
153
154        debug: bool, default False
155            Verbosity toggle.
156
157        Returns
158        -------
159        Don't return anything.
160
161        Raises
162        ------
163        An error if any of the required attributes are missing.
164        """
165        from meerschaum.utils.warnings import error
166        from meerschaum.utils.misc import items_str
167        if required_attributes is None:
168            required_attributes = ['type', 'label']
169
170        missing_attributes = set()
171        for a in required_attributes:
172            if a not in self.__dict__:
173                missing_attributes.add(a)
174        if len(missing_attributes) > 0:
175            error(
176                (
177                    f"Missing {items_str(list(missing_attributes))} "
178                    + f"for connector '{self.type}:{self.label}'."
179                ),
180                InvalidAttributesError,
181                silent=True,
182                stack=False
183            )
184
185
186    def __str__(self):
187        """
188        When cast to a string, return type:label.
189        """
190        return f"{self.type}:{self.label}"
191
192    def __repr__(self):
193        """
194        Represent the connector as type:label.
195        """
196        return str(self)
197
198    @property
199    def meta(self) -> Dict[str, Any]:
200        """
201        Return the keys needed to reconstruct this Connector.
202        """
203        _meta = {
204            key: value
205            for key, value in self.__dict__.items()
206            if not str(key).startswith('_')
207        }
208        _meta.update({
209            'type': self.type,
210            'label': self.label,
211        })
212        return _meta
213
214
215    @property
216    def protected(self) -> bool:
217        """
218        Whether the connector is protected (`protected: true` in its attributes, or
219        `?protected=true` on its URI): every write through it is refused unless writes to
220        protected connectors are allowed (`--allow-protected`, `MRSM_ALLOW_PROTECTED=1`, or
221        `meerschaum.connectors.allow_protected()`). Reads are never blocked.
222        """
223        from meerschaum.connectors import is_protected_value, uri_marks_protected
224        return (
225            is_protected_value(self.__dict__.get('protected', False))
226            or uri_marks_protected(self.__dict__.get('uri', None))
227        )
228
229    @protected.setter
230    def protected(self, value: Any) -> None:
231        self.__dict__['protected'] = value
232
233    @property
234    def type(self) -> str:
235        """
236        Return the type for this connector.
237        """
238        _type = self.__dict__.get('type', None)
239        if _type is None:
240            import re
241            is_executor = self.__class__.__name__.lower().endswith('executor')
242            suffix_regex = (
243                r'connector$'
244                if not is_executor
245                else r'executor$'
246            )
247            _type = re.sub(suffix_regex, '', self.__class__.__name__.lower())
248            if not _type or _type.lower() == 'instance':
249                raise ValueError("No type could be determined for this connector.")
250            self.__dict__['type'] = _type
251        return _type
252
253
254    @property
255    def label(self) -> str:
256        """
257        Return the label for this connector.
258        """
259        _label = self.__dict__.get('label', None)
260        if _label is None:
261            from meerschaum._internal.static import STATIC_CONFIG
262            _label = STATIC_CONFIG['connectors']['default_label']
263            self.__dict__['label'] = _label
264        return _label

The base connector class to hold connection attributes.

Connector(type: str | None = None, label: str | None = None, **kw: Any)
29    def __init__(
30        self,
31        type: Optional[str] = None,
32        label: Optional[str] = None,
33        **kw: Any
34    ):
35        """
36        Set the given keyword arguments as attributes.
37
38        Parameters
39        ----------
40        type: str
41            The `type` of the connector (e.g. `sql`, `api`, `plugin`).
42
43        label: str
44            The `label` for the connector.
45
46
47        Examples
48        --------
49        Run `mrsm edit config` and to edit connectors in the YAML file:
50
51        ```yaml
52        meerschaum:
53            connections:
54                {type}:
55                    {label}:
56                        ### attributes go here
57        ```
58
59        """
60        self._original_dict = copy.deepcopy(self.__dict__)
61        self._set_attributes(type=type, label=label, **kw)
62
63        ### NOTE: Override `REQUIRED_ATTRIBUTES` if `uri` is set.
64        self.verify_attributes(
65            ['uri']
66            if 'uri' in self.__dict__
67            else getattr(self, 'REQUIRED_ATTRIBUTES', None)
68        )

Set the given keyword arguments as attributes.

Parameters
  • type (str): The type of the connector (e.g. sql, api, plugin).
  • label (str): The label for the connector.
Examples

Run mrsm edit config and to edit connectors in the YAML file:

meerschaum:
    connections:
        {type}:
            {label}:
                ### attributes go here
IS_INSTANCE: bool = False
def verify_attributes( self, required_attributes: List[str] | None = None, debug: bool = False) -> None:
138    def verify_attributes(
139        self,
140        required_attributes: Optional[List[str]] = None,
141        debug: bool = False,
142    ) -> None:
143        """
144        Ensure that the required attributes have been met.
145        
146        The Connector base class checks the minimum requirements.
147        Child classes may enforce additional requirements.
148
149        Parameters
150        ----------
151        required_attributes: Optional[List[str]], default None
152            Attributes to be verified. If `None`, default to `['label']`.
153
154        debug: bool, default False
155            Verbosity toggle.
156
157        Returns
158        -------
159        Don't return anything.
160
161        Raises
162        ------
163        An error if any of the required attributes are missing.
164        """
165        from meerschaum.utils.warnings import error
166        from meerschaum.utils.misc import items_str
167        if required_attributes is None:
168            required_attributes = ['type', 'label']
169
170        missing_attributes = set()
171        for a in required_attributes:
172            if a not in self.__dict__:
173                missing_attributes.add(a)
174        if len(missing_attributes) > 0:
175            error(
176                (
177                    f"Missing {items_str(list(missing_attributes))} "
178                    + f"for connector '{self.type}:{self.label}'."
179                ),
180                InvalidAttributesError,
181                silent=True,
182                stack=False
183            )

Ensure that the required attributes have been met.

The Connector base class checks the minimum requirements. Child classes may enforce additional requirements.

Parameters
  • required_attributes (Optional[List[str]], default None): Attributes to be verified. If None, default to ['label'].
  • debug (bool, default False): Verbosity toggle.
Returns
  • Don't return anything.
Raises
  • An error if any of the required attributes are missing.
meta: Dict[str, Any]
198    @property
199    def meta(self) -> Dict[str, Any]:
200        """
201        Return the keys needed to reconstruct this Connector.
202        """
203        _meta = {
204            key: value
205            for key, value in self.__dict__.items()
206            if not str(key).startswith('_')
207        }
208        _meta.update({
209            'type': self.type,
210            'label': self.label,
211        })
212        return _meta

Return the keys needed to reconstruct this Connector.

protected: bool
215    @property
216    def protected(self) -> bool:
217        """
218        Whether the connector is protected (`protected: true` in its attributes, or
219        `?protected=true` on its URI): every write through it is refused unless writes to
220        protected connectors are allowed (`--allow-protected`, `MRSM_ALLOW_PROTECTED=1`, or
221        `meerschaum.connectors.allow_protected()`). Reads are never blocked.
222        """
223        from meerschaum.connectors import is_protected_value, uri_marks_protected
224        return (
225            is_protected_value(self.__dict__.get('protected', False))
226            or uri_marks_protected(self.__dict__.get('uri', None))
227        )

Whether the connector is protected (protected: true in its attributes, or ?protected=true on its URI): every write through it is refused unless writes to protected connectors are allowed (--allow-protected, MRSM_ALLOW_PROTECTED=1, or meerschaum.connectors.allow_protected()). Reads are never blocked.

type: str
233    @property
234    def type(self) -> str:
235        """
236        Return the type for this connector.
237        """
238        _type = self.__dict__.get('type', None)
239        if _type is None:
240            import re
241            is_executor = self.__class__.__name__.lower().endswith('executor')
242            suffix_regex = (
243                r'connector$'
244                if not is_executor
245                else r'executor$'
246            )
247            _type = re.sub(suffix_regex, '', self.__class__.__name__.lower())
248            if not _type or _type.lower() == 'instance':
249                raise ValueError("No type could be determined for this connector.")
250            self.__dict__['type'] = _type
251        return _type

Return the type for this connector.

label: str
254    @property
255    def label(self) -> str:
256        """
257        Return the label for this connector.
258        """
259        _label = self.__dict__.get('label', None)
260        if _label is None:
261            from meerschaum._internal.static import STATIC_CONFIG
262            _label = STATIC_CONFIG['connectors']['default_label']
263            self.__dict__['label'] = _label
264        return _label

Return the label for this connector.

class InstanceConnector(meerschaum.connectors.Connector):
 18class InstanceConnector(Connector):
 19    """
 20    Instance connectors define the interface for managing pipes and provide methods
 21    for management of users, plugins, tokens, and other metadata built atop pipes.
 22    """
 23
 24    IS_INSTANCE: bool = True
 25    IS_THREAD_SAFE: bool = False
 26
 27    from ._users import (
 28        get_users_pipe,
 29        register_user,
 30        get_user_id,
 31        get_username,
 32        get_users,
 33        edit_user,
 34        delete_user,
 35        get_user_password_hash,
 36        get_user_type,
 37        get_user_attributes,
 38    )
 39
 40    from ._plugins import (
 41        get_plugins_pipe,
 42        register_plugin,
 43        get_plugin_user_id,
 44        delete_plugin,
 45        get_plugin_id,
 46        get_plugin_version,
 47        get_plugins,
 48        get_plugin_user_id,
 49        get_plugin_username,
 50        get_plugin_attributes,
 51    )
 52
 53    from ._tokens import (
 54        get_tokens_pipe,
 55        register_token,
 56        edit_token,
 57        invalidate_token,
 58        delete_token,
 59        get_token,
 60        get_tokens,
 61        get_token_model,
 62        get_token_secret_hash,
 63        token_exists,
 64        get_token_scopes,
 65    )
 66
 67    from ._pipes import (
 68        register_pipe,
 69        get_pipe_attributes,
 70        get_pipe_id,
 71        edit_pipe,
 72        delete_pipe,
 73        fetch_pipes_keys,
 74        pipe_exists,
 75        drop_pipe,
 76        drop_pipe_indices,
 77        sync_pipe,
 78        create_pipe_indices,
 79        clear_pipe,
 80        get_pipe_data,
 81        get_pipe_docs,
 82        get_sync_time,
 83        get_pipe_columns_types,
 84        get_pipe_columns_indices,
 85        get_pipe_size,
 86        compress_pipe,
 87        decompress_pipe,
 88        vacuum_pipe,
 89        analyze_pipe,
 90        partition_pipe,
 91    )
 92
 93    ### Teams, pipe locks and pipe access (the access model, `meerschaum.core.access`).
 94    from ._teams import (
 95        get_teams,
 96        get_team,
 97        register_team,
 98        edit_team,
 99        delete_team,
100        rename_team,
101        get_user_teams,
102        get_team_pipes_count,
103        get_user_permissions,
104        get_user_role,
105        set_user_attributes,
106        lock_pipe,
107        unlock_pipe,
108        set_pipe_access,
109    )

Instance connectors define the interface for managing pipes and provide methods for management of users, plugins, tokens, and other metadata built atop pipes.

IS_INSTANCE: bool = True
IS_THREAD_SAFE: bool = False
def get_users_pipe(self) -> meerschaum.Pipe:
18def get_users_pipe(self) -> 'mrsm.Pipe':
19    """
20    Return the pipe used for users registration.
21    """
22    if '_users_pipe' in self.__dict__:
23        return self._users_pipe
24
25    cache_connector = self.__dict__.get('_cache_connector', None)
26    self._users_pipe = mrsm.Pipe(
27        'mrsm', 'users',
28        instance=self,
29        target='mrsm_users',
30        temporary=True,
31        cache=True,
32        cache_connector_keys=cache_connector,
33        static=True,
34        null_indices=False,
35        columns={
36            'primary': 'user_id',
37        },
38        dtypes={
39            'user_id': 'uuid',
40            'username': 'string',
41            'password_hash': 'string',
42            'email': 'string',
43            'user_type': 'string',
44            'attributes': 'json',
45        },
46        indices={
47            'unique': 'username',
48        },
49    )
50    return self._users_pipe

Return the pipe used for users registration.

def register_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
53def register_user(
54    self,
55    user: User,
56    debug: bool = False,
57    **kwargs: Any
58) -> mrsm.SuccessTuple:
59    """
60    Register a new user to the users pipe.
61    """
62    users_pipe = self.get_users_pipe()
63    user.user_id = uuid.uuid4()
64    sync_success, sync_msg = users_pipe.sync(
65        [{
66            'user_id': user.user_id,
67            'username': user.username,
68            'email': user.email,
69            'password_hash': user.password_hash,
70            'user_type': user.type,
71            'attributes': user.attributes,
72        }],
73        check_existing=False,
74        debug=debug,
75    )
76    if not sync_success:
77        return False, f"Failed to register user '{user.username}':\n{sync_msg}"
78
79    return True, "Success"

Register a new user to the users pipe.

def get_user_id( self, user: meerschaum.core.User._User.User, debug: bool = False) -> uuid.UUID | None:
142def get_user_id(self, user: User, debug: bool = False) -> Union[uuid.UUID, None]:
143    """
144    Return a user's ID from the username (matched exactly).
145    """
146    users_pipe = self.get_users_pipe()
147    row = _exact_row(users_pipe, 'username', user.username, ['user_id'], debug=debug)
148    return row.get('user_id', None) if row else None

Return a user's ID from the username (matched exactly).

def get_username(self, user_id: Any, debug: bool = False) -> Any:
151def get_username(self, user_id: Any, debug: bool = False) -> Any:
152    """
153    Return the username from the given ID (matched exactly).
154    """
155    users_pipe = self.get_users_pipe()
156    row = _exact_row(users_pipe, 'user_id', user_id, ['username'], debug=debug)
157    return row.get('username', None) if row else None

Return the username from the given ID (matched exactly).

def get_users(self, debug: bool = False, **kw: Any) -> List[str]:
172def get_users(
173    self,
174    debug: bool = False,
175    **kw: Any
176) -> List[str]:
177    """
178    Get the registered usernames.
179    """
180    users_pipe = self.get_users_pipe()
181    df = users_pipe.get_data()
182    if df is None:
183        return []
184
185    return list(df['username'])

Get the registered usernames.

def edit_user( self, user: meerschaum.core.User._User.User, debug: bool = False) -> Tuple[bool, str]:
188def edit_user(self, user: User, debug: bool = False) -> mrsm.SuccessTuple:
189    """
190    Edit the attributes for an existing user.
191    """
192    users_pipe = self.get_users_pipe()
193    user_id = _resolve_user_id(self, user, debug=debug)
194    if user_id is None:
195        return False, (
196            f"User '{user.username}' does not exist. "
197            f"Register user '{user.username}' before editing."
198        )
199
200    doc = {'user_id': user_id}
201    if user.email != '':
202        doc['email'] = user.email
203    if user.password_hash != '':
204        doc['password_hash'] = user.password_hash
205    if user.type != '':
206        doc['user_type'] = user.type
207    if user.attributes:
208        doc['attributes'] = user.attributes
209
210    sync_success, sync_msg = users_pipe.sync([doc], debug=debug)
211    if not sync_success:
212        return False, f"Failed to edit user '{user.username}':\n{sync_msg}"
213
214    return True, "Success"

Edit the attributes for an existing user.

def delete_user( self, user: meerschaum.core.User._User.User, debug: bool = False) -> Tuple[bool, str]:
217def delete_user(self, user: User, debug: bool = False) -> mrsm.SuccessTuple:
218    """
219    Delete a user from the users table.
220    """
221    user_id = _resolve_user_id(self, user, debug=debug)
222    if user_id is None:
223        return False, f"User '{user.username}' is not registered and cannot be deleted."
224    if not _literal_param_safe(str(user_id)):
225        return False, f"Cannot delete user '{user.username}': its ID cannot be matched exactly."
226    users_pipe = self.get_users_pipe()
227    clear_success, clear_msg = users_pipe.clear(params={'user_id': user_id}, debug=debug)
228    if not clear_success:
229        return False, f"Failed to delete user '{user}':\n{clear_msg}"
230    ### The user leaves every team; their pipes keep their team, without an owner.
231    try:
232        from meerschaum import _core
233        from meerschaum.connectors.instance._teams import _carrier
234        carrier, context = _carrier(self)
235        forgotten, forgotten_msg = _core.forget_user_access(
236            carrier, context, user.username, debug=debug,
237        )
238    except Exception as e:
239        forgotten, forgotten_msg = False, str(e)
240    if not forgotten:
241        from meerschaum.utils.warnings import warn
242        warn(forgotten_msg, stack=False)
243    return True, "Success"

Delete a user from the users table.

def get_user_password_hash( self, user: meerschaum.core.User._User.User, debug: bool = False) -> uuid.UUID | None:
246def get_user_password_hash(self, user: User, debug: bool = False) -> Union[uuid.UUID, None]:
247    """
248    Get a user's password hash from the users table.
249    """
250    return _user_field(self, user, 'password_hash', debug=debug)

Get a user's password hash from the users table.

def get_user_type( self, user: meerschaum.core.User._User.User, debug: bool = False) -> str | None:
253def get_user_type(self, user: User, debug: bool = False) -> Union[str, None]:
254    """
255    Get a user's type from the users table.
256    """
257    return _user_field(self, user, 'user_type', debug=debug)

Get a user's type from the users table.

def get_user_attributes( self, user: meerschaum.core.User._User.User, debug: bool = False) -> Dict[str, Any] | None:
260def get_user_attributes(self, user: User, debug: bool = False) -> Union[Dict[str, Any], None]:
261    """
262    Get a user's attributes from the users table.
263    """
264    return _user_field(self, user, 'attributes', debug=debug)

Get a user's attributes from the users table.

def get_plugins_pipe(self) -> meerschaum.Pipe:
16def get_plugins_pipe(self) -> 'mrsm.Pipe':
17    """
18    Return the internal pipe for syncing plugins metadata.
19    """
20    if '_plugins_pipe' in self.__dict__:
21        return self._plugins_pipe
22
23    cache_connector = self.__dict__.get('_cache_connector', None)
24    users_pipe = self.get_users_pipe()
25    user_id_dtype = users_pipe.dtypes.get('user_id', 'uuid')
26
27    self._plugins_pipe = mrsm.Pipe(
28        'mrsm', 'plugins',
29        instance=self,
30        target='mrsm_plugins',
31        temporary=True,
32        cache=True,
33        cache_connector_keys=cache_connector,
34        static=True,
35        null_indices=False,
36        columns={
37            'primary': 'plugin_name',
38            'user_id': 'user_id',
39        },
40        dtypes={
41            'plugin_name': 'string',
42            'user_id': user_id_dtype,
43            'attributes': 'json',
44            'version': 'string',
45        },
46    )
47    return self._plugins_pipe

Return the internal pipe for syncing plugins metadata.

def register_plugin( self, plugin: meerschaum.Plugin, debug: bool = False) -> Tuple[bool, str]:
50def register_plugin(self, plugin: Plugin, debug: bool = False) -> mrsm.SuccessTuple:
51    """
52    Register a new plugin to the plugins table.
53    """
54    plugins_pipe = self.get_plugins_pipe()
55    users_pipe = self.get_users_pipe()
56    user_id = self.get_plugin_user_id(plugin)
57    if user_id is not None:
58        username = self.get_username(user_id, debug=debug)
59        return False, f"{plugin} is already registered to '{username}'."
60
61    doc = {
62        'plugin_name': plugin.name,
63        'version': plugin.version,
64        'attributes': plugin.attributes,
65        'user_id': plugin.user_id,
66    }
67
68    sync_success, sync_msg = plugins_pipe.sync(
69        [doc],
70        check_existing=False,
71        debug=debug,
72    )
73    if not sync_success:
74        return False, f"Failed to register {plugin}:\n{sync_msg}"
75
76    return True, "Success"

Register a new plugin to the plugins table.

def get_plugin_user_id( self, plugin: meerschaum.Plugin, debug: bool = False) -> uuid.UUID | None:
79def get_plugin_user_id(self, plugin: Plugin, debug: bool = False) -> Union[uuid.UUID, None]:
80    """
81    Return the user ID for plugin's owner.
82    """
83    plugins_pipe = self.get_plugins_pipe() 
84    return plugins_pipe.get_value('user_id', {'plugin_name': plugin.name}, debug=debug)

Return the user ID for plugin's owner.

def delete_plugin( self, plugin: meerschaum.Plugin, debug: bool = False) -> Tuple[bool, str]:
105def delete_plugin(self, plugin: Plugin, debug: bool = False) -> mrsm.SuccessTuple:
106    """
107    Delete a plugin's registration.
108    """
109    plugin_id = self.get_plugin_id(plugin, debug=debug)
110    if plugin_id is None:
111        return False, f"{plugin} is not registered."
112    
113    plugins_pipe = self.get_plugins_pipe()
114    clear_success, clear_msg = plugins_pipe.clear(params={'plugin_name': plugin.name}, debug=debug)
115    if not clear_success:
116        return False, f"Failed to delete {plugin}:\n{clear_msg}"
117    return True, "Success"

Delete a plugin's registration.

def get_plugin_id( self, plugin: meerschaum.Plugin, debug: bool = False) -> str | None:
 97def get_plugin_id(self, plugin: Plugin, debug: bool = False) -> Union[str, None]:
 98    """
 99    Return a plugin's ID.
100    """
101    user_id = self.get_plugin_user_id(plugin, debug=debug)
102    return plugin.name if user_id is not None else None

Return a plugin's ID.

def get_plugin_version( self, plugin: meerschaum.Plugin, debug: bool = False) -> str | None:
120def get_plugin_version(self, plugin: Plugin, debug: bool = False) -> Union[str, None]:
121    """
122    Return the version for a plugin.
123    """
124    plugins_pipe = self.get_plugins_pipe() 
125    return plugins_pipe.get_value('version', {'plugin_name': plugin.name}, debug=debug)

Return the version for a plugin.

def get_plugins( self, user_id: int | None = None, search_term: str | None = None, debug: bool = False, **kw: Any) -> List[str]:
136def get_plugins(
137    self,
138    user_id: Optional[int] = None,
139    search_term: Optional[str] = None,
140    debug: bool = False,
141    **kw: Any
142) -> List[str]:
143    """
144    Return a list of plugin names.
145    """
146    plugins_pipe = self.get_plugins_pipe()
147    params = {}
148    if user_id:
149        params['user_id'] = user_id
150
151    docs = plugins_pipe.get_docs(select_columns=['plugin_name'], params=params, debug=debug)
152    if not docs:
153        return []
154
155    return [
156        plugin_name
157        for doc in docs
158        if (plugin_name := doc['plugin_name']).startswith(search_term or '')
159    ]

Return a list of plugin names.

def get_plugin_username( self, plugin: meerschaum.Plugin, debug: bool = False) -> uuid.UUID | None:
87def get_plugin_username(self, plugin: Plugin, debug: bool = False) -> Union[uuid.UUID, None]:
88    """
89    Return the username for plugin's owner.
90    """
91    user_id = self.get_plugin_user_id(plugin, debug=debug)
92    if user_id is None:
93        return None
94    return self.get_username(user_id, debug=debug)

Return the username for plugin's owner.

def get_plugin_attributes( self, plugin: meerschaum.Plugin, debug: bool = False) -> Dict[str, Any]:
128def get_plugin_attributes(self, plugin: Plugin, debug: bool = False) -> Dict[str, Any]:
129    """
130    Return the attributes for a plugin.
131    """
132    plugins_pipe = self.get_plugins_pipe() 
133    return plugins_pipe.get_value('attributes', {'plugin_name': plugin.name}, debug=debug) or {}

Return the attributes for a plugin.

def get_tokens_pipe(self) -> meerschaum.Pipe:
52def get_tokens_pipe(self) -> mrsm.Pipe:
53    """
54    Return the internal pipe for tokens management.
55    """
56    if '_tokens_pipe' in self.__dict__:
57        return self._tokens_pipe
58
59    users_pipe = self.get_users_pipe()
60    user_id_dtype = (
61        users_pipe._attributes.get('parameters', {}).get('dtypes', {}).get('user_id', 'uuid')
62    )
63
64    cache_connector = self.__dict__.get('_cache_connector', None)
65
66    self._tokens_pipe = mrsm.Pipe(
67        'mrsm', 'tokens',
68        instance=self,
69        target='mrsm_tokens',
70        temporary=True,
71        cache=True,
72        cache_connector_keys=cache_connector,
73        static=True,
74        autotime=True,
75        null_indices=False,
76        columns={
77            'datetime': 'creation',
78            'primary': 'id',
79        },
80        indices={
81            'unique': 'label',
82            'user_id': 'user_id',
83        },
84        dtypes={
85            'id': 'uuid',
86            'creation': 'datetime',
87            'expiration': 'datetime',
88            'is_valid': 'bool',
89            'label': 'string',
90            'user_id': user_id_dtype,
91            'scopes': 'json',
92            'secret_hash': 'string',
93        },
94    )
95    return self._tokens_pipe

Return the internal pipe for tokens management.

def register_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
 98def register_token(
 99    self,
100    token: Token,
101    debug: bool = False,
102) -> mrsm.SuccessTuple:
103    """
104    Register the new token to the tokens table.
105    """
106    token_id, token_secret = token.generate_credentials()
107    tokens_pipe = self.get_tokens_pipe()
108    user_id = self.get_user_id(token.user) if token.user is not None else None
109    if user_id is None:
110        return False, "Cannot register a token without a user."
111
112    doc = {
113        'id': token_id,
114        'user_id': user_id,
115        'creation': datetime.now(timezone.utc),
116        'expiration': token.expiration,
117        'label': token.label,
118        'is_valid': token.is_valid,
119        'scopes': list(token.scopes) if token.scopes else [],
120        'secret_hash': hash_password(
121            str(token_secret),
122            rounds=STATIC_CONFIG['tokens']['hash_rounds']
123        ),
124    }
125    sync_success, sync_msg = tokens_pipe.sync([doc], check_existing=False, debug=debug)
126    if not sync_success:
127        return False, f"Failed to register token:\n{sync_msg}"
128    return True, "Success"

Register the new token to the tokens table.

def edit_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
131def edit_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
132    """
133    Persist the token's in-memory state to the tokens pipe.
134    """
135    if not token.id:
136        return False, "Token ID is not set."
137
138    if not token.exists(debug=debug):
139        return False, f"Token {token.id} does not exist."
140
141    if not token.creation:
142        token_model = self.get_token_model(token.id)
143        token.creation = token_model.creation
144
145    tokens_pipe = self.get_tokens_pipe()
146    doc = {
147        'id': token.id,
148        'creation': token.creation,
149        'expiration': token.expiration,
150        'label': token.label,
151        'is_valid': token.is_valid,
152        'scopes': list(token.scopes) if token.scopes else [],
153    }
154    sync_success, sync_msg = tokens_pipe.sync([doc], debug=debug)
155    if not sync_success:
156        return False, f"Failed to edit token '{token.id}':\n{sync_msg}"
157
158    return True, "Success"

Persist the token's in-memory state to the tokens pipe.

def invalidate_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
161def invalidate_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
162    """
163    Set `is_valid` to `False` for the given token.
164    """
165    if not token.id:
166        return False, "Token ID is not set."
167
168    if not token.exists(debug=debug):
169        return False, f"Token {token.id} does not exist."
170
171    if not token.creation:
172        token_model = self.get_token_model(token.id)
173        token.creation = token_model.creation
174
175    token.is_valid = False
176    tokens_pipe = self.get_tokens_pipe()
177    doc = {
178        'id': token.id,
179        'creation': token.creation,
180        'is_valid': False,
181    }
182    sync_success, sync_msg = tokens_pipe.sync([doc], debug=debug)
183    if not sync_success:
184        return False, f"Failed to invalidate token '{token.id}':\n{sync_msg}"
185
186    return True, "Success"

Set is_valid to False for the given token.

def delete_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
189def delete_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
190    """
191    Delete the given token from the tokens table.
192    """
193    if not token.id:
194        return False, "Token ID is not set."
195
196    if not token.exists(debug=debug):
197        return False, f"Token {token.id} does not exist."
198
199    if not token.creation:
200        token_model = self.get_token_model(token.id)
201        token.creation = token_model.creation
202
203    token.is_valid = False
204    tokens_pipe = self.get_tokens_pipe()
205    clear_success, clear_msg = tokens_pipe.clear(params={'id': token.id}, debug=debug)
206    if not clear_success:
207        return False, f"Failed to delete token '{token.id}':\n{clear_msg}"
208
209    return True, "Success"

Delete the given token from the tokens table.

def get_token( self, token_id: uuid.UUID | str, debug: bool = False) -> meerschaum.core.Token._Token.Token | None:
264def get_token(self, token_id: Union[uuid.UUID, str], debug: bool = False) -> Union[Token, None]:
265    """
266    Return the `Token` from its ID.
267    """
268    from meerschaum.utils.misc import is_uuid
269    if isinstance(token_id, str):
270        if is_uuid(token_id):
271            token_id = uuid.UUID(token_id)
272        else:
273            raise ValueError("Invalid token ID.")
274    token_model = self.get_token_model(token_id)
275    if token_model is None:
276        return None
277    return Token(instance=self, **dict(token_model))

Return the Token from its ID.

def get_tokens( self, user: meerschaum.core.User._User.User | None = None, labels: List[str] | None = None, ids: List[uuid.UUID] | None = None, debug: bool = False) -> List[meerschaum.core.Token._Token.Token]:
212def get_tokens(
213    self,
214    user: Optional[User] = None,
215    labels: Optional[List[str]] = None,
216    ids: Optional[List[uuid.UUID]] = None,
217    debug: bool = False,
218) -> List[Token]:
219    """
220    Return a list of `Token` objects.
221    """
222    tokens_pipe = self.get_tokens_pipe()
223    user_id = (
224        self.get_user_id(user, debug=debug)
225        if user is not None
226        else None
227    )
228    user_type = self.get_user_type(user, debug=debug) if user is not None else None
229    params = (
230        {
231            'user_id': (
232                user_id
233                if user_type != 'admin'
234                else [user_id, None]
235            )
236        }
237        if user_id is not None
238        else {}
239    )
240    if labels:
241        params['label'] = labels
242    if ids:
243        params['id'] = ids
244        
245    if debug:
246        dprint(f"Getting tokens with {user_id=}, {params=}")
247
248    tokens_docs = tokens_pipe.get_docs(params=params, debug=debug)
249    if not tokens_docs:
250        return []
251
252    if debug:
253        dprint(f"Retrieved token documents:\n{tokens_docs}")
254
255    return [
256        Token(
257            instance=self,
258            **{**token_doc, 'scopes': _scopes_list(token_doc.get('scopes'))}
259        )
260        for token_doc in reversed(tokens_docs)
261    ]

Return a list of Token objects.

def get_token_model( self, token_id: uuid.UUID | meerschaum.core.Token._Token.Token, debug: bool = False) -> "'Union[TokenModel, None]'":
280def get_token_model(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> 'Union[TokenModel, None]':
281    """
282    Return a token's model from the instance.
283    """
284    from meerschaum.models import TokenModel
285    if isinstance(token_id, Token):
286        token_id = token_id.id
287    if not token_id:
288        raise ValueError("Invalid token ID.")
289    tokens_pipe = self.get_tokens_pipe()
290    doc = tokens_pipe.get_doc(
291        params={'id': token_id},
292        debug=debug,
293    )
294    if doc is None:
295        return None
296    doc = dict(doc)
297    doc['scopes'] = _scopes_list(doc.get('scopes'))
298    return TokenModel(**doc)

Return a token's model from the instance.

def get_token_secret_hash( self, token_id: uuid.UUID | meerschaum.core.Token._Token.Token, debug: bool = False) -> str | None:
301def get_token_secret_hash(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> Union[str, None]:
302    """
303    Return the secret hash for a given token.
304    """
305    if isinstance(token_id, Token):
306        token_id = token_id.id
307    if not token_id:
308        raise ValueError("Invalid token ID.")
309    tokens_pipe = self.get_tokens_pipe()
310    return tokens_pipe.get_value('secret_hash', params={'id': token_id}, debug=debug)

Return the secret hash for a given token.

def token_exists( self, token_id: uuid.UUID | meerschaum.core.Token._Token.Token, debug: bool = False) -> bool:
341def token_exists(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> bool:
342    """
343    Return `True` if a token exists in the tokens pipe.
344    """
345    if isinstance(token_id, Token):
346        token_id = token_id.id
347    if not token_id:
348        raise ValueError("Invalid token ID.")
349
350    tokens_pipe = self.get_tokens_pipe()
351    return tokens_pipe.get_value('creation', params={'id': token_id}, debug=debug) is not None

Return True if a token exists in the tokens pipe.

def get_token_scopes( self, token_id: uuid.UUID | meerschaum.core.Token._Token.Token, debug: bool = False) -> List[str]:
326def get_token_scopes(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> List[str]:
327    """
328    Return the scopes for a token.
329    """
330    if isinstance(token_id, Token):
331        token_id = token_id.id
332    if not token_id:
333        raise ValueError("Invalid token ID.")
334
335    tokens_pipe = self.get_tokens_pipe()
336    return _scopes_list(
337        tokens_pipe.get_value('scopes', params={'id': token_id}, debug=debug)
338    )

Return the scopes for a token.

@abc.abstractmethod
def register_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
17@abc.abstractmethod
18def register_pipe(
19    self,
20    pipe: mrsm.Pipe,
21    debug: bool = False,
22    **kwargs: Any
23) -> mrsm.SuccessTuple:
24    """
25    Insert the pipe's attributes into the internal `pipes` table.
26
27    Parameters
28    ----------
29    pipe: mrsm.Pipe
30        The pipe to be registered.
31
32    Returns
33    -------
34    A `SuccessTuple` of the result.
35    """

Insert the pipe's attributes into the internal pipes table.

Parameters
  • pipe (mrsm.Pipe): The pipe to be registered.
Returns
  • A SuccessTuple of the result.
@abc.abstractmethod
def get_pipe_attributes( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Dict[str, Any]:
37@abc.abstractmethod
38def get_pipe_attributes(
39    self,
40    pipe: mrsm.Pipe,
41    debug: bool = False,
42    **kwargs: Any
43) -> Dict[str, Any]:
44    """
45    Return the pipe's document from the internal `pipes` table.
46
47    Parameters
48    ----------
49    pipe: mrsm.Pipe
50        The pipe whose attributes should be retrieved.
51
52    Returns
53    -------
54    The document that matches the keys of the pipe.
55    """

Return the pipe's document from the internal pipes table.

Parameters
  • pipe (mrsm.Pipe): The pipe whose attributes should be retrieved.
Returns
  • The document that matches the keys of the pipe.
@abc.abstractmethod
def get_pipe_id( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> str | int | None:
57@abc.abstractmethod
58def get_pipe_id(
59    self,
60    pipe: mrsm.Pipe,
61    debug: bool = False,
62    **kwargs: Any
63) -> Union[str, int, None]:
64    """
65    Return the `id` for the pipe if it exists.
66
67    Parameters
68    ----------
69    pipe: mrsm.Pipe
70        The pipe whose `id` to fetch.
71
72    Returns
73    -------
74    The `id` for the pipe's document or `None`.
75    """

Return the id for the pipe if it exists.

Parameters
  • pipe (mrsm.Pipe): The pipe whose id to fetch.
Returns
  • The id for the pipe's document or None.
def edit_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
77def edit_pipe(
78    self,
79    pipe: mrsm.Pipe,
80    debug: bool = False,
81    **kwargs: Any
82) -> mrsm.SuccessTuple:
83    """
84    Edit the attributes of the pipe.
85
86    Parameters
87    ----------
88    pipe: mrsm.Pipe
89        The pipe whose in-memory parameters must be persisted.
90
91    Returns
92    -------
93    A `SuccessTuple` indicating success.
94    """
95    raise NotImplementedError

Edit the attributes of the pipe.

Parameters
  • pipe (mrsm.Pipe): The pipe whose in-memory parameters must be persisted.
Returns
  • A SuccessTuple indicating success.
def delete_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
 97def delete_pipe(
 98    self,
 99    pipe: mrsm.Pipe,
100    debug: bool = False,
101    **kwargs: Any
102) -> mrsm.SuccessTuple:
103    """
104    Delete a pipe's registration from the `pipes` collection.
105
106    Parameters
107    ----------
108    pipe: mrsm.Pipe
109        The pipe to be deleted.
110
111    Returns
112    -------
113    A `SuccessTuple` indicating success.
114    """
115    raise NotImplementedError

Delete a pipe's registration from the pipes collection.

Parameters
  • pipe (mrsm.Pipe): The pipe to be deleted.
Returns
  • A SuccessTuple indicating success.
@abc.abstractmethod
def fetch_pipes_keys( self, connector_keys: List[str] | None = None, metric_keys: List[str] | None = None, location_keys: List[str] | None = None, tags: List[str] | None = None, debug: bool = False, **kwargs: Any) -> List[Tuple[str, str, str]] | List[Tuple[str, str, str, Dict[str, Any] | List[str]]] | Dict[int | str, Tuple[str, str, str]] | Dict[int | str, Tuple[str, str, str, Dict[str, Any] | List[str]]]:
117@abc.abstractmethod
118def fetch_pipes_keys(
119    self,
120    connector_keys: Optional[List[str]] = None,
121    metric_keys: Optional[List[str]] = None,
122    location_keys: Optional[List[str]] = None,
123    tags: Optional[List[str]] = None,
124    debug: bool = False,
125    **kwargs: Any
126) -> Union[
127    List[Tuple[str, str, str]],
128    List[Tuple[str, str, str, Union[Dict[str, Any], List[str]]]],
129    Dict[Union[int, str], Tuple[str, str, str]],
130    Dict[Union[int, str], Tuple[str, str, str, Union[Dict[str, Any], List[str]]]],
131]:
132    """
133    Return registered pipes' keys according to the provided filters.
134
135    May return either a list of key tuples or a dictionary mapping pipe IDs to key tuples.
136    When returning a dictionary, the key is the pipe's unique ID (int or str).
137    Tuples may be length 3 `(connector_keys, metric_key, location_key)` or length 4
138    with parameters or tags appended as the fourth element.
139
140    Parameters
141    ----------
142    connector_keys: list[str] | None, default None
143        The keys passed via `-c`.
144
145    metric_keys: list[str] | None, default None
146        The keys passed via `-m`.
147
148    location_keys: list[str] | None, default None
149        The keys passed via `-l`.
150
151    tags: List[str] | None, default None
152        Tags passed via `--tags` which are stored under `parameters:tags`.
153
154    Returns
155    -------
156    A list of tuples or a dictionary mapping pipe IDs to tuples.
157    You may return the string `"None"` for location keys in place of nulls.
158
159    Examples
160    --------
161    >>> import meerschaum as mrsm
162    >>> conn = mrsm.get_connector('example:demo')
163    >>>
164    >>> pipe_a = mrsm.Pipe('a', 'demo', tags=['foo'], instance=conn)
165    >>> pipe_b = mrsm.Pipe('b', 'demo', tags=['bar'], instance=conn)
166    >>> pipe_a.register()
167    >>> pipe_b.register()
168    >>>
169    >>> conn.fetch_pipes_keys(['a', 'b'])
170    [('a', 'demo', 'None'), ('b', 'demo', 'None')]
171    >>> conn.fetch_pipes_keys(metric_keys=['demo'])
172    [('a', 'demo', 'None'), ('b', 'demo', 'None')]
173    >>> conn.fetch_pipes_keys(tags=['foo'])
174    [('a', 'demo', 'None')]
175    >>> conn.fetch_pipes_keys(location_keys=[None])
176    [('a', 'demo', 'None'), ('b', 'demo', 'None')]
177    """

Return registered pipes' keys according to the provided filters.

May return either a list of key tuples or a dictionary mapping pipe IDs to key tuples. When returning a dictionary, the key is the pipe's unique ID (int or str). Tuples may be length 3 (connector_keys, metric_key, location_key) or length 4 with parameters or tags appended as the fourth element.

Parameters
  • connector_keys (list[str] | None, default None): The keys passed via -c.
  • metric_keys (list[str] | None, default None): The keys passed via -m.
  • location_keys (list[str] | None, default None): The keys passed via -l.
  • tags (List[str] | None, default None): Tags passed via --tags which are stored under parameters:tags.
Returns
  • A list of tuples or a dictionary mapping pipe IDs to tuples.
  • You may return the string "None" for location keys in place of nulls.
Examples
>>> import meerschaum as mrsm
>>> conn = mrsm.get_connector('example:demo')
>>>
>>> pipe_a = mrsm.Pipe('a', 'demo', tags=['foo'], instance=conn)
>>> pipe_b = mrsm.Pipe('b', 'demo', tags=['bar'], instance=conn)
>>> pipe_a.register()
>>> pipe_b.register()
>>>
>>> conn.fetch_pipes_keys(['a', 'b'])
[('a', 'demo', 'None'), ('b', 'demo', 'None')]
>>> conn.fetch_pipes_keys(metric_keys=['demo'])
[('a', 'demo', 'None'), ('b', 'demo', 'None')]
>>> conn.fetch_pipes_keys(tags=['foo'])
[('a', 'demo', 'None')]
>>> conn.fetch_pipes_keys(location_keys=[None])
[('a', 'demo', 'None'), ('b', 'demo', 'None')]
@abc.abstractmethod
def pipe_exists( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> bool:
179@abc.abstractmethod
180def pipe_exists(
181    self,
182    pipe: mrsm.Pipe,
183    debug: bool = False,
184    **kwargs: Any
185) -> bool:
186    """
187    Check whether a pipe's target table exists.
188
189    Parameters
190    ----------
191    pipe: mrsm.Pipe
192        The pipe to check whether its table exists.
193
194    Returns
195    -------
196    A `bool` indicating the table exists.
197    """

Check whether a pipe's target table exists.

Parameters
  • pipe (mrsm.Pipe): The pipe to check whether its table exists.
Returns
  • A bool indicating the table exists.
@abc.abstractmethod
def drop_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
199@abc.abstractmethod
200def drop_pipe(
201    self,
202    pipe: mrsm.Pipe,
203    debug: bool = False,
204    **kwargs: Any
205) -> mrsm.SuccessTuple:
206    """
207    Drop a pipe's collection if it exists.
208
209    Parameters
210    ----------
211    pipe: mrsm.Pipe
212        The pipe to be dropped.
213
214    Returns
215    -------
216    A `SuccessTuple` indicating success.
217    """
218    raise NotImplementedError

Drop a pipe's collection if it exists.

Parameters
  • pipe (mrsm.Pipe): The pipe to be dropped.
Returns
  • A SuccessTuple indicating success.
def drop_pipe_indices( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
220def drop_pipe_indices(
221    self,
222    pipe: mrsm.Pipe,
223    debug: bool = False,
224    **kwargs: Any
225) -> mrsm.SuccessTuple:
226    """
227    Drop a pipe's indices.
228
229    Parameters
230    ----------
231    pipe: mrsm.Pipe
232        The pipe whose indices need to be dropped.
233
234    Returns
235    -------
236    A `SuccessTuple` indicating success.
237    """
238    return False, f"Cannot drop indices for instance connectors of type '{self.type}'."

Drop a pipe's indices.

Parameters
  • pipe (mrsm.Pipe): The pipe whose indices need to be dropped.
Returns
  • A SuccessTuple indicating success.
@abc.abstractmethod
def sync_pipe( self, pipe: meerschaum.Pipe, df: "'pd.DataFrame'" = None, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, chunksize: int | None = -1, check_existing: bool = True, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
240@abc.abstractmethod
241def sync_pipe(
242    self,
243    pipe: mrsm.Pipe,
244    df: 'pd.DataFrame' = None,
245    begin: Union[datetime, int, None] = None,
246    end: Union[datetime, int, None] = None,
247    chunksize: Optional[int] = -1,
248    check_existing: bool = True,
249    debug: bool = False,
250    **kwargs: Any
251) -> mrsm.SuccessTuple:
252    """
253    Sync a pipe using a database connection.
254
255    Parameters
256    ----------
257    pipe: mrsm.Pipe
258        The Meerschaum Pipe instance into which to sync the data.
259
260    df: Optional[pd.DataFrame]
261        An optional DataFrame or equivalent to sync into the pipe.
262        Defaults to `None`.
263
264    begin: Union[datetime, int, None], default None
265        Optionally specify the earliest datetime to search for data.
266        Defaults to `None`.
267
268    end: Union[datetime, int, None], default None
269        Optionally specify the latest datetime to search for data.
270        Defaults to `None`.
271
272    chunksize: Optional[int], default -1
273        Specify the number of rows to sync per chunk.
274        If `-1`, resort to system configuration (default is `900`).
275        A `chunksize` of `None` will sync all rows in one transaction.
276        Defaults to `-1`.
277
278    check_existing: bool, default True
279        If `True`, pull and diff with existing data from the pipe. Defaults to `True`.
280
281    debug: bool, default False
282        Verbosity toggle. Defaults to False.
283
284    Returns
285    -------
286    A `SuccessTuple` of success (`bool`) and message (`str`).
287    """

Sync a pipe using a database connection.

Parameters
  • pipe (mrsm.Pipe): The Meerschaum Pipe instance into which to sync the data.
  • df (Optional[pd.DataFrame]): An optional DataFrame or equivalent to sync into the pipe. Defaults to None.
  • begin (Union[datetime, int, None], default None): Optionally specify the earliest datetime to search for data. Defaults to None.
  • end (Union[datetime, int, None], default None): Optionally specify the latest datetime to search for data. Defaults to None.
  • chunksize (Optional[int], default -1): Specify the number of rows to sync per chunk. If -1, resort to system configuration (default is 900). A chunksize of None will sync all rows in one transaction. Defaults to -1.
  • check_existing (bool, default True): If True, pull and diff with existing data from the pipe. Defaults to True.
  • debug (bool, default False): Verbosity toggle. Defaults to False.
Returns
  • A SuccessTuple of success (bool) and message (str).
def create_pipe_indices( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
289def create_pipe_indices(
290    self,
291    pipe: mrsm.Pipe,
292    debug: bool = False,
293    **kwargs: Any
294) -> mrsm.SuccessTuple:
295    """
296    Create a pipe's indices.
297
298    Parameters
299    ----------
300    pipe: mrsm.Pipe
301        The pipe whose indices need to be created.
302
303    Returns
304    -------
305    A `SuccessTuple` indicating success.
306    """
307    return False, f"Cannot create indices for instance connectors of type '{self.type}'."

Create a pipe's indices.

Parameters
  • pipe (mrsm.Pipe): The pipe whose indices need to be created.
Returns
  • A SuccessTuple indicating success.
def clear_pipe( self, pipe: meerschaum.Pipe, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
309def clear_pipe(
310    self,
311    pipe: mrsm.Pipe,
312    begin: Union[datetime, int, None] = None,
313    end: Union[datetime, int, None] = None,
314    params: Optional[Dict[str, Any]] = None,
315    debug: bool = False,
316    **kwargs: Any
317) -> mrsm.SuccessTuple:
318    """
319    Delete rows within `begin`, `end`, and `params`.
320
321    Parameters
322    ----------
323    pipe: mrsm.Pipe
324        The pipe whose rows to clear.
325
326    begin: datetime | int | None, default None
327        If provided, remove rows >= `begin`.
328
329    end: datetime | int | None, default None
330        If provided, remove rows < `end`.
331
332    params: dict[str, Any] | None, default None
333        If provided, only remove rows which match the `params` filter.
334
335    Returns
336    -------
337    A `SuccessTuple` indicating success.
338    """
339    raise NotImplementedError

Delete rows within begin, end, and params.

Parameters
  • pipe (mrsm.Pipe): The pipe whose rows to clear.
  • begin (datetime | int | None, default None): If provided, remove rows >= begin.
  • end (datetime | int | None, default None): If provided, remove rows < end.
  • params (dict[str, Any] | None, default None): If provided, only remove rows which match the params filter.
Returns
  • A SuccessTuple indicating success.
def get_pipe_data( self, pipe: meerschaum.Pipe, select_columns: List[str] | None = None, omit_columns: List[str] | None = None, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, debug: bool = False, **kwargs: Any) -> "Union['pd.DataFrame', None]":
341def get_pipe_data(
342    self,
343    pipe: mrsm.Pipe,
344    select_columns: Optional[List[str]] = None,
345    omit_columns: Optional[List[str]] = None,
346    begin: Union[datetime, int, None] = None,
347    end: Union[datetime, int, None] = None,
348    params: Optional[Dict[str, Any]] = None,
349    debug: bool = False,
350    **kwargs: Any
351) -> Union['pd.DataFrame', None]:
352    """
353    Query a pipe's target table and return the DataFrame.
354
355    Parameters
356    ----------
357    pipe: mrsm.Pipe
358        The pipe with the target table from which to read.
359
360    select_columns: list[str] | None, default None
361        If provided, only select these given columns.
362        Otherwise select all available columns (i.e. `SELECT *`).
363
364    omit_columns: list[str] | None, default None
365        If provided, remove these columns from the selection.
366
367    begin: datetime | int | None, default None
368        The earliest `datetime` value to search from (inclusive).
369
370    end: datetime | int | None, default None
371        The lastest `datetime` value to search from (exclusive).
372
373    params: dict[str | str] | None, default None
374        Additional filters to apply to the query.
375
376    Returns
377    -------
378    The target table's data as a DataFrame.
379    """
380    if type(self).get_pipe_docs is get_pipe_docs:
381        raise NotImplementedError(
382            f"Missing `get_pipe_data()` or `get_pipe_docs()` for {type(self)}."
383        )
384
385    docs = self.get_pipe_docs(
386        pipe=pipe,
387        select_columns=select_columns,
388        omit_columns=omit_columns,
389        begin=begin,
390        end=end,
391        params=params,
392        debug=debug,
393        **kwargs
394    )
395    if not docs:
396        return None
397
398    pd = mrsm.attempt_import('pandas')
399    try:
400        return pd.DataFrame(docs)
401    except Exception as e:
402        from meerschaum.utils.warnings import warn
403        warn(f"Cannot build DataFrame from pipe docs:\n{e}")
404    
405    return None

Query a pipe's target table and return the DataFrame.

Parameters
  • pipe (mrsm.Pipe): The pipe with the target table from which to read.
  • select_columns (list[str] | None, default None): If provided, only select these given columns. Otherwise select all available columns (i.e. SELECT *).
  • omit_columns (list[str] | None, default None): If provided, remove these columns from the selection.
  • begin (datetime | int | None, default None): The earliest datetime value to search from (inclusive).
  • end (datetime | int | None, default None): The lastest datetime value to search from (exclusive).
  • params (dict[str | str] | None, default None): Additional filters to apply to the query.
Returns
  • The target table's data as a DataFrame.
def get_pipe_docs( self, pipe: meerschaum.Pipe, select_columns: List[str] | None = None, omit_columns: List[str] | None = None, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, debug: bool = False, **kwargs: Any) -> list[dict[str, typing.Any]]:
407def get_pipe_docs(
408    self,
409    pipe: mrsm.Pipe,
410    select_columns: Optional[List[str]] = None,
411    omit_columns: Optional[List[str]] = None,
412    begin: Union[datetime, int, None] = None,
413    end: Union[datetime, int, None] = None,
414    params: Optional[Dict[str, Any]] = None,
415    debug: bool = False,
416    **kwargs: Any
417) -> list[dict[str, Any]]:
418    """
419    Return a pipe's data as a list of documents.
420    Converts either a Polars or pandas frame returned by `get_pipe_data()`.
421
422    Parameters
423    ----------
424    pipe: mrsm.Pipe
425        The pipe with the target table from which to read.
426
427    select_columns: list[str] | None, default None
428        If provided, only select these given columns.
429        Otherwise select all available columns (i.e. `SELECT *`).
430
431    omit_columns: list[str] | None, default None
432        If provided, remove these columns from the selection.
433
434    begin: datetime | int | None, default None
435        The earliest `datetime` value to search from (inclusive).
436
437    end: datetime | int | None, default None
438        The lastest `datetime` value to search from (exclusive).
439
440    params: dict[str | str] | None, default None
441        Additional filters to apply to the query.
442
443    Returns
444    -------
445    The target table's data as a list of dictionaries.
446    """
447    df = self.get_pipe_data(
448        pipe=pipe,
449        select_columns=select_columns,
450        omit_columns=omit_columns,
451        begin=begin,
452        end=end,
453        params=params,
454        debug=debug,
455        **kwargs
456    )
457    if df is None:
458        return []
459    to_dicts = getattr(df, 'to_dicts', None)
460    if callable(to_dicts):
461        return to_dicts()
462    return df.to_dict(orient='records')

Return a pipe's data as a list of documents. Converts either a Polars or pandas frame returned by get_pipe_data().

Parameters
  • pipe (mrsm.Pipe): The pipe with the target table from which to read.
  • select_columns (list[str] | None, default None): If provided, only select these given columns. Otherwise select all available columns (i.e. SELECT *).
  • omit_columns (list[str] | None, default None): If provided, remove these columns from the selection.
  • begin (datetime | int | None, default None): The earliest datetime value to search from (inclusive).
  • end (datetime | int | None, default None): The lastest datetime value to search from (exclusive).
  • params (dict[str | str] | None, default None): Additional filters to apply to the query.
Returns
  • The target table's data as a list of dictionaries.
@abc.abstractmethod
def get_sync_time( self, pipe: meerschaum.Pipe, params: Dict[str, Any] | None = None, newest: bool = True, debug: bool = False, **kwargs: Any) -> datetime.datetime | int | None:
464@abc.abstractmethod
465def get_sync_time(
466    self,
467    pipe: mrsm.Pipe,
468    params: Optional[Dict[str, Any]] = None,
469    newest: bool = True,
470    debug: bool = False,
471    **kwargs: Any
472) -> datetime | int | None:
473    """
474    Return the most recent value for the `datetime` axis.
475
476    Parameters
477    ----------
478    pipe: mrsm.Pipe
479        The pipe whose collection contains documents.
480
481    params: dict[str, Any] | None, default None
482        Filter certain parameters when determining the sync time.
483
484    newest: bool, default True
485        If `True`, return the maximum value for the column.
486
487    Returns
488    -------
489    The largest `datetime` or `int` value of the `datetime` axis. 
490    """

Return the most recent value for the datetime axis.

Parameters
  • pipe (mrsm.Pipe): The pipe whose collection contains documents.
  • params (dict[str, Any] | None, default None): Filter certain parameters when determining the sync time.
  • newest (bool, default True): If True, return the maximum value for the column.
Returns
  • The largest datetime or int value of the datetime axis.
@abc.abstractmethod
def get_pipe_columns_types( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Dict[str, str]:
492@abc.abstractmethod
493def get_pipe_columns_types(
494    self,
495    pipe: mrsm.Pipe,
496    debug: bool = False,
497    **kwargs: Any
498) -> Dict[str, str]:
499    """
500    Return the data types for the columns in the target table for data type enforcement.
501
502    Parameters
503    ----------
504    pipe: mrsm.Pipe
505        The pipe whose target table contains columns and data types.
506
507    Returns
508    -------
509    A dictionary mapping columns to data types.
510    """

Return the data types for the columns in the target table for data type enforcement.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table contains columns and data types.
Returns
  • A dictionary mapping columns to data types.
def get_pipe_columns_indices(self, debug: bool = False) -> Dict[str, List[Dict[str, str]]]:
512def get_pipe_columns_indices(
513    self,
514    debug: bool = False,
515) -> Dict[str, List[Dict[str, str]]]:
516    """
517    Return a dictionary mapping columns to metadata about related indices.
518
519    Parameters
520    ----------
521    pipe: mrsm.Pipe
522        The pipe whose target table has related indices.
523
524    Returns
525    -------
526    A list of dictionaries with the keys "type" and "name".
527
528    Examples
529    --------
530    >>> pipe = mrsm.Pipe('demo', 'shirts', columns={'primary': 'id'}, indices={'size_color': ['color', 'size']})
531    >>> pipe.sync([{'color': 'red', 'size': 'M'}])
532    >>> pipe.get_columns_indices()
533    {'id': [{'name': 'demo_shirts_pkey', 'type': 'PRIMARY KEY'}], 'color': [{'name': 'IX_demo_shirts_color_size', 'type': 'INDEX'}], 'size': [{'name': 'IX_demo_shirts_color_size', 'type': 'INDEX'}]}
534    """
535    return {}

Return a dictionary mapping columns to metadata about related indices.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table has related indices.
Returns
  • A list of dictionaries with the keys "type" and "name".
Examples
>>> pipe = mrsm.Pipe('demo', 'shirts', columns={'primary': 'id'}, indices={'size_color': ['color', 'size']})
>>> pipe.sync([{'color': 'red', 'size': 'M'}])
>>> pipe.get_columns_indices()
{'id': [{'name': 'demo_shirts_pkey', 'type': 'PRIMARY KEY'}], 'color': [{'name': 'IX_demo_shirts_color_size', 'type': 'INDEX'}], 'size': [{'name': 'IX_demo_shirts_color_size', 'type': 'INDEX'}]}
def get_pipe_size( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> int | None:
537def get_pipe_size(
538    self,
539    pipe: mrsm.Pipe,
540    debug: bool = False,
541    **kwargs: Any
542) -> Union[int, None]:
543    """
544    Return the on-disk size of a pipe's target table in bytes.
545
546    Parameters
547    ----------
548    pipe: mrsm.Pipe
549        The pipe whose target table size to measure.
550
551    Returns
552    -------
553    An `int` of the number of bytes occupied by the target table,
554    or `None` if the size cannot be determined.
555    """
556    raise NotImplementedError(
557        f"`get_pipe_size()` is not implemented for instance connectors of type '{self.type}'."
558    )

Return the on-disk size of a pipe's target table in bytes.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table size to measure.
Returns
  • An int of the number of bytes occupied by the target table,
  • or None if the size cannot be determined.
def compress_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
560def compress_pipe(
561    self,
562    pipe: mrsm.Pipe,
563    debug: bool = False,
564    **kwargs: Any
565) -> mrsm.SuccessTuple:
566    """
567    Compress a pipe's target table to reduce disk usage.
568
569    Parameters
570    ----------
571    pipe: mrsm.Pipe
572        The pipe whose target table to compress.
573
574    Returns
575    -------
576    A `SuccessTuple` indicating success.
577    """
578    return False, (
579        f"Compression is not supported for instance connectors of type '{self.type}'."
580    )

Compress a pipe's target table to reduce disk usage.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to compress.
Returns
  • A SuccessTuple indicating success.
def decompress_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
582def decompress_pipe(
583    self,
584    pipe: mrsm.Pipe,
585    debug: bool = False,
586    **kwargs: Any
587) -> mrsm.SuccessTuple:
588    """
589    Decompress a pipe's target table, the inverse of `compress_pipe()`.
590
591    Parameters
592    ----------
593    pipe: mrsm.Pipe
594        The pipe whose target table to decompress.
595
596    Returns
597    -------
598    A `SuccessTuple` indicating success.
599    """
600    return False, (
601        f"Decompression is not supported for instance connectors of type '{self.type}'."
602    )

Decompress a pipe's target table, the inverse of compress_pipe().

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to decompress.
Returns
  • A SuccessTuple indicating success.
def vacuum_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
604def vacuum_pipe(
605    self,
606    pipe: mrsm.Pipe,
607    debug: bool = False,
608    **kwargs: Any
609) -> mrsm.SuccessTuple:
610    """
611    Reclaim disk space from a pipe's target table.
612
613    Parameters
614    ----------
615    pipe: mrsm.Pipe
616        The pipe whose target table to vacuum.
617
618    Returns
619    -------
620    A `SuccessTuple` indicating success.
621    """
622    return False, (
623        f"Vacuuming is not supported for instance connectors of type '{self.type}'."
624    )

Reclaim disk space from a pipe's target table.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to vacuum.
Returns
  • A SuccessTuple indicating success.
def analyze_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
626def analyze_pipe(
627    self,
628    pipe: mrsm.Pipe,
629    debug: bool = False,
630    **kwargs: Any
631) -> mrsm.SuccessTuple:
632    """
633    Refresh the planner statistics for a pipe's target table.
634
635    Parameters
636    ----------
637    pipe: mrsm.Pipe
638        The pipe whose target table to analyze.
639
640    Returns
641    -------
642    A `SuccessTuple` indicating success.
643    """
644    return False, (
645        f"Analyzing is not supported for instance connectors of type '{self.type}'."
646    )

Refresh the planner statistics for a pipe's target table.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to analyze.
Returns
  • A SuccessTuple indicating success.
def partition_pipe( self, pipe: meerschaum.Pipe, chunk_minutes: int | None = None, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
648def partition_pipe(
649    self,
650    pipe: mrsm.Pipe,
651    chunk_minutes: Optional[int] = None,
652    debug: bool = False,
653    **kwargs: Any
654) -> mrsm.SuccessTuple:
655    """
656    Rebuild a pipe's target table to a new partition (chunk) width.
657
658    Parameters
659    ----------
660    pipe: mrsm.Pipe
661        The partitioned pipe whose target table to repartition.
662
663    chunk_minutes: Optional[int], default None
664        The new partition width in minutes. Defaults to the pipe's `verify.chunk_minutes`.
665
666    Returns
667    -------
668    A `SuccessTuple` indicating success.
669    """
670    return False, (
671        f"Repartitioning is not supported for instance connectors of type '{self.type}'."
672    )

Rebuild a pipe's target table to a new partition (chunk) width.

Parameters
  • pipe (mrsm.Pipe): The partitioned pipe whose target table to repartition.
  • chunk_minutes (Optional[int], default None): The new partition width in minutes. Defaults to the pipe's verify.chunk_minutes.
Returns
  • A SuccessTuple indicating success.
def get_teams(self, debug: bool = False, **kwargs: Any) -> List[Dict[str, Any]]:
41def get_teams(self, debug: bool = False, **kwargs: Any) -> List[Dict[str, Any]]:
42    """
43    Return every team on this instance, `Everyone` first.
44    """
45    from meerschaum import _core
46    carrier, context = _carrier(self)
47    return list(_core.get_teams(carrier, context, debug=debug))

Return every team on this instance, Everyone first.

def get_team( self, name: str, debug: bool = False, **kwargs: Any) -> Dict[str, Any] | None:
50def get_team(self, name: str, debug: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]:
51    """
52    Return the team named `name` (any case), or `None`.
53    """
54    from meerschaum import _core
55    carrier, context = _carrier(self)
56    return _core.get_team(carrier, context, str(name), debug=debug)

Return the team named name (any case), or None.

def register_team(self, team: Any, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
59def register_team(self, team: Any, debug: bool = False, **kwargs: Any) -> mrsm.SuccessTuple:
60    """
61    Register a new team (a `Team` or a dictionary; its new id is written back to it).
62    """
63    from meerschaum import _core
64    carrier, context = _carrier(self)
65    record = _team_record(team)
66    success, message, registered = _core.register_team(carrier, context, record, debug=debug)
67    if success:
68        new_id = (registered or {}).get('id', None)
69        if isinstance(team, dict):
70            team['id'] = new_id
71        elif hasattr(team, '_id'):
72            team._id = new_id
73    return success, message

Register a new team (a Team or a dictionary; its new id is written back to it).

def edit_team(self, team: Any, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
76def edit_team(self, team: Any, debug: bool = False, **kwargs: Any) -> mrsm.SuccessTuple:
77    """
78    Save a team's members and attributes.
79    """
80    from meerschaum import _core
81    carrier, context = _carrier(self)
82    return tuple(_core.edit_team(carrier, context, _team_record(team), debug=debug))

Save a team's members and attributes.

def delete_team(self, name: Any, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
85def delete_team(self, name: Any, debug: bool = False, **kwargs: Any) -> mrsm.SuccessTuple:
86    """
87    Delete a team: its pipes and saved Pipelines move to `Everyone`.
88    """
89    from meerschaum import _core
90    carrier, context = _carrier(self)
91    return tuple(_core.delete_team(carrier, context, str(getattr(name, 'name', name)), debug=debug))

Delete a team: its pipes and saved Pipelines move to Everyone.

def rename_team( self, old_name: Any, new_name: str, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
 94def rename_team(
 95    self,
 96    old_name: Any,
 97    new_name: str,
 98    debug: bool = False,
 99    **kwargs: Any
100) -> mrsm.SuccessTuple:
101    """
102    Rename a team, in the pipes, saved Pipelines and primary teams that name it too.
103    """
104    from meerschaum import _core
105    carrier, context = _carrier(self)
106    return tuple(_core.rename_team(
107        carrier, context, str(getattr(old_name, 'name', old_name)), str(new_name), debug=debug,
108    ))

Rename a team, in the pipes, saved Pipelines and primary teams that name it too.

def get_user_teams( self, user: Any, debug: bool = False, **kwargs: Any) -> List[Tuple[str, str]]:
111def get_user_teams(self, user: Any, debug: bool = False, **kwargs: Any) -> List[Tuple[str, str]]:
112    """
113    Return a user's teams as `(team, role)` pairs (`Everyone` first).
114    """
115    from meerschaum import _core
116    carrier, context = _carrier(self)
117    return [tuple(pair) for pair in _core.get_user_teams(carrier, context, _username(user), debug=debug)]

Return a user's teams as (team, role) pairs (Everyone first).

def get_team_pipes_count(self, name: str, debug: bool = False, **kwargs: Any) -> int:
120def get_team_pipes_count(self, name: str, debug: bool = False, **kwargs: Any) -> int:
121    """
122    Return how many pipes (and saved Pipelines) name a team (what renaming or deleting it
123    changes).
124    """
125    from meerschaum import _core
126    carrier, context = _carrier(self)
127    return int(_core.team_pipes_count(carrier, context, str(name), debug=debug))

Return how many pipes (and saved Pipelines) name a team (what renaming or deleting it changes).

def get_user_permissions(self, user: Any, debug: bool = False, **kwargs: Any) -> List[str]:
130def get_user_permissions(self, user: Any, debug: bool = False, **kwargs: Any) -> List[str]:
131    """
132    Return a user's effective permissions on this instance.
133    """
134    from meerschaum import _core
135    carrier, context = _carrier(self)
136    return list(_core.get_user_permissions(carrier, context, _username(user), debug=debug))

Return a user's effective permissions on this instance.

def get_user_role(self, user: Any, debug: bool = False, **kwargs: Any) -> str:
139def get_user_role(self, user: Any, debug: bool = False, **kwargs: Any) -> str:
140    """
141    Return a user's role on this instance (`'viewer'`, `'editor'`, `'operator'`, `'admin'` or
142    `'custom'`).
143    """
144    from meerschaum import _core
145    carrier, context = _carrier(self)
146    return str(_core.get_user_role(carrier, context, _username(user), debug=debug))

Return a user's role on this instance ('viewer', 'editor', 'operator', 'admin' or 'custom').

def set_user_attributes( self, user: Any, attributes: Dict[str, Any], debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
149def set_user_attributes(
150    self,
151    user: Any,
152    attributes: Dict[str, Any],
153    debug: bool = False,
154    **kwargs: Any
155) -> mrsm.SuccessTuple:
156    """
157    Replace a user's attributes (and nothing else).
158    """
159    from meerschaum import _core
160    carrier, context = _carrier(self)
161    return tuple(_core.set_user_attributes(
162        carrier, context, _username(user), dict(attributes or {}), debug=debug,
163    ))

Replace a user's attributes (and nothing else).

def lock_pipe( self, pipe: meerschaum.Pipe, locks: Any, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
166def lock_pipe(self, pipe: 'mrsm.Pipe', locks: Any, debug: bool = False, **kwargs: Any) -> mrsm.SuccessTuple:
167    """
168    Add locks to a registered pipe (`parameters['locks']`).
169    """
170    from meerschaum import _core
171    from meerschaum.core.Pipe._status import _status_context
172    return tuple(_core.pipe_lock(pipe, _status_context(pipe), list(locks), debug=debug))

Add locks to a registered pipe (parameters['locks']).

def unlock_pipe( self, pipe: meerschaum.Pipe, locks: Any, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
175def unlock_pipe(self, pipe: 'mrsm.Pipe', locks: Any, debug: bool = False, **kwargs: Any) -> mrsm.SuccessTuple:
176    """
177    Remove locks from a registered pipe (removing `'all'` removes every lock).
178    """
179    from meerschaum import _core
180    from meerschaum.core.Pipe._status import _status_context
181    return tuple(_core.pipe_unlock(pipe, _status_context(pipe), list(locks), debug=debug))

Remove locks from a registered pipe (removing 'all' removes every lock).

def set_pipe_access( self, pipe: meerschaum.Pipe, access: Dict[str, Any], debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
184def set_pipe_access(
185    self,
186    pipe: 'mrsm.Pipe',
187    access: Dict[str, Any],
188    debug: bool = False,
189    **kwargs: Any
190) -> mrsm.SuccessTuple:
191    """
192    Store a registered pipe's access (`parameters['access']`).
193    """
194    from meerschaum import _core
195    from meerschaum.core.Pipe._status import _status_context
196    return tuple(_core.pipe_set_access(pipe, _status_context(pipe), dict(access), debug=debug))

Store a registered pipe's access (parameters['access']).

class SQLConnector(meerschaum.connectors.InstanceConnector):
 94class SQLConnector(InstanceConnector):
 95    """
 96    Connect to SQL databases through the Rust connector's connection pool.
 97
 98    Raw SQL and pipe operations need no SQLAlchemy. The optional `.engine` property
 99    provides a SQLAlchemy engine for code that explicitly uses that interface.
100    
101    SQLConnectors may be used as Meerschaum instance connectors.
102    Read more about connectors and instances at
103    https://meerschaum.io/reference/connectors/
104
105    """
106
107    from ._create_engine import flavor_configs, create_engine
108    from ._sql import (
109        read,
110        value,
111        exec,
112        execute,
113        to_sql,
114        exec_queries,
115        get_connection,
116        _cleanup_connections,
117    )
118    from meerschaum.utils.sql import test_connection
119    from ._fetch import fetch, get_pipe_metadef
120    from ._cli import cli, _cli_exit
121    from ._compress import (
122        get_pipe_size,
123        compress_pipe,
124        decompress_pipe,
125        apply_compression_policy,
126        set_integer_now_func,
127    )
128    from ._maintenance import (
129        vacuum_pipe,
130        analyze_pipe,
131    )
132    from ._partition import (
133        get_partition_info,
134        partition_pipe,
135    )
136    from ._pipes import (
137        fetch_pipes_keys,
138        create_indices,
139        drop_indices,
140        get_create_index_queries,
141        get_drop_index_queries,
142        delete_pipe,
143        get_pipe_data,
144        get_pipe_docs,
145        get_pipe_data_query,
146        register_pipe,
147        edit_pipe,
148        get_pipe_id,
149        get_pipe_attributes,
150        sync_pipe,
151        sync_pipe_inplace,
152        get_sync_time,
153        pipe_exists,
154        get_pipe_rowcount,
155        drop_pipe,
156        clear_pipe,
157        deduplicate_pipe,
158        get_pipe_table,
159        get_pipe_columns_types,
160        get_pipe_schema,
161        get_pipe_columns_indices,
162        get_temporary_target,
163        create_pipe_indices,
164        drop_pipe_indices,
165        get_pipe_index_names,
166    )
167    from ._plugins import (
168        get_plugins_pipe,
169        register_plugin,
170        delete_plugin,
171        get_plugin_id,
172        get_plugin_version,
173        get_plugins,
174        get_plugin_user_id,
175        get_plugin_username,
176        get_plugin_attributes,
177    )
178    from ._users import (
179        get_users_pipe,
180        register_user,
181        get_user_id,
182        get_users,
183        edit_user,
184        delete_user,
185        get_user_password_hash,
186        get_user_type,
187        get_user_attributes,
188    )
189    from ._uri import from_uri, parse_uri
190
191    def __init__(
192        self,
193        label: Optional[str] = None,
194        flavor: Optional[str] = None,
195        wait: bool = False,
196        connect: bool = False,
197        debug: bool = False,
198        **kw: Any
199    ):
200        """
201        Parameters
202        ----------
203        label: str, default 'main'
204            The identifying label for the connector.
205            E.g. for `sql:main`, 'main' is the label.
206            Defaults to 'main'.
207
208        flavor: Optional[str], default None
209            The database flavor, e.g.
210            `'sqlite'`, `'postgresql'`, `'cockroachdb'`, etc.
211            To see supported flavors, run the `bootstrap connectors` command.
212
213        wait: bool, default False
214            If `True`, block until a database connection has been made.
215            Defaults to `False`.
216
217        connect: bool, default False
218            If `True`, immediately attempt to connect the database and raise
219            a warning if the connection fails.
220            Defaults to `False`.
221
222        debug: bool, default False
223            Verbosity toggle.
224            Defaults to `False`.
225
226        kw: Any
227            All other arguments will be passed to the connector's attributes.
228            Therefore, a connector may be made without being registered,
229            as long enough parameters are supplied to the constructor.
230        """
231        if 'uri' in kw:
232            uri = kw['uri']
233            if uri.startswith('postgres') and not uri.startswith('postgresql'):
234                uri = uri.replace('postgres', 'postgresql', 1)
235            if uri.startswith('postgresql') and not uri.startswith('postgresql+'):
236                uri = uri.replace('postgresql://', 'postgresql+psycopg://', 1)
237            if uri.startswith('timescaledb://'):
238                uri = uri.replace('timescaledb://', 'postgresql+psycopg://', 1)
239                flavor = 'timescaledb'
240            if uri.startswith('timescaledb-ha://'):
241                uri = uri.replace('timescaledb-ha://', 'postgresql+psycopg://', 1)
242                flavor = 'timescaledb-ha'
243            if uri.startswith('postgis://'):
244                uri = uri.replace('postgis://', 'postgresql+psycopg://', 1)
245                flavor = 'postgis'
246            kw['uri'] = uri
247            from_uri_params = self.from_uri(kw['uri'], as_dict=True)
248            label = label or from_uri_params.get('label', None)
249            _ = from_uri_params.pop('label', None)
250
251            ### Sometimes the flavor may be provided with a URI.
252            kw.update(from_uri_params)
253            if flavor:
254                kw['flavor'] = flavor
255            ### `?protected=true` marks the connector (`Connector.protected`); the driver must
256            ### not see it.
257            if 'protected' in from_uri_params:
258                kw['uri'] = _strip_uri_param(kw['uri'], 'protected')
259
260        ### set __dict__ in base class
261        super().__init__(
262            'sql',
263            label = label or self.__dict__.get('label', None),
264            **kw
265        )
266
267        if self.__dict__.get('flavor', None) in ('sqlite', 'geopackage'):
268            self._reset_attributes()
269            self._set_attributes(
270                'sql',
271                label = label,
272                inherit_default = False,
273                **kw
274            )
275            ### For backwards compatability reasons, set the path for sql:local if its missing.
276            if (
277                self.label == 'local'
278                and self.__dict__.get('database', None) in (None, '{SQLITE_DB_PATH}')
279            ):
280                import meerschaum.config.paths as paths
281                self.database = paths.SQLITE_DB_PATH.as_posix()
282
283        ### A `uri` decides where the connection goes (v4's engine is built from it alone), so
284        ### its scheme must also decide the dialect when it names another driver family than the
285        ### label's configured flavor (`MRSM_SQL_MAIN=sqlite:///x.db` on a `timescaledb-ha`
286        ### default config). v4 kept the configured flavor and generated SQL for the wrong
287        ### dialect; a flavor of the same family (`postgresql://` for `timescaledb-ha`) is kept.
288        if self.__dict__.get('uri') and self.__dict__.get('flavor'):
289            try:
290                uri_flavor = self.parse_uri(self.__dict__['uri']).get('flavor', None)
291            except Exception:
292                uri_flavor = None
293            if (
294                uri_flavor in _FLAVOR_FAMILIES or uri_flavor in _RUST_FLAVORS
295            ) and _flavor_family(uri_flavor) != _flavor_family(self.__dict__['flavor']):
296                self.__dict__['flavor'] = uri_flavor
297
298        ### ensure flavor and label are set accordingly
299        if 'flavor' not in self.__dict__:
300            if flavor is None and 'uri' not in self.__dict__:
301                raise ValueError(
302                    f"    Missing flavor. Provide flavor as a key for '{self}'."
303                )
304            self.flavor = flavor or self.parse_uri(self.__dict__['uri']).get('flavor', None)
305
306        if self.flavor == 'postgres':
307            self.flavor = 'postgresql'
308
309        self._debug = debug
310        ### Store the PID and thread at initialization
311        ### so we can dispose of the Pool in child processes or threads.
312        import os
313        import threading
314        self._pid = os.getpid()
315        self._thread_ident = threading.current_thread().ident
316        self._sessions = {}
317        self._locks = {'_sessions': threading.RLock(), }
318
319        ### verify the flavor's requirements are met
320        if self.flavor not in self.flavor_configs:
321            error(f"Flavor '{self.flavor}' is not supported by Meerschaum SQLConnector")
322        if not self.__dict__.get('uri'):
323            self.verify_attributes(
324                self.flavor_configs[self.flavor].get('requirements', set()),
325                debug=debug,
326            )
327
328        if wait:
329            from meerschaum.connectors.poll import retry_connect
330            retry_connect(connector=self, debug=debug)
331
332        if connect:
333            if not self.test_connection(debug=debug):
334                warn(f"Failed to connect with connector '{self}'!", stack=False)
335
336    @property
337    def Session(self):
338        if '_Session' not in self.__dict__:
339            if self.engine is None:
340                return None
341
342            from meerschaum.utils.packages import attempt_import
343            sqlalchemy_orm = attempt_import('sqlalchemy.orm', lazy=False)
344            session_factory = sqlalchemy_orm.sessionmaker(self.engine)
345            self._Session = sqlalchemy_orm.scoped_session(session_factory)
346
347        return self._Session
348
349    @property
350    def engine(self):
351        """
352        Return the SQLAlchemy engine connected to the configured database (the `sql` extra).
353
354        `read()`, `exec()`, `value()`, `exec_queries()` and `to_sql()` do not use it: they run on the
355        connector's Rust connection pool (see `meerschaum.connectors.sql._native`). The engine
356        opens the database through Python's own driver. On SQLite that is Python's `sqlite3`
357        library, a second copy of SQLite in the process: do not write the connector's file through
358        the engine while the process also reads or writes it through `Pipe` methods or `read()` /
359        `exec()` (two SQLite libraries keep separate lock bookkeeping for one file, which can
360        corrupt it; sqlite.org/howtocorrupt.html, 2.2.1).
361        """
362        import os
363        import threading
364        if self.flavor == 'duckdb':
365            from ._duckdb_transport import guard_sdk
366            guard_sdk()
367        if '_engine' not in self.__dict__:
368            try:
369                self._engine, self._engine_str = self.create_engine(include_uri=True)
370            except ImportError as e:
371                ### A property must raise `AttributeError` for `hasattr(conn, 'engine')` to be
372                ### `False`; Python cannot make one exception both an `ImportError` and an
373                ### `AttributeError`, so the message names the `sql` extra instead.
374                raise AttributeError(str(e)) from e
375
376        same_process = os.getpid() == self._pid
377        same_thread = threading.current_thread().ident == self._thread_ident
378
379        ### handle child processes
380        if not same_process:
381            self._pid = os.getpid()
382            self._thread = threading.current_thread()
383            warn("Different PID detected. Disposing of connections...")
384            self._engine.dispose()
385
386        ### handle different threads
387        if not same_thread:
388            if self.flavor == 'duckdb':
389                warn("Different thread detected.")
390                self._engine.dispose()
391
392        return self._engine
393
394    @property
395    def DATABASE_URL(self) -> str:
396        """
397        Return the URI connection string (alias for `SQLConnector.URI`).
398        """
399        return self.URI
400
401    @property
402    def URI(self) -> str:
403        """
404        Return the URI connection string, built from the connector's attributes
405        (no SQLAlchemy needed).
406        """
407        if '_engine_str' not in self.__dict__:
408            from ._create_engine import get_engine_str
409            self._engine_str = get_engine_str(self)
410        return str(self._engine_str)
411
412    @property
413    def IS_THREAD_SAFE(self) -> str:
414        """
415        Return whether this connector may be multithreaded.
416        """
417        if self.flavor in ('duckdb', 'oracle'):
418            return False
419        if self.flavor in ('sqlite', 'geopackage'):
420            return (
421                ':memory:' not in self.URI
422                and self.__dict__.get('_memory_path', None) is None
423            )
424        return True
425
426    @property
427    def metadata(self):
428        """
429        Return the metadata bound to this configured schema.
430        """
431        from ._create_engine import import_sqlalchemy
432        sqlalchemy = import_sqlalchemy()
433        if '_metadata' not in self.__dict__:
434            self._metadata = sqlalchemy.MetaData(schema=self.schema)
435        return self._metadata
436
437    @property
438    def instance_schema(self):
439        """
440        Return the schema name for Meerschaum tables. 
441        """
442        return self.schema
443
444    @property
445    def internal_schema(self):
446        """
447        Return the schema name for internal tables. 
448        """
449        from meerschaum._internal.static import STATIC_CONFIG
450        from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
451        schema_name = self.__dict__.get('internal_schema', None) or (
452            STATIC_CONFIG['sql']['internal_schema']
453            if self.flavor not in NO_SCHEMA_FLAVORS
454            else self.schema
455        )
456
457        if '_internal_schema' not in self.__dict__:
458            self._internal_schema = schema_name
459        return self._internal_schema
460
461    @property
462    def db(self) -> Optional[databases.Database]:
463        from meerschaum.utils.packages import attempt_import
464        databases = attempt_import('databases', lazy=False, install=True)
465        url = self.DATABASE_URL
466        if 'mysql' in url:
467            url = url.replace('+pymysql', '')
468        if '_db' not in self.__dict__:
469            try:
470                self._db = databases.Database(url)
471            except KeyError:
472                ### Likely encountered an unsupported flavor.
473                self._db = None
474        return self._db
475
476    @property
477    def db_version(self) -> Union[str, None]:
478        """
479        Return the database version.
480        """
481        _db_version = self.__dict__.get('_db_version', None)
482        if _db_version is not None:
483            return _db_version
484
485        from meerschaum.utils.sql import get_db_version
486        self._db_version = get_db_version(self)
487        return self._db_version
488
489    @property
490    def schema(self) -> Union[str, None]:
491        """
492        Return the default schema to use.
493        A value of `None` will not prepend a schema.
494        """
495        if 'schema' in self.__dict__:
496            return self.__dict__['schema']
497
498        from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
499        if self.flavor in NO_SCHEMA_FLAVORS:
500            self.__dict__['schema'] = None
501            return None
502
503        ### Ask the database (`SELECT current_schema()` on the PostgreSQL family, `SELECT
504        ### SCHEMA_NAME()` on MSSQL), natively and once: a role's `search_path` or a login's
505        ### default schema is what v4's `sqlalchemy.inspect(engine).default_schema_name`
506        ### reports, and the base wheel has no SQLAlchemy. Raises when the database is unreachable.
507        _schema = self._core_connector().discover_schema()
508        self.__dict__['schema'] = _schema
509        self.__dict__['_schema_pending'] = False
510        return _schema
511
512    def get_metadata_cache_path(self, kind: str = 'json') -> pathlib.Path:
513        """
514        Return the path to the file to which to write metadata cache.
515        """
516        import meerschaum.config.paths as paths
517        filename = (
518            f'{self.label}-metadata.pkl'
519            if kind == 'pkl'
520            else f'{self.label}.json'
521        )
522        return paths.SQL_CONN_CACHE_RESOURCES_PATH / filename
523
524    def __getstate__(self):
525        if '_rust_connector' not in self.__dict__:
526            return self.__dict__
527        return {
528            k: v
529            for k, v in self.__dict__.items()
530            if k not in ('_rust_connector', '_rust_connector_pid')
531        }
532
533    def _shared_memory_database(self) -> Optional[str]:
534        """
535        For an in-memory SQLite connector (`database=':memory:'`), the path of the one database
536        file that SQLAlchemy and the Rust pool both open, so `Pipe` methods and `read()` /
537        `exec()` / `.engine` see the same tables. `None` for any other connector.
538
539        The Python `sqlite3` module and the extension each carry their own SQLite library, which
540        cannot share a `cache=shared` memory database, so the "memory" database is a file in a
541        private temporary directory (a RAM disk where the system has one) that is removed when
542        the connector is collected or the process exits.
543        """
544        if self.__dict__.get('flavor', None) != 'sqlite' or self.__dict__.get('uri', None):
545            return None
546        if self.__dict__.get('database', None) != ':memory:':
547            return None
548        path = self.__dict__.get('_memory_path', None)
549        if path is None:
550            import os
551            import tempfile
552            import weakref
553            shm = '/dev/shm'
554            base = shm if os.path.isdir(shm) and os.access(shm, os.W_OK) else None
555            directory = tempfile.mkdtemp(prefix='mrsm_mem_', dir=base)
556            path = os.path.join(directory, 'memory.db')
557            self.__dict__['_memory_path'] = path
558            weakref.finalize(self, _remove_memory_directory, directory, os.getpid())
559        return path
560
561    def _core_connector(self, discover_schema: bool = True):
562        """
563        Return the `meerschaum._core.SqlConnector` behind this connector (built once per process,
564        see `crates/meerschaum-connector-sql`), which implements every pipe, user and plugin method.
565
566        Every flavor has a Rust driver (`mssql` and `oracle` are compiled into the wheels;
567        `duckdb` uses its public Python driver transport when installed, sharing a database
568        anchor with SQLAlchemy while keeping Pipe algorithms in Rust. Standalone / base
569        installs use DuckDB's C API. Explicit `MRSM_DUCKDB_LIB` pins native mode and refuses
570        mixed SQLAlchemy access; unset it and restart to use both). An in-memory SQLite database (`sql:memory`) is one database file that the Rust
571        pool and SQLAlchemy both open (`_shared_memory_database`), so `Pipe` methods and
572        `read()` / `exec()` / `.engine` see the same tables.
573        `read`, `exec`, `exec_queries`, `value` and `to_sql` run on this connector too (raw SQL,
574        rc15 Q53, `_native.NATIVE_SQL_FLAVORS`); only `.engine` (and the calls that hand out or take
575        SQLAlchemy objects) stays SQLAlchemy.
576
577        The Rust connector (and its pool) is built once per process: a forked child (a job
578        daemon) builds its own, like SQLAlchemy engines must.
579
580        Parameters
581        ----------
582        discover_schema: bool, default True
583            Ask the database for its default schema now (when none is configured) so `self.schema`
584            is the one the Rust methods use. `False` returns the connector without touching the
585            database (`meerschaum._internal.entry.adhoc_context`): its methods still ask for the
586            schema before they run.
587
588        Raises
589        ------
590        `RuntimeError` naming the cause when no driver can be built (the Oracle client library,
591        `libclntsh`, is not installed; an unknown flavor; a failed pool setup).
592        """
593        import os
594        rust = self.__dict__.get('_rust_connector', None)
595        if rust is False:
596            raise RuntimeError(self._rust_unavailable())
597        if rust is not None and self.__dict__.get('_rust_connector_pid', None) != os.getpid():
598            rust = None
599        if rust is None:
600            from meerschaum import _core
601            if self.flavor not in _RUST_FLAVORS:
602                self.__dict__['_rust_connector'] = False
603                self.__dict__['_rust_error'] = f"the flavor '{self.flavor}' has no Rust driver"
604                raise RuntimeError(self._rust_unavailable())
605            if self.flavor == 'oracle' and not _oracle_client_available():
606                self.__dict__['_rust_connector'] = False
607                self.__dict__['_rust_error'] = (
608                    "the Oracle client library (libclntsh, e.g. Oracle Instant Client) "
609                    "is not installed; install it and make it loadable "
610                    "(`LD_LIBRARY_PATH` / `ldconfig`) to use `oracle` instances"
611                )
612                raise RuntimeError(self._rust_unavailable())
613            attributes = {key: value for key, value in self.meta.items()
614                          if key not in ('type', 'label')}
615            ### The core reads `protected` as text (`True` / `'true'` both mark it).
616            if attributes.get('protected') is not None:
617                attributes['protected'] = str(attributes['protected'])
618            if self.flavor == 'duckdb':
619                from ._duckdb_transport import prepare_connector
620                attributes['_duckdb_memory_id'] = prepare_connector(self)
621                attributes['_duckdb_database'] = self.__dict__.get('_duckdb_database')
622            if (shared := self._shared_memory_database()) is not None:
623                attributes['database'] = shared
624            ### Only a configured schema: the Rust connector discovers the default itself.
625            attributes['schema'] = self.__dict__.get('schema', None)
626            try:
627                rust = _core.SqlConnector(self.label, attributes)
628            except Exception as e:
629                self.__dict__['_rust_connector'] = False
630                self.__dict__['_rust_error'] = str(e)
631                raise RuntimeError(self._rust_unavailable()) from e
632            self.__dict__['_rust_connector'] = rust
633            self.__dict__['_rust_connector_pid'] = os.getpid()
634            from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
635            self.__dict__['_schema_pending'] = (
636                not attributes['schema'] and self.flavor not in NO_SCHEMA_FLAVORS
637            )
638        if discover_schema and self.__dict__.get('_schema_pending', False):
639            ### The default schema is asked of the database before the first query runs, so the
640            ### Rust methods see the same schema as `self.schema`. While the database cannot be
641            ### asked, nothing runs: the flavor default (`public` / `dbo`) is the wrong schema
642            ### for a role with another `search_path`, and a write would create duplicates.
643            try:
644                self.__dict__['schema'] = rust.discover_schema()
645            except Exception as e:
646                raise RuntimeError(
647                    f"Could not determine the default schema of '{self}': {e}"
648                ) from e
649            self.__dict__['_schema_pending'] = False
650        return rust
651
652    def _rust_unavailable(self) -> str:
653        """The error text of a connector that has no usable Rust driver."""
654        cause = self.__dict__.get('_rust_error', None)
655        return (
656            f"Cannot use the SQL connector '{self}': no usable Rust driver"
657            + (f" ({cause})." if cause else ".")
658        )
659
660    def _rust_method(self, name: str):
661        """
662        Return the `meerschaum._core.SqlConnector` method `name`. Raises `AttributeError` naming
663        the method when the extension does not define it (raw SQL goes through `_native`, which
664        calls the `sql_*` bindings) and `RuntimeError` when the connector has no usable Rust
665        driver (see `_core_connector`).
666        """
667        rust = self._core_connector()
668        try:
669            method = getattr(rust, name)
670        except AttributeError:
671            raise AttributeError(f"`_core.SqlConnector` has no method '{name}'.") from None
672        ### A façade method called with `debug=True` shows the Rust core's debug output for
673        ### the binding it calls, whether or not it passes `debug` on.
674        try:
675            import sys
676            debug = sys._getframe(1).f_locals.get('debug', False) is True
677        except Exception:
678            debug = False
679        if not debug:
680            return _with_instance_hint(self, method)
681
682        def _debug_call(*args, **kwargs):
683            from meerschaum._core import debugging
684            with debugging():
685                return method(*args, **kwargs)
686
687        return _with_instance_hint(self, _debug_call)
688
689    def __setstate__(self, d):
690        self.__dict__.update(d)
691
692    def __call__(self):
693        return self

Connect to SQL databases through the Rust connector's connection pool.

Raw SQL and pipe operations need no SQLAlchemy. The optional .engine property provides a SQLAlchemy engine for code that explicitly uses that interface.

SQLConnectors may be used as Meerschaum instance connectors. Read more about connectors and instances at https://meerschaum.io/reference/connectors/

SQLConnector( label: str | None = None, flavor: str | None = None, wait: bool = False, connect: bool = False, debug: bool = False, **kw: Any)
191    def __init__(
192        self,
193        label: Optional[str] = None,
194        flavor: Optional[str] = None,
195        wait: bool = False,
196        connect: bool = False,
197        debug: bool = False,
198        **kw: Any
199    ):
200        """
201        Parameters
202        ----------
203        label: str, default 'main'
204            The identifying label for the connector.
205            E.g. for `sql:main`, 'main' is the label.
206            Defaults to 'main'.
207
208        flavor: Optional[str], default None
209            The database flavor, e.g.
210            `'sqlite'`, `'postgresql'`, `'cockroachdb'`, etc.
211            To see supported flavors, run the `bootstrap connectors` command.
212
213        wait: bool, default False
214            If `True`, block until a database connection has been made.
215            Defaults to `False`.
216
217        connect: bool, default False
218            If `True`, immediately attempt to connect the database and raise
219            a warning if the connection fails.
220            Defaults to `False`.
221
222        debug: bool, default False
223            Verbosity toggle.
224            Defaults to `False`.
225
226        kw: Any
227            All other arguments will be passed to the connector's attributes.
228            Therefore, a connector may be made without being registered,
229            as long enough parameters are supplied to the constructor.
230        """
231        if 'uri' in kw:
232            uri = kw['uri']
233            if uri.startswith('postgres') and not uri.startswith('postgresql'):
234                uri = uri.replace('postgres', 'postgresql', 1)
235            if uri.startswith('postgresql') and not uri.startswith('postgresql+'):
236                uri = uri.replace('postgresql://', 'postgresql+psycopg://', 1)
237            if uri.startswith('timescaledb://'):
238                uri = uri.replace('timescaledb://', 'postgresql+psycopg://', 1)
239                flavor = 'timescaledb'
240            if uri.startswith('timescaledb-ha://'):
241                uri = uri.replace('timescaledb-ha://', 'postgresql+psycopg://', 1)
242                flavor = 'timescaledb-ha'
243            if uri.startswith('postgis://'):
244                uri = uri.replace('postgis://', 'postgresql+psycopg://', 1)
245                flavor = 'postgis'
246            kw['uri'] = uri
247            from_uri_params = self.from_uri(kw['uri'], as_dict=True)
248            label = label or from_uri_params.get('label', None)
249            _ = from_uri_params.pop('label', None)
250
251            ### Sometimes the flavor may be provided with a URI.
252            kw.update(from_uri_params)
253            if flavor:
254                kw['flavor'] = flavor
255            ### `?protected=true` marks the connector (`Connector.protected`); the driver must
256            ### not see it.
257            if 'protected' in from_uri_params:
258                kw['uri'] = _strip_uri_param(kw['uri'], 'protected')
259
260        ### set __dict__ in base class
261        super().__init__(
262            'sql',
263            label = label or self.__dict__.get('label', None),
264            **kw
265        )
266
267        if self.__dict__.get('flavor', None) in ('sqlite', 'geopackage'):
268            self._reset_attributes()
269            self._set_attributes(
270                'sql',
271                label = label,
272                inherit_default = False,
273                **kw
274            )
275            ### For backwards compatability reasons, set the path for sql:local if its missing.
276            if (
277                self.label == 'local'
278                and self.__dict__.get('database', None) in (None, '{SQLITE_DB_PATH}')
279            ):
280                import meerschaum.config.paths as paths
281                self.database = paths.SQLITE_DB_PATH.as_posix()
282
283        ### A `uri` decides where the connection goes (v4's engine is built from it alone), so
284        ### its scheme must also decide the dialect when it names another driver family than the
285        ### label's configured flavor (`MRSM_SQL_MAIN=sqlite:///x.db` on a `timescaledb-ha`
286        ### default config). v4 kept the configured flavor and generated SQL for the wrong
287        ### dialect; a flavor of the same family (`postgresql://` for `timescaledb-ha`) is kept.
288        if self.__dict__.get('uri') and self.__dict__.get('flavor'):
289            try:
290                uri_flavor = self.parse_uri(self.__dict__['uri']).get('flavor', None)
291            except Exception:
292                uri_flavor = None
293            if (
294                uri_flavor in _FLAVOR_FAMILIES or uri_flavor in _RUST_FLAVORS
295            ) and _flavor_family(uri_flavor) != _flavor_family(self.__dict__['flavor']):
296                self.__dict__['flavor'] = uri_flavor
297
298        ### ensure flavor and label are set accordingly
299        if 'flavor' not in self.__dict__:
300            if flavor is None and 'uri' not in self.__dict__:
301                raise ValueError(
302                    f"    Missing flavor. Provide flavor as a key for '{self}'."
303                )
304            self.flavor = flavor or self.parse_uri(self.__dict__['uri']).get('flavor', None)
305
306        if self.flavor == 'postgres':
307            self.flavor = 'postgresql'
308
309        self._debug = debug
310        ### Store the PID and thread at initialization
311        ### so we can dispose of the Pool in child processes or threads.
312        import os
313        import threading
314        self._pid = os.getpid()
315        self._thread_ident = threading.current_thread().ident
316        self._sessions = {}
317        self._locks = {'_sessions': threading.RLock(), }
318
319        ### verify the flavor's requirements are met
320        if self.flavor not in self.flavor_configs:
321            error(f"Flavor '{self.flavor}' is not supported by Meerschaum SQLConnector")
322        if not self.__dict__.get('uri'):
323            self.verify_attributes(
324                self.flavor_configs[self.flavor].get('requirements', set()),
325                debug=debug,
326            )
327
328        if wait:
329            from meerschaum.connectors.poll import retry_connect
330            retry_connect(connector=self, debug=debug)
331
332        if connect:
333            if not self.test_connection(debug=debug):
334                warn(f"Failed to connect with connector '{self}'!", stack=False)
Parameters
  • label (str, default 'main'): The identifying label for the connector. E.g. for sql:main, 'main' is the label. Defaults to 'main'.
  • flavor (Optional[str], default None): The database flavor, e.g. 'sqlite', 'postgresql', 'cockroachdb', etc. To see supported flavors, run the bootstrap connectors command.
  • wait (bool, default False): If True, block until a database connection has been made. Defaults to False.
  • connect (bool, default False): If True, immediately attempt to connect the database and raise a warning if the connection fails. Defaults to False.
  • debug (bool, default False): Verbosity toggle. Defaults to False.
  • kw (Any): All other arguments will be passed to the connector's attributes. Therefore, a connector may be made without being registered, as long enough parameters are supplied to the constructor.
Session
336    @property
337    def Session(self):
338        if '_Session' not in self.__dict__:
339            if self.engine is None:
340                return None
341
342            from meerschaum.utils.packages import attempt_import
343            sqlalchemy_orm = attempt_import('sqlalchemy.orm', lazy=False)
344            session_factory = sqlalchemy_orm.sessionmaker(self.engine)
345            self._Session = sqlalchemy_orm.scoped_session(session_factory)
346
347        return self._Session
engine
349    @property
350    def engine(self):
351        """
352        Return the SQLAlchemy engine connected to the configured database (the `sql` extra).
353
354        `read()`, `exec()`, `value()`, `exec_queries()` and `to_sql()` do not use it: they run on the
355        connector's Rust connection pool (see `meerschaum.connectors.sql._native`). The engine
356        opens the database through Python's own driver. On SQLite that is Python's `sqlite3`
357        library, a second copy of SQLite in the process: do not write the connector's file through
358        the engine while the process also reads or writes it through `Pipe` methods or `read()` /
359        `exec()` (two SQLite libraries keep separate lock bookkeeping for one file, which can
360        corrupt it; sqlite.org/howtocorrupt.html, 2.2.1).
361        """
362        import os
363        import threading
364        if self.flavor == 'duckdb':
365            from ._duckdb_transport import guard_sdk
366            guard_sdk()
367        if '_engine' not in self.__dict__:
368            try:
369                self._engine, self._engine_str = self.create_engine(include_uri=True)
370            except ImportError as e:
371                ### A property must raise `AttributeError` for `hasattr(conn, 'engine')` to be
372                ### `False`; Python cannot make one exception both an `ImportError` and an
373                ### `AttributeError`, so the message names the `sql` extra instead.
374                raise AttributeError(str(e)) from e
375
376        same_process = os.getpid() == self._pid
377        same_thread = threading.current_thread().ident == self._thread_ident
378
379        ### handle child processes
380        if not same_process:
381            self._pid = os.getpid()
382            self._thread = threading.current_thread()
383            warn("Different PID detected. Disposing of connections...")
384            self._engine.dispose()
385
386        ### handle different threads
387        if not same_thread:
388            if self.flavor == 'duckdb':
389                warn("Different thread detected.")
390                self._engine.dispose()
391
392        return self._engine

Return the SQLAlchemy engine connected to the configured database (the sql extra).

read(), exec(), value(), exec_queries() and to_sql() do not use it: they run on the connector's Rust connection pool (see meerschaum.connectors.sql._native). The engine opens the database through Python's own driver. On SQLite that is Python's sqlite3 library, a second copy of SQLite in the process: do not write the connector's file through the engine while the process also reads or writes it through Pipe methods or read() / exec() (two SQLite libraries keep separate lock bookkeeping for one file, which can corrupt it; sqlite.org/howtocorrupt.html, 2.2.1).

DATABASE_URL: str
394    @property
395    def DATABASE_URL(self) -> str:
396        """
397        Return the URI connection string (alias for `SQLConnector.URI`).
398        """
399        return self.URI

Return the URI connection string (alias for SQLConnector.URI).

URI: str
401    @property
402    def URI(self) -> str:
403        """
404        Return the URI connection string, built from the connector's attributes
405        (no SQLAlchemy needed).
406        """
407        if '_engine_str' not in self.__dict__:
408            from ._create_engine import get_engine_str
409            self._engine_str = get_engine_str(self)
410        return str(self._engine_str)

Return the URI connection string, built from the connector's attributes (no SQLAlchemy needed).

IS_THREAD_SAFE: str
412    @property
413    def IS_THREAD_SAFE(self) -> str:
414        """
415        Return whether this connector may be multithreaded.
416        """
417        if self.flavor in ('duckdb', 'oracle'):
418            return False
419        if self.flavor in ('sqlite', 'geopackage'):
420            return (
421                ':memory:' not in self.URI
422                and self.__dict__.get('_memory_path', None) is None
423            )
424        return True

Return whether this connector may be multithreaded.

metadata
426    @property
427    def metadata(self):
428        """
429        Return the metadata bound to this configured schema.
430        """
431        from ._create_engine import import_sqlalchemy
432        sqlalchemy = import_sqlalchemy()
433        if '_metadata' not in self.__dict__:
434            self._metadata = sqlalchemy.MetaData(schema=self.schema)
435        return self._metadata

Return the metadata bound to this configured schema.

instance_schema
437    @property
438    def instance_schema(self):
439        """
440        Return the schema name for Meerschaum tables. 
441        """
442        return self.schema

Return the schema name for Meerschaum tables.

internal_schema
444    @property
445    def internal_schema(self):
446        """
447        Return the schema name for internal tables. 
448        """
449        from meerschaum._internal.static import STATIC_CONFIG
450        from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
451        schema_name = self.__dict__.get('internal_schema', None) or (
452            STATIC_CONFIG['sql']['internal_schema']
453            if self.flavor not in NO_SCHEMA_FLAVORS
454            else self.schema
455        )
456
457        if '_internal_schema' not in self.__dict__:
458            self._internal_schema = schema_name
459        return self._internal_schema

Return the schema name for internal tables.

db: 'Optional[databases.Database]'
461    @property
462    def db(self) -> Optional[databases.Database]:
463        from meerschaum.utils.packages import attempt_import
464        databases = attempt_import('databases', lazy=False, install=True)
465        url = self.DATABASE_URL
466        if 'mysql' in url:
467            url = url.replace('+pymysql', '')
468        if '_db' not in self.__dict__:
469            try:
470                self._db = databases.Database(url)
471            except KeyError:
472                ### Likely encountered an unsupported flavor.
473                self._db = None
474        return self._db
db_version: str | None
476    @property
477    def db_version(self) -> Union[str, None]:
478        """
479        Return the database version.
480        """
481        _db_version = self.__dict__.get('_db_version', None)
482        if _db_version is not None:
483            return _db_version
484
485        from meerschaum.utils.sql import get_db_version
486        self._db_version = get_db_version(self)
487        return self._db_version

Return the database version.

schema: str | None
489    @property
490    def schema(self) -> Union[str, None]:
491        """
492        Return the default schema to use.
493        A value of `None` will not prepend a schema.
494        """
495        if 'schema' in self.__dict__:
496            return self.__dict__['schema']
497
498        from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
499        if self.flavor in NO_SCHEMA_FLAVORS:
500            self.__dict__['schema'] = None
501            return None
502
503        ### Ask the database (`SELECT current_schema()` on the PostgreSQL family, `SELECT
504        ### SCHEMA_NAME()` on MSSQL), natively and once: a role's `search_path` or a login's
505        ### default schema is what v4's `sqlalchemy.inspect(engine).default_schema_name`
506        ### reports, and the base wheel has no SQLAlchemy. Raises when the database is unreachable.
507        _schema = self._core_connector().discover_schema()
508        self.__dict__['schema'] = _schema
509        self.__dict__['_schema_pending'] = False
510        return _schema

Return the default schema to use. A value of None will not prepend a schema.

def get_metadata_cache_path(self, kind: str = 'json') -> pathlib.Path:
512    def get_metadata_cache_path(self, kind: str = 'json') -> pathlib.Path:
513        """
514        Return the path to the file to which to write metadata cache.
515        """
516        import meerschaum.config.paths as paths
517        filename = (
518            f'{self.label}-metadata.pkl'
519            if kind == 'pkl'
520            else f'{self.label}.json'
521        )
522        return paths.SQL_CONN_CACHE_RESOURCES_PATH / filename

Return the path to the file to which to write metadata cache.

flavor_configs = {'timescaledb': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 5432}}, 'timescaledb-ha': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 5432}}, 'postgresql': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 5432}}, 'postgis': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 5432}}, 'citus': {'engine': 'postgresql+psycopg', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 5432}}, 'mssql': {'engine': 'mssql+pyodbc', 'create_engine': {'fast_executemany': True, 'use_insertmanyvalues': False, 'isolation_level': 'AUTOCOMMIT', 'use_setinputsizes': False, 'pool_pre_ping': True, 'ignore_no_transaction_on_rollback': True}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 1433, 'options': 'driver=ODBC Driver 18 for SQL Server&UseFMTONLY=Yes&TrustServerCertificate=yes&Encrypt=no&MARS_Connection=yes'}}, 'mysql': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 3306}}, 'mariadb': {'engine': 'mysql+pymysql', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': 'multi'}, 'requirements': {'database', 'username', 'host', 'password'}, 'defaults': {'port': 3306}}, 'oracle': {'engine': 'oracle+oracledb', 'create_engine': {'pool_size': 6, 'max_overflow': 6, 'pool_recycle': 3600, 'connect_args': {}}, 'omit_create_engine': {'method'}, 'to_sql': {'method': None}, 'requirements': {'database', 'username', 'host', 'password'}, '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'}}}
def create_engine( self, include_uri: bool = False, debug: bool = False, **kw) -> 'sqlalchemy.engine.Engine':
136def create_engine(
137    self,
138    include_uri: bool = False,
139    debug: bool = False,
140    **kw
141) -> 'sqlalchemy.engine.Engine':
142    """Create a sqlalchemy engine by building the engine string."""
143    from meerschaum.utils.packages import attempt_import
144    from meerschaum.utils.warnings import error, warn
145    if self.flavor == 'duckdb':
146        from ._duckdb_transport import guard_sdk, validate_connector_uri
147        guard_sdk()
148        validate_connector_uri(self)
149        for source in (kw, self._sys_config.get('create_engine', {})):
150            if any(source.get(key) is not None for key in ('creator', 'module')):
151                raise RuntimeError('DuckDB custom creator/module overrides cannot share the native driver safely.')
152    sqlalchemy = import_sqlalchemy()
153    import urllib
154    import copy
155    ### Install and patch required drivers.
156    if self.flavor in install_flavor_drivers:
157        _ = attempt_import(
158            *install_flavor_drivers[self.flavor],
159            debug=debug,
160            lazy=False,
161            warn=False,
162        )
163        if self.flavor == 'mssql':
164            _init_mssql_sqlalchemy()
165
166    engine_str = get_engine_str(self)
167    _password = self.__dict__.get('password', None)
168
169    ### Handle registering specific dialects (due to installing in virtual environments).
170    if self.flavor in flavor_dialects:
171        sqlalchemy.dialects.registry.register(*flavor_dialects[self.flavor])
172
173    if self.flavor in ("sqlite", "geopackage"):
174        if 'create_engine' not in self._sys_config:
175            self._sys_config['create_engine'] = {}
176        if 'connect_args' not in self._sys_config['create_engine']:
177            self._sys_config['create_engine']['connect_args'] = {}
178        self._sys_config['create_engine']['connect_args'].update({"check_same_thread": False})
179
180    if debug:
181        dprint(
182            (
183                (engine_str.replace(':' + _password, ':' + ('*' * len(_password))))
184                    if _password is not None else engine_str
185            ) + '\n' + f"{self._sys_config.get('create_engine', {}).get('connect_args', {})}"
186        )
187
188    _kw_copy = copy.deepcopy(kw)
189
190    ### NOTE: Order of inheritance:
191    ###       1. Defaults
192    ###       2. System configuration
193    ###       3. Connector configuration
194    ###       4. Keyword arguments
195    # Flavor defaults may share nested dictionaries across SQL dialects. Per-connector
196    # overrides must not mutate STATIC_CONFIG or affect engines created afterwards.
197    _create_engine_args = copy.deepcopy(
198        flavor_configs.get(self.flavor, {}).get('create_engine', {})
199    )
200    def _apply_create_engine_args(update):
201        if self.flavor == 'duckdb' or 'ALL' not in flavor_configs[self.flavor].get('omit_create_engine', {}):
202            _create_engine_args.update(
203                { k: v for k, v in update.items()
204                    if 'omit_create_engine' not in flavor_configs[self.flavor]
205                        or k not in flavor_configs[self.flavor].get('omit_create_engine')
206                }
207            )
208    _apply_create_engine_args(self._sys_config.get('create_engine', {}))
209    _apply_create_engine_args(self.__dict__.get('create_engine', {}))
210    _apply_create_engine_args(_kw_copy)
211
212    if self.flavor == 'duckdb':
213        # Historical system config includes pandas.to_sql's method, not an engine option.
214        _create_engine_args.pop('method', None)
215
216    duckdb_creator = None
217    if self.flavor == 'duckdb':
218        if any(_create_engine_args.get(key) is not None for key in ('creator', 'module')):
219            raise RuntimeError('DuckDB custom creator/module overrides cannot share the native driver safely.')
220        duckdb_creator = []
221        # SQLAlchemy constructs engines lazily; bind the shared creator before first checkout.
222        _create_engine_args['creator'] = lambda: duckdb_creator[0]()
223
224    try:
225        engine = sqlalchemy.create_engine(
226            engine_str,
227            ### I know this looks confusing, and maybe it's bad code,
228            ### but it's simple. It dynamically parses the config string
229            ### and splits it to separate the class name (QueuePool)
230            ### from the module name (sqlalchemy.pool).
231            poolclass    = getattr(
232                attempt_import(
233                    ".".join(self._sys_config['poolclass'].split('.')[:-1])
234                ),
235                self._sys_config['poolclass'].split('.')[-1]
236            ),
237            echo         = debug,
238            **_create_engine_args
239        )
240    except Exception:
241        warn(f"Failed to create connector '{self}':\n{traceback.format_exc()}", stack=False)
242        engine = None
243
244    if engine is not None and duckdb_creator is not None:
245        from ._duckdb_transport import engine_creator
246        args = dict(_create_engine_args)
247        args.pop('creator', None)
248        try:
249            duckdb_creator.append(engine_creator(self, engine, args))
250        except BaseException:
251            engine.dispose()
252            raise
253
254    if include_uri:
255        return engine, engine_str
256    return engine

Create a sqlalchemy engine by building the engine string.

def read( self, query_or_table: 'Union[str, sqlalchemy.Query]', params: Dict[str, Any] | List[str] | None = None, dtype: Dict[str, Any] | None = None, coerce_float: bool = True, chunksize: int | None = -1, workers: int | None = None, chunk_hook: Callable[[pandas.DataFrame], Any] | None = None, as_hook_results: bool = False, chunks: int | None = None, schema: str | None = None, as_chunks: bool = False, as_iterator: bool = False, as_dask: bool = False, index_col: str | None = None, silent: bool = False, debug: bool = False, as_polars: bool | None = None, as_pandas: bool | None = None, **kw: Any) -> 'Union[pandas.DataFrame, dask.DataFrame, List[pandas.DataFrame], List[Any], None]':
147def read(
148    self,
149    query_or_table: Union[str, sqlalchemy.Query],
150    params: Union[Dict[str, Any], List[str], None] = None,
151    dtype: Optional[Dict[str, Any]] = None,
152    coerce_float: bool = True,
153    chunksize: Optional[int] = -1,
154    workers: Optional[int] = None,
155    chunk_hook: Optional[Callable[[pandas.DataFrame], Any]] = None,
156    as_hook_results: bool = False,
157    chunks: Optional[int] = None,
158    schema: Optional[str] = None,
159    as_chunks: bool = False,
160    as_iterator: bool = False,
161    as_dask: bool = False,
162    index_col: Optional[str] = None,
163    silent: bool = False,
164    debug: bool = False,
165    as_polars: Optional[bool] = None,
166    as_pandas: Optional[bool] = None,
167    **kw: Any
168) -> Union[
169    pandas.DataFrame,
170    dask.DataFrame,
171    List[pandas.DataFrame],
172    List[Any],
173    None,
174]:
175    """
176    Read a SQL query or table into a DataFrame (Polars or pandas, see `as_polars`).
177
178    The query runs on the connector's Rust connection pool (the one `Pipe` methods use) for every
179    flavor in `meerschaum.connectors.sql._native.NATIVE_SQL_FLAVORS`, so no SQLAlchemy is needed;
180    the frame is the one Meerschaum 4 built with `pd.read_sql_query()` (same columns, dtypes and
181    values) when the result is pandas.
182
183    Parameters
184    ----------
185    query_or_table: Union[str, sqlalchemy.Query]
186        The SQL query (text, or a SQLAlchemy construct such as `text()` or `select()`) or the name
187        of the table from which to select.
188
189    params: Union[Dict[str, Any], List[Any], None], default None
190        The query's parameters: a dictionary for `:name` placeholders (SQLAlchemy's style, also
191        `%(name)s`), or a list / tuple of values for `?` (or `%s`) placeholders, in order.
192
193    dtype: Optional[Dict[str, Any]], default None
194        A dictionary of pandas data types to cast columns to (`DataFrame.astype()`).
195        For Polars results, declared `json` and `geometry` columns.
196
197    chunksize: Optional[int], default -1
198        How many chunks to read at a time. `None` will read everything in one large chunk.
199        Defaults to system configuration.
200
201        **NOTE:** DuckDB does not allow for chunking.
202
203    workers: Optional[int], default None
204        How many threads to use when consuming the generator.
205        Only applies if `chunk_hook` is provided.
206
207    chunk_hook: Optional[Callable[[pandas.DataFrame], Any]], default None
208        Hook function to execute once per chunk, e.g. writing and reading chunks intermittently.
209        See `--sync-chunks` for an example.
210        **NOTE:** `as_iterator` MUST be False (default).
211
212    as_hook_results: bool, default False
213        If `True`, return a `List` of the outputs of the hook function.
214        Only applicable if `chunk_hook` is not None.
215
216        **NOTE:** `as_iterator` MUST be `False` (default).
217
218    chunks: Optional[int], default None
219        Limit the number of chunks to read into memory, i.e. how many chunks to retrieve and
220        return into a single dataframe.
221        For example, to limit the returned dataframe to 100,000 rows,
222        you could specify a `chunksize` of `1000` and `chunks` of `100`.
223
224    schema: Optional[str], default None
225        If just a table name is provided, optionally specify the table schema.
226        Defaults to `SQLConnector.schema`.
227
228    as_chunks: bool, default False
229        If `True`, return a list of DataFrames.
230        Otherwise return a single DataFrame.
231
232    as_iterator: bool, default False
233        If `True`, return the pandas DataFrame iterator.
234        `chunksize` must not be `None` (falls back to 1000 if so),
235        and hooks are not called in this case.
236
237    index_col: Optional[str], default None
238        Make this column the index of a pandas result (with Dask, the index column).
239
240    silent: bool, default False
241        If `True`, return `None` instead of raising when the query fails, without a warning
242        (Meerschaum 4's behavior). By default, database errors raise.
243
244    as_polars: Optional[bool], default None
245        `True` returns Polars frames, `False` pandas; `None` follows the `dataframe` setting
246        (`meerschaum.utils.dataframe.get_dataframe_library()`).
247
248    as_pandas: Optional[bool], default None
249        `True` returns pandas frames, `False` Polars; `None` follows the setting.
250
251    Returns
252    -------
253    A DataFrame (default case), or an iterator, or a list of dataframes / iterators.
254    Raises the database error if the query fails (`None` with `silent=True`): a PEP 249 exception
255    (`meerschaum.connectors.sql.OperationalError`, `ProgrammingError`, `IntegrityError`, ...;
256    also an instance of SQLAlchemy's class of the same name when SQLAlchemy is imported).
257    On a protected connector (`protected: true`) without the allowance (`--allow-protected`,
258    `MRSM_ALLOW_PROTECTED=1`, `meerschaum.connectors.allow_protected()`), the query must be one
259    read statement (else `PermissionError`), and it runs in a transaction that is rolled back
260    (read-only on PostgreSQL and MySQL / MariaDB).
261
262    """
263    from meerschaum.connectors.sql import _native
264    if _native.uses_native(self):
265        from meerschaum.utils.packages import pandas_name
266        if 'dask' not in pandas_name():
267            return _native.read(
268                self,
269                query_or_table,
270                params=params,
271                dtype=dtype,
272                coerce_float=coerce_float,
273                chunksize=chunksize,
274                workers=workers,
275                chunk_hook=chunk_hook,
276                as_hook_results=as_hook_results,
277                chunks=chunks,
278                schema=schema,
279                as_chunks=as_chunks,
280                as_iterator=as_iterator,
281                index_col=index_col,
282                silent=silent,
283                debug=debug,
284                as_polars=as_polars,
285                as_pandas=as_pandas,
286                **kw
287            )
288        _native.fallback(self, 'read', 'the configured pandas implementation is Dask', debug)
289    result = _sqlalchemy_read(
290        self,
291        query_or_table,
292        params=params,
293        dtype=dtype,
294        coerce_float=coerce_float,
295        chunksize=chunksize,
296        workers=workers,
297        chunk_hook=chunk_hook,
298        as_hook_results=as_hook_results,
299        chunks=chunks,
300        schema=schema,
301        as_chunks=as_chunks,
302        as_iterator=as_iterator,
303        as_dask=as_dask,
304        index_col=index_col,
305        silent=silent,
306        debug=debug,
307        **kw
308    )
309    return _as_library(result, as_polars, as_pandas)

Read a SQL query or table into a DataFrame (Polars or pandas, see as_polars).

The query runs on the connector's Rust connection pool (the one Pipe methods use) for every flavor in meerschaum.connectors.sql._native.NATIVE_SQL_FLAVORS, so no SQLAlchemy is needed; the frame is the one Meerschaum 4 built with pd.read_sql_query() (same columns, dtypes and values) when the result is pandas.

Parameters
  • query_or_table (Union[str, sqlalchemy.Query]): The SQL query (text, or a SQLAlchemy construct such as text() or select()) or the name of the table from which to select.
  • params (Union[Dict[str, Any], List[Any], None], default None): The query's parameters: a dictionary for :name placeholders (SQLAlchemy's style, also %(name)s), or a list / tuple of values for ? (or %s) placeholders, in order.
  • dtype (Optional[Dict[str, Any]], default None): A dictionary of pandas data types to cast columns to (DataFrame.astype()). For Polars results, declared json and geometry columns.
  • chunksize (Optional[int], default -1): How many chunks to read at a time. None will read everything in one large chunk. Defaults to system configuration.

    NOTE: DuckDB does not allow for chunking.

  • workers (Optional[int], default None): How many threads to use when consuming the generator. Only applies if chunk_hook is provided.
  • chunk_hook (Optional[Callable[[pandas.DataFrame], Any]], default None): Hook function to execute once per chunk, e.g. writing and reading chunks intermittently. See --sync-chunks for an example. NOTE: as_iterator MUST be False (default).
  • as_hook_results (bool, default False): If True, return a List of the outputs of the hook function. Only applicable if chunk_hook is not None.

    NOTE: as_iterator MUST be False (default).

  • chunks (Optional[int], default None): Limit the number of chunks to read into memory, i.e. how many chunks to retrieve and return into a single dataframe. For example, to limit the returned dataframe to 100,000 rows, you could specify a chunksize of 1000 and chunks of 100.
  • schema (Optional[str], default None): If just a table name is provided, optionally specify the table schema. Defaults to SQLConnector.schema.
  • as_chunks (bool, default False): If True, return a list of DataFrames. Otherwise return a single DataFrame.
  • as_iterator (bool, default False): If True, return the pandas DataFrame iterator. chunksize must not be None (falls back to 1000 if so), and hooks are not called in this case.
  • index_col (Optional[str], default None): Make this column the index of a pandas result (with Dask, the index column).
  • silent (bool, default False): If True, return None instead of raising when the query fails, without a warning (Meerschaum 4's behavior). By default, database errors raise.
  • as_polars (Optional[bool], default None): True returns Polars frames, False pandas; None follows the dataframe setting (meerschaum.utils.dataframe.get_dataframe_library()).
  • as_pandas (Optional[bool], default None): True returns pandas frames, False Polars; None follows the setting.
Returns
  • A DataFrame (default case), or an iterator, or a list of dataframes / iterators.
  • Raises the database error if the query fails (None with silent=True) (a PEP 249 exception):

  • (meerschaum.connectors.sql.OperationalError, ProgrammingError, IntegrityError, ...;

  • also an instance of SQLAlchemy's class of the same name when SQLAlchemy is imported).
  • **On a protected connector (protected** (true) without the allowance (--allow-protected,):

  • MRSM_ALLOW_PROTECTED=1, meerschaum.connectors.allow_protected()), the query must be one

  • read statement (else PermissionError), and it runs in a transaction that is rolled back
  • (read-only on PostgreSQL and MySQL / MariaDB).
def value(self, query: str, *args: Any, use_pandas: bool = False, **kw: Any) -> Any:
672def value(
673    self,
674    query: str,
675    *args: Any,
676    use_pandas: bool = False,
677    **kw: Any
678) -> Any:
679    """
680    Execute the provided query and return the first value.
681
682    Parameters
683    ----------
684    query: str
685        The SQL query to execute.
686        
687    *args: Any
688        The arguments passed to `meerschaum.connectors.sql.SQLConnector.exec`
689        if `use_pandas` is `False` (default) or to `meerschaum.connectors.sql.SQLConnector.read`.
690        
691    use_pandas: bool, default False
692        If `True`, use `meerschaum.connectors.SQLConnector.read`, otherwise use
693        `meerschaum.connectors.sql.SQLConnector.exec` (default).
694        **NOTE:** This is always `True` for DuckDB.
695
696    **kw: Any
697        See `args`. `silent=True` returns `None` instead of raising when the query fails.
698
699    Returns
700    -------
701    Any value returned from the query (`None` when it returns no rows), as the flavor's Python
702    driver returns it (`int`, `float`, `Decimal`, `str`, `datetime`, ...; a NumPy scalar on DuckDB,
703    as in Meerschaum 4, which read it through pandas).
704    Raises the database error if the query fails (`None` with `silent=True`).
705    On a protected connector without the allowance, the query must be one read statement
706    (else `PermissionError`), and it runs read-only, as in `read()`.
707
708    """
709    from meerschaum.connectors.sql import _native
710    if _native.uses_native(self):
711        return _native.value(self, query, *args, use_pandas=use_pandas, **kw)
712    ### A missing `sql` extra is an `ImportError`, not a swallowed failure.
713    import_sqlalchemy()
714    silent = kw.get('silent', False)
715    if self.flavor == 'duckdb':
716        use_pandas = True
717    if use_pandas:
718        import_pandas_for_sql()
719        df = self.read(query, *args, **kw)
720        if df is None:
721            return None
722        try:
723            return df.iloc[0, 0]
724        except IndexError:
725            return None
726
727    _close = kw.pop('close', True)
728    _commit = kw.pop('commit', (self.flavor != 'mssql'))
729
730    ### A protected connector (without `--allow-protected`) reads one read statement, read-only.
731    read_only, refusal = _read_guard(self, query)
732    if refusal is not None:
733        if silent:
734            return None
735        raise PermissionError(refusal)
736    if read_only:
737        try:
738            with _read_only_connection(self) as connection:
739                result = self.exec(
740                    query,
741                    *args,
742                    close=False,
743                    commit=False,
744                    _connection=connection,
745                    _skip_protected=True,
746                    **kw
747                )
748                first = result.first() if result is not None else None
749                return first[0] if first is not None else None
750        except Exception:
751            if not silent:
752                raise
753            return None
754
755    ### Database errors raise; `silent=True` returns `None` without a warning (v4's result).
756    result, connection = self.exec(
757        query,
758        *args,
759        with_connection=True,
760        close=False,
761        commit=_commit,
762        _skip_protected=True,
763        **kw
764    )
765    try:
766        first = result.first() if result is not None else None
767        _val = first[0] if first is not None else None
768    except Exception:
769        if not silent:
770            raise
771        _val = None
772    finally:
773        if _close and connection is not None:
774            try:
775                connection.close()
776            except Exception as e:
777                warn("Failed to close connection with exception:\n" + str(e))
778    return _val

Execute the provided query and return the first value.

Parameters
Returns
  • Any value returned from the query (None when it returns no rows), as the flavor's Python
  • driver returns it (int, float, Decimal, str, datetime, ...; a NumPy scalar on DuckDB,
  • as in Meerschaum 4, which read it through pandas).
  • Raises the database error if the query fails (None with silent=True).
  • On a protected connector without the allowance, the query must be one read statement
  • (else PermissionError), and it runs read-only, as in read().
def exec( self, query: str, *args: Any, silent: bool = False, debug: bool = False, commit: bool | None = None, close: bool | None = None, with_connection: bool = False, _connection=None, _transaction=None, **kw: Any) -> 'Union[sqlalchemy.engine.result.resultProxy, sqlalchemy.engine.cursor.LegacyCursorResult, Tuple[sqlalchemy.engine.result.resultProxy, sqlalchemy.engine.base.Connection], Tuple[sqlalchemy.engine.cursor.LegacyCursorResult, sqlalchemy.engine.base.Connection], None]':
792def exec(
793    self,
794    query: str,
795    *args: Any,
796    silent: bool = False,
797    debug: bool = False,
798    commit: Optional[bool] = None,
799    close: Optional[bool] = None,
800    with_connection: bool = False,
801    _connection=None,
802    _transaction=None,
803    **kw: Any
804) -> Union[
805        sqlalchemy.engine.result.resultProxy,
806        sqlalchemy.engine.cursor.LegacyCursorResult,
807        Tuple[sqlalchemy.engine.result.resultProxy, sqlalchemy.engine.base.Connection],
808        Tuple[sqlalchemy.engine.cursor.LegacyCursorResult, sqlalchemy.engine.base.Connection],
809        None
810]:
811    """
812    Execute SQL code and return the result, e.g. when calling stored procedures.
813
814    The statement runs on the connector's Rust connection pool for every flavor in
815    `meerschaum.connectors.sql._native.NATIVE_SQL_FLAVORS` (no SQLAlchemy needed), and the result
816    has the parts of SQLAlchemy's `CursorResult` that code written for Meerschaum 4 uses
817    (`fetchall()`, `first()`, `scalar()`, `mappings()`, `keys()`, `rowcount`, iteration, ...).
818    `with_connection=True` (which returns a SQLAlchemy connection) uses SQLAlchemy.
819
820    If inserting data, please use bind variables to avoid SQL injection!
821
822    Parameters
823    ----------
824    query: Union[str, List[str], Tuple[str]]
825        The query to execute (text, or a SQLAlchemy construct).
826        If `query` is a list or tuple, call `self.exec_queries()` instead.
827
828    args: Any
829        The statement's parameters: a dictionary for `:name` placeholders, a list / tuple of
830        values for `?` (or `%s`) placeholders, or a list of dictionaries to run the statement
831        once per dictionary (`executemany`). `parameters=` is accepted too.
832
833    silent: bool, default False
834        If `True`, return `None` instead of raising when the query fails, without a warning
835        (Meerschaum 4's behavior). By default, database errors raise.
836
837    commit: Optional[bool], default None
838        If `True`, commit the changes after execution.
839        Causes issues with flavors like `'mssql'`.
840        This does not apply if `query` is a list of strings.
841
842    close: Optional[bool], default None
843        If `True`, close the connection after execution.
844        Causes issues with flavors like `'mssql'`.
845        This does not apply if `query` is a list of strings.
846
847    with_connection: bool, default False
848        If `True`, return a tuple including the connection object.
849        This does not apply if `query` is a list of strings.
850
851    Returns
852    -------
853    The `sqlalchemy` result object, or a tuple with the connection if `with_connection` is provided.
854    Raises the database error if the query fails (`None` with `silent=True`), and
855    `PermissionError` on a protected connector unless writes are allowed
856    (`--allow-protected`, `MRSM_ALLOW_PROTECTED=1`, `meerschaum.connectors.allow_protected()`).
857
858    """
859    if isinstance(query, (list, tuple)):
860        return self.exec_queries(
861            list(query),
862            *args,
863            silent=silent,
864            debug=debug,
865            **kw
866        )
867
868    ### A protected connector refuses raw SQL unless writes are allowed
869    ### (`--allow-protected`, `MRSM_ALLOW_PROTECTED=1`, `allow_protected()`).
870    if not kw.pop('_skip_protected', False):
871        from meerschaum.connectors import protected_refusal
872        refusal = protected_refusal(self, 'execute SQL')
873        if refusal is not None:
874            if silent:
875                return (None, None) if with_connection else None
876            raise PermissionError(refusal)
877
878    from meerschaum.connectors.sql import _native
879    if _native.uses_native(self):
880        reason = (
881            '`with_connection=True` returns a SQLAlchemy connection'
882            if with_connection
883            else (
884                'it was given a SQLAlchemy connection or transaction'
885                if (_connection is not None or _transaction is not None)
886                else None
887            )
888        )
889        if reason is None:
890            return _native.exec(
891                self, query, *args,
892                silent=silent, debug=debug, commit=commit, close=close, **kw
893            )
894        _native.fallback(self, 'exec', reason, debug)
895
896    from meerschaum.utils.packages import attempt_import
897    sqlalchemy = import_sqlalchemy()
898    if debug:
899        dprint(f"[{self}] Executing query:\n{query}")
900
901    _close = close if close is not None else (self.flavor != 'mssql')
902    _commit = commit if commit is not None else (
903        (self.flavor != 'mssql' or 'select' not in str(query).lower())
904    )
905
906    ### Select and Insert objects need to be compiled (SQLAlchemy 2.0.0+).
907    if not hasattr(query, 'compile'):
908        query = sqlalchemy.text(query)
909
910    connection = _connection if _connection is not None else self.get_connection()
911
912    try:
913        transaction = (
914            _transaction
915            if _transaction is not None else (
916                connection.begin()
917                if _commit
918                else None
919            )
920        )
921    except sqlalchemy.exc.InvalidRequestError as e:
922        if _connection is not None or _transaction is not None:
923            raise e
924        connection = self.get_connection(rebuild=True)
925        transaction = connection.begin()
926
927    if transaction is not None and not transaction.is_active and _transaction is not None:
928        connection = self.get_connection(rebuild=True)
929        transaction = connection.begin() if _commit else None
930
931    result = None
932    try:
933        result = connection.execute(query, *args, **kw)
934        if _commit:
935            transaction.commit()
936    except Exception as e:
937        if debug:
938            dprint(f"[{self}] Failed to execute query:\n\n{query}\n\n{e}")
939        result = None
940        if _commit:
941            if debug:
942                dprint(f"[{self}] Rolling back failed transaction...")
943            transaction.rollback()
944            connection = self.get_connection(rebuild=True)
945        ### Database errors raise; `silent=True` returns `None` without a warning (v4's result).
946        if not silent:
947            if _connection is None and not _close:
948                try:
949                    connection.close()
950                except Exception:
951                    pass
952            raise
953    finally:
954        if _close:
955            connection.close()
956
957    if debug:
958        dprint(f"[{self}] Done executing.")
959
960    if with_connection:
961        return result, connection
962
963    return result

Execute SQL code and return the result, e.g. when calling stored procedures.

The statement runs on the connector's Rust connection pool for every flavor in meerschaum.connectors.sql._native.NATIVE_SQL_FLAVORS (no SQLAlchemy needed), and the result has the parts of SQLAlchemy's CursorResult that code written for Meerschaum 4 uses (fetchall(), first(), scalar(), mappings(), keys(), rowcount, iteration, ...). with_connection=True (which returns a SQLAlchemy connection) uses SQLAlchemy.

If inserting data, please use bind variables to avoid SQL injection!

Parameters
  • query (Union[str, List[str], Tuple[str]]): The query to execute (text, or a SQLAlchemy construct). If query is a list or tuple, call self.exec_queries() instead.
  • args (Any): The statement's parameters: a dictionary for :name placeholders, a list / tuple of values for ? (or %s) placeholders, or a list of dictionaries to run the statement once per dictionary (executemany). parameters= is accepted too.
  • silent (bool, default False): If True, return None instead of raising when the query fails, without a warning (Meerschaum 4's behavior). By default, database errors raise.
  • commit (Optional[bool], default None): If True, commit the changes after execution. Causes issues with flavors like 'mssql'. This does not apply if query is a list of strings.
  • close (Optional[bool], default None): If True, close the connection after execution. Causes issues with flavors like 'mssql'. This does not apply if query is a list of strings.
  • with_connection (bool, default False): If True, return a tuple including the connection object. This does not apply if query is a list of strings.
Returns
  • The sqlalchemy result object, or a tuple with the connection if with_connection is provided.
  • Raises the database error if the query fails (None with silent=True), and
  • PermissionError on a protected connector unless writes are allowed
  • (--allow-protected, MRSM_ALLOW_PROTECTED=1, meerschaum.connectors.allow_protected()).
def execute( self, *args: Any, **kw: Any) -> 'Optional[sqlalchemy.engine.result.resultProxy]':
781def execute(
782    self,
783    *args : Any,
784    **kw : Any
785) -> Optional[sqlalchemy.engine.result.resultProxy]:
786    """
787    An alias for `meerschaum.connectors.sql.SQLConnector.exec`.
788    """
789    return self.exec(*args, **kw)
def to_sql( self, df: pandas.DataFrame, name: str = None, index: bool = False, if_exists: str = 'replace', method: str = '', chunksize: int | None = -1, schema: str | None = None, safe_copy: bool = True, silent: bool = False, debug: bool = False, as_tuple: bool = False, as_dict: bool = False, _connection=None, _transaction=None, **kw) -> bool | Tuple[bool, str]:
1083def to_sql(
1084    self,
1085    df: pandas.DataFrame,
1086    name: str = None,
1087    index: bool = False,
1088    if_exists: str = 'replace',
1089    method: str = "",
1090    chunksize: Optional[int] = -1,
1091    schema: Optional[str] = None,
1092    safe_copy: bool = True,
1093    silent: bool = False,
1094    debug: bool = False,
1095    as_tuple: bool = False,
1096    as_dict: bool = False,
1097    _connection=None,
1098    _transaction=None,
1099    **kw
1100) -> Union[bool, SuccessTuple]:
1101    """
1102    Upload a DataFrame's contents to the SQL server.
1103
1104    The rows go through the connector's Rust connection pool (no SQLAlchemy) for every flavor in
1105    `meerschaum.connectors.sql._native.NATIVE_SQL_FLAVORS`; a new table gets the column types
1106    `pandas.DataFrame.to_sql()` would create (or `dtype`).
1107
1108    Parameters
1109    ----------
1110    df: Union[pd.DataFrame, pl.DataFrame, pyarrow.Table]
1111        The DataFrame to be inserted: pandas, Polars, or Arrow data (anything with
1112        `__arrow_c_stream__`).
1113
1114    name: str
1115        The name of the table to be created.
1116
1117    index: bool, default False
1118        If True, creates the DataFrame's indices as columns.
1119
1120    if_exists: str, default 'replace'
1121        Drop and create the table ('replace') or append if it exists
1122        ('append') or raise Exception ('fail').
1123        Options are ['replace', 'append', 'fail'].
1124
1125    method: str, default ''
1126        None or multi. Details on pandas.to_sql.
1127
1128    chunksize: Optional[int], default -1
1129        How many rows to insert at a time.
1130
1131    schema: Optional[str], default None
1132        Optionally override the schema for the table.
1133        Defaults to `SQLConnector.schema`.
1134
1135    safe_copy: bool, defaul True
1136        If `True`, copy the dataframe before making any changes.
1137
1138    as_tuple: bool, default False
1139        If `True`, return a (success_bool, message) tuple instead of a `bool`.
1140        Defaults to `False`.
1141
1142    as_dict: bool, default False
1143        If `True`, return a dictionary of transaction information.
1144        The keys are `success`, `msg`, `start`, `end`, `duration`, `num_rows`, `chunksize`,
1145        `method`, and `target`.
1146
1147    kw: Any
1148        Additional arguments will be passed to the DataFrame's `to_sql` function
1149
1150    Returns
1151    -------
1152    Either a `bool` or a `SuccessTuple` (depends on `as_tuple`).
1153    """
1154    import time
1155    import json
1156    from datetime import timedelta
1157    from meerschaum.utils.warnings import error, warn
1158    import warnings
1159    import functools
1160    import traceback
1161
1162    if name is None:
1163        error(f"Name must not be `None` to insert data into {self}.")
1164
1165    from meerschaum.connectors import protected_refusal
1166    refusal = protected_refusal(self, f"write to table '{name}'")
1167    if refusal is not None:
1168        if not silent:
1169            warn(refusal, stack=False)
1170        if as_dict:
1171            return {'success': False, 'msg': refusal, 'target': name}
1172        return (False, refusal) if as_tuple else False
1173
1174    ### We're requiring `name` to be positional, and sometimes it's passed in from background jobs.
1175    kw.pop('name', None)
1176
1177    ### Arrow data (a pyarrow table, any `__arrow_c_stream__` producer) becomes the configured
1178    ### library's frame first.
1179    module_name = str(getattr(type(df), '__module__', ''))
1180    if (
1181        df is not None
1182        and not module_name.startswith(('pandas', 'polars', 'geopandas', 'dask'))
1183        and (hasattr(df, '__arrow_c_stream__') or hasattr(df, '__arrow_c_array__'))
1184    ):
1185        from meerschaum.utils.dataframe import frame_from_arrow
1186        df = frame_from_arrow(df)
1187
1188    ### The rows go through the Rust connector unless the call needs SQLAlchemy.
1189    from meerschaum.connectors.sql import _native
1190    is_dask = 'dask' in str(getattr(type(df), '__module__', ''))
1191    is_polars = str(getattr(type(df), '__module__', '')).startswith('polars')
1192    native = _native.uses_native(self)
1193    if native:
1194        reason = (
1195            'Dask frames write through `DataFrame.to_sql()`'
1196            if is_dask
1197            else (
1198                'a custom `method` writes through SQLAlchemy'
1199                if callable(method)
1200                else (
1201                    'it was given a SQLAlchemy connection or transaction'
1202                    if (_connection is not None or _transaction is not None)
1203                    else None
1204                )
1205            )
1206        )
1207        if reason is not None:
1208            native = False
1209            _native.fallback(self, 'to_sql', reason, debug)
1210    if is_polars and not native:
1211        df = df.to_pandas()
1212        is_polars = False
1213
1214    schema = schema or self.schema
1215
1216    from meerschaum.utils.sql import (
1217        sql_item_name,
1218        table_exists,
1219        json_flavors,
1220        truncate_item_name,
1221        DROP_IF_EXISTS_FLAVORS,
1222    )
1223    from meerschaum.utils.dataframe import (
1224        get_json_cols,
1225        get_numeric_cols,
1226        get_uuid_cols,
1227        get_bytes_cols,
1228        get_geometry_cols,
1229    )
1230    from meerschaum.utils.dtypes import (
1231        are_dtypes_equal,
1232        coerce_timezone,
1233        encode_bytes_for_bytea,
1234        serialize_bytes,
1235        serialize_decimal,
1236        serialize_geometry,
1237        json_serialize_value,
1238        get_geometry_type_srid,
1239    )
1240    from meerschaum.utils.dtypes.sql import (
1241        PD_TO_SQLALCHEMY_DTYPES_FLAVORS,
1242        TIMEZONE_NAIVE_FLAVORS,
1243        get_db_type_from_pd_type,
1244        get_pd_type_from_db_type,
1245        get_numeric_precision_scale,
1246    )
1247    from meerschaum.utils.misc import interval_str
1248    from meerschaum.connectors.sql._create_engine import flavor_configs
1249    from meerschaum.utils.packages import attempt_import, import_pandas
1250    sqlalchemy = import_sqlalchemy() if not native else None
1251    if native and is_polars:
1252        return _native.to_sql_polars(
1253            self, df, name,
1254            if_exists=if_exists,
1255            chunksize=chunksize,
1256            schema=schema,
1257            silent=silent,
1258            debug=debug,
1259            as_tuple=as_tuple,
1260            as_dict=as_dict,
1261            dtype=kw.get('dtype', None),
1262        )
1263    pd = import_pandas_for_sql()
1264
1265    bytes_cols = get_bytes_cols(df)
1266    numeric_cols = get_numeric_cols(df)
1267    geometry_cols = get_geometry_cols(df)
1268    ### NOTE: This excludes non-numeric serialized Decimals (e.g. SQLite).
1269    numeric_cols_dtypes = {
1270        col: typ
1271        for col, typ in kw.get('dtype', {}).items()
1272        if (
1273            col in df.columns
1274            and 'numeric' in str(typ).lower()
1275        )
1276    }
1277    numeric_cols.extend([col for col in numeric_cols_dtypes if col not in numeric_cols])
1278    numeric_cols_precisions_scales = {
1279        col: (
1280            (typ.precision, typ.scale)
1281            if hasattr(typ, 'precision')
1282            else get_numeric_precision_scale(self.flavor)
1283        )
1284        for col, typ in numeric_cols_dtypes.items()
1285    }
1286    geometry_cols_dtypes = {
1287        col: typ
1288        for col, typ in kw.get('dtype', {}).items()
1289        if (
1290            col in df.columns
1291            and ('geometry' in str(typ).lower() or 'geography' in str(typ).lower())
1292        )
1293    }
1294    geometry_cols.extend([col for col in geometry_cols_dtypes if col not in geometry_cols])
1295    geometry_cols_types_srids = {
1296        col: (typ.geometry_type, typ.srid)
1297        if hasattr(typ, 'srid')
1298        else get_geometry_type_srid()
1299        for col, typ in geometry_cols_dtypes.items()
1300    }
1301
1302    if self.flavor in TIMEZONE_NAIVE_FLAVORS:
1303        for col, typ in df.dtypes.items():
1304            if are_dtypes_equal(str(typ), 'datetime'):
1305                df[col] = coerce_timezone(df[col], strip_utc=True)
1306
1307    cols_pd_types = {
1308        col: get_pd_type_from_db_type(str(typ))
1309        for col, typ in kw.get('dtype', {}).items()
1310    }
1311    cols_pd_types.update({
1312        col: f'numeric[{precision},{scale}]'
1313        for col, (precision, scale) in numeric_cols_precisions_scales.items()
1314        if precision and scale
1315    })
1316    cols_db_types = {
1317        col: get_db_type_from_pd_type(typ, flavor=self.flavor)
1318        for col, typ in cols_pd_types.items()
1319    }
1320
1321    enable_bulk_insert = mrsm.get_config(
1322        'system', 'connectors', 'sql', 'bulk_insert', self.flavor,
1323        warn=False,
1324    ) or False
1325    stats = {'target': name}
1326    ### resort to defaults if None
1327    copied = False
1328    use_bulk_insert = False
1329    if method == "":
1330        if enable_bulk_insert:
1331            method = (
1332                functools.partial(mssql_insert_json, cols_types=cols_db_types, debug=debug)
1333                if self.flavor == 'mssql'
1334                else functools.partial(psql_insert_copy, debug=debug)
1335            )
1336            use_bulk_insert = True
1337        else:
1338            ### Should resolve to 'multi' or `None`.
1339            method = flavor_configs.get(self.flavor, {}).get('to_sql', {}).get('method', 'multi')
1340    ### The Rust connector bulk-inserts itself (`COPY` / `OPENJSON`) when the flag is on.
1341    method_name = (
1342        (method.func.__name__ if isinstance(method, functools.partial) else method.__name__)
1343        if callable(method)
1344        else str(method)
1345    )
1346
1347    if bytes_cols and (use_bulk_insert or self.flavor == 'oracle'):
1348        if safe_copy and not copied:
1349            df = df.copy()
1350            copied = True
1351        bytes_serializer = (
1352            functools.partial(encode_bytes_for_bytea, with_prefix=(self.flavor != 'oracle'))
1353            if self.flavor != 'mssql'
1354            else serialize_bytes
1355        )
1356        for col in bytes_cols:
1357            df[col] = df[col].apply(bytes_serializer)
1358
1359    ### Check for numeric columns.
1360    for col in numeric_cols:
1361        precision, scale = numeric_cols_precisions_scales.get(
1362            col,
1363            get_numeric_precision_scale(self.flavor)
1364        )
1365        df[col] = df[col].apply(
1366            functools.partial(
1367                serialize_decimal,
1368                quantize=True,
1369                precision=precision,
1370                scale=scale,
1371            )
1372        )
1373
1374    geometry_format = 'wkt' if self.flavor == 'mssql' else (
1375        'gpkg_wkb'
1376        if self.flavor == 'geopackage'
1377        else 'wkb_hex'
1378    )
1379    for col in geometry_cols:
1380        geometry_type, srid = geometry_cols_types_srids.get(col, get_geometry_type_srid())
1381        with warnings.catch_warnings():
1382            warnings.simplefilter("ignore")
1383            df[col] = df[col].apply(
1384                functools.partial(
1385                    serialize_geometry,
1386                    geometry_format=geometry_format,
1387                )
1388            )
1389
1390    stats['method'] = method_name
1391
1392    default_chunksize = self._sys_config.get('chunksize', None)
1393    chunksize = chunksize if chunksize != -1 else default_chunksize
1394    if chunksize is not None and self.flavor in _max_chunks_flavors:
1395        if chunksize > _max_chunks_flavors[self.flavor]:
1396            if chunksize != default_chunksize:
1397                warn(
1398                    f"The specified chunksize of {chunksize} exceeds the maximum of "
1399                    + f"{_max_chunks_flavors[self.flavor]} for flavor '{self.flavor}'.\n"
1400                    + f"    Falling back to a chunksize of {_max_chunks_flavors[self.flavor]}.",
1401                    stacklevel = 3,
1402                )
1403            chunksize = _max_chunks_flavors[self.flavor]
1404    stats['chunksize'] = chunksize
1405
1406    success, msg = False, "Default to_sql message"
1407    start = time.perf_counter()
1408    if debug:
1409        msg = f"[{self}] Inserting {len(df)} rows with chunksize: {chunksize}..."
1410        print(msg, end="", flush=True)
1411    stats['num_rows'] = len(df)
1412
1413    ### Check if the name is too long.
1414    truncated_name = truncate_item_name(name, self.flavor)
1415    if name != truncated_name:
1416        if self.flavor not in ('oracle', 'mysql', 'mariadb'):
1417            warn(
1418                f"Table '{name}' is too long for '{self.flavor}',"
1419                f" will instead create the table '{truncated_name}'."
1420            )
1421
1422    ### filter out non-pandas args
1423    import inspect
1424    to_sql_params = inspect.signature(df.to_sql).parameters
1425    to_sql_kw = {}
1426    for k, v in kw.items():
1427        if k in to_sql_params:
1428            to_sql_kw[k] = v
1429
1430    to_sql_kw.update({
1431        'name': truncated_name,
1432        'schema': schema,
1433        'index': index,
1434        'if_exists': if_exists,
1435        'method': method,
1436        'chunksize': chunksize,
1437    })
1438    if not native:
1439        to_sql_kw[('con' if not is_dask else 'uri')] = (
1440            self.engine if not is_dask else self.URI
1441        )
1442    if is_dask:
1443        to_sql_kw.update({
1444            'parallel': True,
1445        })
1446    elif _connection is not None:
1447        to_sql_kw['con'] = _connection
1448
1449    if_exists_str = "IF EXISTS" if self.flavor in DROP_IF_EXISTS_FLAVORS else ""
1450    if self.flavor == 'oracle' and not native:
1451        ### For some reason 'replace' doesn't work properly in pandas,
1452        ### so try dropping first.
1453        if if_exists == 'replace' and table_exists(name, self, schema=schema, debug=debug):
1454            success = self.exec(
1455                f"DROP TABLE {if_exists_str}" + sql_item_name(name, 'oracle', schema)
1456            ) is not None
1457            if not success:
1458                warn(f"Unable to drop {name}")
1459
1460        ### Enforce NVARCHAR(2000) as text instead of CLOB.
1461        dtype = to_sql_kw.get('dtype', {})
1462        for col, typ in df.dtypes.items():
1463            if are_dtypes_equal(str(typ), 'object'):
1464                dtype[col] = sqlalchemy.types.NVARCHAR(2000)
1465            elif are_dtypes_equal(str(typ), 'int'):
1466                dtype[col] = sqlalchemy.types.INTEGER
1467        to_sql_kw['dtype'] = dtype
1468    elif self.flavor == 'duckdb':
1469        dtype = to_sql_kw.get('dtype', {})
1470        dt_cols = [col for col, typ in df.dtypes.items() if are_dtypes_equal(str(typ), 'datetime')]
1471        for col in dt_cols:
1472            df[col] = coerce_timezone(df[col], strip_utc=False)
1473    elif self.flavor == 'mssql' and not native:
1474        dtype = to_sql_kw.get('dtype', {})
1475        dt_cols = [col for col, typ in df.dtypes.items() if are_dtypes_equal(str(typ), 'datetime')]
1476        new_dtype = {}
1477        for col in dt_cols:
1478            if col in dtype:
1479                continue
1480            dt_typ = get_db_type_from_pd_type(str(df.dtypes[col]), self.flavor, as_sqlalchemy=True)
1481            if col not in dtype:
1482                new_dtype[col] = dt_typ
1483
1484        dtype.update(new_dtype)
1485        to_sql_kw['dtype'] = dtype
1486
1487    ### Check for JSON columns.
1488    if self.flavor not in json_flavors:
1489        json_cols = get_json_cols(df)
1490        for col in json_cols:
1491            df[col] = df[col].apply(
1492                (
1493                    lambda x: json.dumps(x, default=json_serialize_value, sort_keys=True)
1494                    if not isinstance(x, Hashable)
1495                    else x
1496                )
1497            )
1498
1499    if PD_TO_SQLALCHEMY_DTYPES_FLAVORS['uuid'].get(self.flavor, None) != 'Uuid':
1500        uuid_cols = get_uuid_cols(df)
1501        for col in uuid_cols:
1502            df[col] = df[col].astype(str)
1503
1504    if native:
1505        try:
1506            success, error_msg = _native.to_sql_write(
1507                self,
1508                df,
1509                truncated_name,
1510                schema,
1511                if_exists,
1512                index,
1513                chunksize,
1514                dtype=kw.get('dtype', None),
1515            )
1516        except Exception:
1517            success, error_msg = False, traceback.format_exc()
1518        if not success:
1519            if not silent:
1520                warn(error_msg)
1521            msg = error_msg
1522    else:
1523        try:
1524            with warnings.catch_warnings():
1525                warnings.filterwarnings('ignore')
1526                df.to_sql(**to_sql_kw)
1527            success = True
1528        except Exception:
1529            if not silent:
1530                warn(traceback.format_exc())
1531            success, msg = False, traceback.format_exc()
1532
1533    end = time.perf_counter()
1534    if success:
1535        num_rows = len(df)
1536        msg = (
1537            f"It took {interval_str(timedelta(seconds=(end - start)))} "
1538            + f"to sync {num_rows:,} row"
1539            + ('s' if num_rows != 1 else '')
1540            + f" to {name}."
1541        )
1542    stats['start'] = start
1543    stats['end'] = end
1544    stats['duration'] = end - start
1545
1546    if debug:
1547        print(" done.", flush=True)
1548        dprint(msg)
1549
1550    stats['success'] = success
1551    stats['msg'] = msg
1552    if as_tuple:
1553        return success, msg
1554    if as_dict:
1555        return stats
1556    return success

Upload a DataFrame's contents to the SQL server.

The rows go through the connector's Rust connection pool (no SQLAlchemy) for every flavor in meerschaum.connectors.sql._native.NATIVE_SQL_FLAVORS; a new table gets the column types pandas.DataFrame.to_sql() would create (or dtype).

Parameters
  • df (Union[pd.DataFrame, pl.DataFrame, pyarrow.Table]): The DataFrame to be inserted: pandas, Polars, or Arrow data (anything with __arrow_c_stream__).
  • name (str): The name of the table to be created.
  • index (bool, default False): If True, creates the DataFrame's indices as columns.
  • if_exists (str, default 'replace'): Drop and create the table ('replace') or append if it exists ('append') or raise Exception ('fail'). Options are ['replace', 'append', 'fail'].
  • method (str, default ''): None or multi. Details on pandas.to_sql.
  • chunksize (Optional[int], default -1): How many rows to insert at a time.
  • schema (Optional[str], default None): Optionally override the schema for the table. Defaults to SQLConnector.schema.
  • safe_copy (bool, defaul True): If True, copy the dataframe before making any changes.
  • as_tuple (bool, default False): If True, return a (success_bool, message) tuple instead of a bool. Defaults to False.
  • as_dict (bool, default False): If True, return a dictionary of transaction information. The keys are success, msg, start, end, duration, num_rows, chunksize, method, and target.
  • kw (Any): Additional arguments will be passed to the DataFrame's to_sql function
Returns
  • Either a bool or a SuccessTuple (depends on as_tuple).
def exec_queries( self, queries: "List[Union[str, Tuple[str, Callable[['sqlalchemy.orm.session.Session'], List[str]]]]]", break_on_error: bool = False, rollback: bool = True, silent: bool = False, debug: bool = False) -> 'List[Union[sqlalchemy.engine.cursor.CursorResult, None]]':
 966def exec_queries(
 967    self,
 968    queries: List[
 969        Union[
 970            str,
 971            Tuple[str, Callable[['sqlalchemy.orm.session.Session'], List[str]]]
 972        ]
 973    ],
 974    break_on_error: bool = False,
 975    rollback: bool = True,
 976    silent: bool = False,
 977    debug: bool = False,
 978) -> List[Union[sqlalchemy.engine.cursor.CursorResult, None]]:
 979    """
 980    Execute a list of queries in a single transaction.
 981
 982    Parameters
 983    ----------
 984    queries: List[
 985        Union[
 986            str,
 987            Tuple[str, Callable[[], List[str]]]
 988        ]
 989    ]
 990        The queries in the transaction to be executed.
 991        If a query is a tuple, the second item of the tuple
 992        will be considered a callable hook that returns a list of queries to be executed
 993        before the next item in the list.
 994
 995    break_on_error: bool, default False
 996        If `True`, stop executing when a query fails.
 997
 998    rollback: bool, default True
 999        If `break_on_error` is `True`, rollback the transaction if a query fails.
1000
1001    silent: bool, default False
1002        If `True`, suppress warnings.
1003
1004    Returns
1005    -------
1006    A list of SQLAlchemy results.
1007    """
1008    from meerschaum.utils.warnings import warn
1009    from meerschaum.utils.debug import dprint
1010    from meerschaum.utils.packages import attempt_import
1011    from meerschaum.connectors import protected_refusal
1012    refusal = protected_refusal(self, 'execute SQL')
1013    if refusal is not None:
1014        if silent:
1015            return []
1016        raise PermissionError(refusal)
1017    from meerschaum.connectors.sql import _native
1018    if _native.uses_native(self):
1019        return _native.exec_queries(
1020            self,
1021            queries,
1022            break_on_error=break_on_error,
1023            rollback=rollback,
1024            silent=silent,
1025            debug=debug,
1026        )
1027    sqlalchemy, sqlalchemy_orm = import_sqlalchemy('sqlalchemy', 'sqlalchemy.orm')
1028    session = sqlalchemy_orm.Session(self.engine)
1029
1030    result = None
1031    results = []
1032    with session.begin():
1033        for query in queries:
1034            hook = None
1035            result = None
1036
1037            if isinstance(query, tuple):
1038                query, hook = query
1039            if isinstance(query, str):
1040                query = sqlalchemy.text(query)
1041
1042            if debug:
1043                dprint(f"[{self}]\n" + str(query))
1044
1045            try:
1046                result = session.execute(query)
1047                session.flush()
1048            except Exception as e:
1049                msg = (f"Encountered error while executing:\n{e}")
1050                if not silent:
1051                    warn(msg)
1052                elif debug:
1053                    dprint(f"[{self}]\n" + str(msg))
1054                result = None
1055
1056            if debug:
1057                dprint(f"[{self}] Finished executing.")
1058
1059            if result is None and break_on_error:
1060                if rollback:
1061                    if debug:
1062                        dprint(f"[{self}] Rolling back...")
1063                    session.rollback()
1064                results.append(result)
1065                break
1066            elif result is not None and hook is not None:
1067                hook_queries = hook(session)
1068                if hook_queries:
1069                    hook_results = self.exec_queries(
1070                        hook_queries,
1071                        break_on_error = break_on_error,
1072                        rollback=rollback,
1073                        silent=silent,
1074                        debug=debug,
1075                    )
1076                    result = (result, hook_results)
1077
1078            results.append(result)
1079
1080    return results

Execute a list of queries in a single transaction.

Parameters
  • queries (List[): Union[ str, Tuple[str, Callable[[], List[str]]] ]
  • ]: The queries in the transaction to be executed. If a query is a tuple, the second item of the tuple will be considered a callable hook that returns a list of queries to be executed before the next item in the list.
  • break_on_error (bool, default False): If True, stop executing when a query fails.
  • rollback (bool, default True): If break_on_error is True, rollback the transaction if a query fails.
  • silent (bool, default False): If True, suppress warnings.
Returns
  • A list of SQLAlchemy results.
def get_connection(self, rebuild: bool = False) -> "'sqlalchemy.engine.base.Connection'":
1754def get_connection(self, rebuild: bool = False) -> 'sqlalchemy.engine.base.Connection':
1755    """
1756    Return the current alive connection.
1757
1758    Parameters
1759    ----------
1760    rebuild: bool, default False
1761        If `True`, close the previous connection and open a new one.
1762
1763    Returns
1764    -------
1765    A `sqlalchemy.engine.base.Connection` object.
1766    """
1767    import threading
1768    if '_thread_connections' not in self.__dict__:
1769        self.__dict__['_thread_connections'] = {}
1770
1771    self._cleanup_connections()
1772
1773    thread_id = threading.get_ident()
1774
1775    thread_connections = self.__dict__.get('_thread_connections', {})
1776    connection = thread_connections.get(thread_id, None)
1777
1778    if rebuild and connection is not None:
1779        try:
1780            connection.close()
1781        except Exception:
1782            pass
1783
1784        _ = thread_connections.pop(thread_id, None)
1785        connection = None
1786
1787    if connection is None or connection.closed:
1788        connection = self.engine.connect()
1789        thread_connections[thread_id] = connection
1790
1791    return connection

Return the current alive connection.

Parameters
  • rebuild (bool, default False): If True, close the previous connection and open a new one.
Returns
  • A sqlalchemy.engine.base.Connection object.
def test_connection(self, **kw: Any) -> bool | None:
650def test_connection(
651    self,
652    **kw: Any
653) -> Union[bool, None]:
654    """
655    Test if a successful connection to the database may be made.
656
657    Parameters
658    ----------
659    **kw:
660        `max_retries` (default `1`), `retry_wait` seconds between attempts (default `0`)
661        and `warn` (print the error of a failed attempt, default `False`).
662
663    Returns
664    -------
665    `True` if a connection is made, otherwise `False`.
666
667    """
668    import time
669    max_retries = max(int(kw.get('max_retries', 1)), 1)
670    retry_wait = kw.get('retry_wait', 0)
671    warn = kw.get('warn', False)
672    for attempt in range(max_retries):
673        try:
674            ### The Rust connector's `SELECT 1`: no SQLAlchemy engine, so the base wheel
675            ### can check a reachable database.
676            if self._core_connector().test_connection():
677                return True
678        except Exception as e:
679            if warn:
680                print(e)
681        if attempt + 1 < max_retries and retry_wait:
682            time.sleep(retry_wait)
683    return False

Test if a successful connection to the database may be made.

Parameters
  • **kw:: max_retries (default 1), retry_wait seconds between attempts (default 0) and warn (print the error of a failed attempt, default False).
Returns
  • True if a connection is made, otherwise False.
def fetch( self, pipe: meerschaum.Pipe, begin: datetime.datetime | int | str | None = '', end: datetime.datetime | int | str | None = None, check_existing: bool = True, chunksize: int | None = -1, workers: int | None = None, debug: bool = False, **kw: Any) -> pandas.DataFrame | List[Any] | None:
 68def fetch(
 69    self,
 70    pipe: mrsm.Pipe,
 71    begin: Union[datetime, int, str, None] = '',
 72    end: Union[datetime, int, str, None] = None,
 73    check_existing: bool = True,
 74    chunksize: Optional[int] = -1,
 75    workers: Optional[int] = None,
 76    debug: bool = False,
 77    **kw: Any
 78) -> Union['pd.DataFrame', List[Any], None]:
 79    """Execute the SQL definition and return a Pandas DataFrame.
 80
 81    Parameters
 82    ----------
 83    pipe: mrsm.Pipe
 84        The pipe object which contains the `fetch` metadata.
 85
 86        - pipe.columns['datetime']: str
 87            - Name of the datetime column for the remote table.
 88        - pipe.parameters['fetch']: Dict[str, Any]
 89            - Parameters necessary to execute a query.
 90        - pipe.parameters['fetch']['definition']: str
 91            - Raw SQL query to execute to generate the pandas DataFrame.
 92        - pipe.parameters['fetch']['backtrack_minutes']: Union[int, float]
 93            - How many minutes before `begin` to search for data (*optional*).
 94
 95    begin: Union[datetime, int, str, None], default None
 96        Most recent datatime to search for data.
 97        If `backtrack_minutes` is provided, subtract `backtrack_minutes`.
 98
 99    end: Union[datetime, int, str, None], default None
100        The latest datetime to search for data.
101        If `end` is `None`, do not bound 
102
103    check_existing: bool, defult True
104        If `False`, use a backtrack interval of 0 minutes.
105
106    chunksize: Optional[int], default -1
107        How many rows to load into memory at once.
108        Otherwise the entire result set is loaded into memory.
109
110    workers: Optional[int], default None
111        How many threads to use when consuming the generator.
112        Defaults to the number of cores.
113
114    debug: bool, default False
115        Verbosity toggle.
116
117    Returns
118    -------
119    A pandas DataFrame generator.
120    """
121    meta_def = self.get_pipe_metadef(
122        pipe,
123        begin=begin,
124        end=end,
125        check_existing=check_existing,
126        debug=debug,
127        **kw
128    )
129    chunks = self.read(
130        meta_def,
131        chunksize=chunksize,
132        workers=workers,
133        as_iterator=True,
134        debug=debug,
135    )
136    return chunks

Execute the SQL definition and return a Pandas DataFrame.

Parameters
  • pipe (mrsm.Pipe): The pipe object which contains the fetch metadata.

    • pipe.columns['datetime']: str
      • Name of the datetime column for the remote table.
    • pipe.parameters['fetch']: Dict[str, Any]
      • Parameters necessary to execute a query.
    • pipe.parameters['fetch']['definition']: str
      • Raw SQL query to execute to generate the pandas DataFrame.
    • pipe.parameters['fetch']['backtrack_minutes']: Union[int, float]
      • How many minutes before begin to search for data (optional).
  • begin (Union[datetime, int, str, None], default None): Most recent datatime to search for data. If backtrack_minutes is provided, subtract backtrack_minutes.
  • end (Union[datetime, int, str, None], default None): The latest datetime to search for data. If end is None, do not bound
  • check_existing (bool, defult True): If False, use a backtrack interval of 0 minutes.
  • chunksize (Optional[int], default -1): How many rows to load into memory at once. Otherwise the entire result set is loaded into memory.
  • workers (Optional[int], default None): How many threads to use when consuming the generator. Defaults to the number of cores.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A pandas DataFrame generator.
def get_pipe_metadef( self, pipe: meerschaum.Pipe, params: Dict[str, Any] | None = None, begin: datetime.datetime | int | str | None = '', end: datetime.datetime | int | str | None = None, check_existing: bool = True, debug: bool = False, **kw: Any) -> str | None:
139def get_pipe_metadef(
140    self,
141    pipe: mrsm.Pipe,
142    params: Optional[Dict[str, Any]] = None,
143    begin: Union[datetime, int, str, None] = '',
144    end: Union[datetime, int, str, None] = None,
145    check_existing: bool = True,
146    debug: bool = False,
147    **kw: Any
148) -> Union[str, None]:
149    """
150    Return a pipe's meta definition fetch query.
151
152    params: Optional[Dict[str, Any]], default None
153        Optional params dictionary to build the `WHERE` clause.
154        See `meerschaum.utils.sql.build_where`.
155
156    begin: Union[datetime, int, str, None], default None
157        Most recent datatime to search for data.
158        If `backtrack_minutes` is provided, subtract `backtrack_minutes`.
159
160    end: Union[datetime, int, str, None], default None
161        The latest datetime to search for data.
162        If `end` is `None`, do not bound 
163
164    check_existing: bool, default True
165        If `True`, apply the backtrack interval.
166
167    debug: bool, default False
168        Verbosity toggle.
169
170    Returns
171    -------
172    A pipe's meta definition fetch query string.
173    """
174    from meerschaum import _core
175    from meerschaum.config import get_config
176    if (pipe.columns or {}).get('id', None) and get_config('system', 'experimental', 'join_fetch'):
177        _warn_join_fetch_ignored()
178
179    parent = pipe.parent
180    parent_spec = None
181    if parent is not None:
182        parent_dt_col = parent.columns.get('datetime', None)
183        parent_instance = parent.instance_connector
184        parent_spec = {
185            'target': parent.target,
186            'schema': (
187                parent_instance.get_pipe_schema(parent)
188                if parent_instance.type == 'sql'
189                else None
190            ),
191            'dt_col': parent_dt_col,
192            'dt_typ': (
193                parent.dtypes.get(parent_dt_col, 'datetime')
194                if parent_dt_col
195                else None
196            ),
197            'precision_unit': (parent.precision or {}).get('unit', 'second'),
198        }
199    sync_time = pipe.get_sync_time(debug=debug) if begin == '' else None
200    _check_bounds_are_comparable(pipe, parent, begin, end, sync_time)
201    meta_def = _core.get_pipe_metadef(
202        self._core_connector(),
203        pipe,
204        params,
205        begin,
206        end,
207        check_existing,
208        sync_time,
209        parent_spec,
210    )
211    if meta_def is None:
212        raise ValueError(f"No SQL definition could be found for {pipe}.")
213    return meta_def

Return a pipe's meta definition fetch query.

params: Optional[Dict[str, Any]], default None Optional params dictionary to build the WHERE clause. See meerschaum.utils.sql.build_where.

begin: Union[datetime, int, str, None], default None Most recent datatime to search for data. If backtrack_minutes is provided, subtract backtrack_minutes.

end: Union[datetime, int, str, None], default None The latest datetime to search for data. If end is None, do not bound

check_existing: bool, default True If True, apply the backtrack interval.

debug: bool, default False Verbosity toggle.

Returns
  • A pipe's meta definition fetch query string.
def cli(self, debug: bool = False, nopretty: bool = False) -> Tuple[bool, str]:
36def cli(
37    self,
38    debug: bool = False,
39    nopretty: bool = False,
40) -> SuccessTuple:
41    """Open the native SQL client launcher using this connector's exact attributes.
42
43    Rainfrog, configured preferences, fallback clients, credentials, and exit status use
44    the same implementation as ``mrsm sql LABEL``. ``nopretty`` forces the plain client.
45    No SQLAlchemy engine or database connection is needed to launch an external client.
46    """
47    from meerschaum import _core
48    from meerschaum._internal.entry import _plain_value
49
50    attributes = dict(self.meta)
51    attributes.pop('type', None)
52    attributes.pop('label', None)
53    context = {'attributes': {str(self): {
54        key: value for key, value in attributes.items() if _plain_value(value)
55    }}}
56    arguments = ['sql', self.label]
57    if debug:
58        arguments.append('--debug')
59    if nopretty:
60        arguments.append('--nopretty')
61    return _core.entry(arguments, None, context)

Open the native SQL client launcher using this connector's exact attributes.

Rainfrog, configured preferences, fallback clients, credentials, and exit status use the same implementation as mrsm sql LABEL. nopretty forces the plain client. No SQLAlchemy engine or database connection is needed to launch an external client.

def get_pipe_size( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> int | None:
17def get_pipe_size(
18    self,
19    pipe: mrsm.Pipe,
20    debug: bool = False,
21    **kwargs: Any
22) -> Union[int, None]:
23    """
24    Return the on-disk size of a pipe's target table in bytes.
25
26    For TimescaleDB hypertables, the total hypertable size (including chunks and indexes)
27    is returned. Other flavors use their native size functions where available.
28
29    Parameters
30    ----------
31    pipe: mrsm.Pipe
32        The pipe whose target table size to measure.
33
34    debug: bool, default False
35        Verbosity toggle.
36
37    Returns
38    -------
39    An `int` of the number of bytes occupied by the target table,
40    or `None` if the size could not be determined.
41    """
42    return self._rust_method('get_pipe_size')(pipe)

Return the on-disk size of a pipe's target table in bytes.

For TimescaleDB hypertables, the total hypertable size (including chunks and indexes) is returned. Other flavors use their native size functions where available.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table size to measure.
  • debug (bool, default False): Verbosity toggle.
Returns
  • An int of the number of bytes occupied by the target table,
  • or None if the size could not be determined.
def compress_pipe( self, pipe: meerschaum.Pipe, no_policy: bool = False, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
 82def compress_pipe(
 83    self,
 84    pipe: mrsm.Pipe,
 85    no_policy: bool = False,
 86    debug: bool = False,
 87    **kwargs: Any
 88) -> SuccessTuple:
 89    """
 90    Compress a pipe's target table to reduce disk usage.
 91
 92    For TimescaleDB, enables the Hypercore columnstore, installs a columnstore (compression)
 93    policy (so future synced chunks are converted automatically), and converts any existing
 94    uncompressed chunks now. For MySQL/MariaDB and MSSQL, applies the flavor's native table
 95    compression. Other flavors are unsupported.
 96
 97    Parameters
 98    ----------
 99    pipe: mrsm.Pipe
100        The pipe whose target table to compress.
101
102    no_policy: bool, default False
103        If `True` (TimescaleDB only), compress existing chunks now without installing an ongoing
104        columnstore (compression) policy. Any pre-existing policy is left untouched.
105
106    debug: bool, default False
107        Verbosity toggle.
108
109    Returns
110    -------
111    A `SuccessTuple` indicating success, including the amount of disk reclaimed.
112    """
113    return self._rust_method('compress_pipe')(pipe, no_policy=no_policy)

Compress a pipe's target table to reduce disk usage.

For TimescaleDB, enables the Hypercore columnstore, installs a columnstore (compression) policy (so future synced chunks are converted automatically), and converts any existing uncompressed chunks now. For MySQL/MariaDB and MSSQL, applies the flavor's native table compression. Other flavors are unsupported.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to compress.
  • no_policy (bool, default False): If True (TimescaleDB only), compress existing chunks now without installing an ongoing columnstore (compression) policy. Any pre-existing policy is left untouched.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SuccessTuple indicating success, including the amount of disk reclaimed.
def decompress_pipe( self, pipe: meerschaum.Pipe, no_policy: bool = False, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
116def decompress_pipe(
117    self,
118    pipe: mrsm.Pipe,
119    no_policy: bool = False,
120    debug: bool = False,
121    **kwargs: Any
122) -> SuccessTuple:
123    """
124    Decompress a pipe's target table, the inverse of `compress_pipe()`.
125
126    For TimescaleDB, removes the columnstore (compression) policy, converts every compressed
127    chunk back to row-store, and disables the columnstore so future synced chunks stay
128    uncompressed. For MySQL/MariaDB and MSSQL, reverts the flavor's native table compression.
129    Other flavors are unsupported.
130
131    Parameters
132    ----------
133    pipe: mrsm.Pipe
134        The pipe whose target table to decompress.
135
136    no_policy: bool, default False
137        If `True` (TimescaleDB only), decompress existing chunks now but leave the columnstore
138        (compression) policy in place — chunks will be recompressed on the policy's schedule.
139        Useful to temporarily decompress for a bulk backfill without disabling compression.
140
141    debug: bool, default False
142        Verbosity toggle.
143
144    Returns
145    -------
146    A `SuccessTuple` indicating success, including the change in disk size.
147    """
148    return self._rust_method('decompress_pipe')(pipe, no_policy=no_policy)

Decompress a pipe's target table, the inverse of compress_pipe().

For TimescaleDB, removes the columnstore (compression) policy, converts every compressed chunk back to row-store, and disables the columnstore so future synced chunks stay uncompressed. For MySQL/MariaDB and MSSQL, reverts the flavor's native table compression. Other flavors are unsupported.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to decompress.
  • no_policy (bool, default False): If True (TimescaleDB only), decompress existing chunks now but leave the columnstore (compression) policy in place, chunks will be recompressed on the policy's schedule. Useful to temporarily decompress for a bulk backfill without disabling compression.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SuccessTuple indicating success, including the change in disk size.
def apply_compression_policy( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
57def apply_compression_policy(
58    self,
59    pipe: mrsm.Pipe,
60    debug: bool = False,
61    **kwargs: Any
62) -> SuccessTuple:
63    """
64    Idempotently enable compression and install a compression policy for a pipe.
65
66    Intended to be called automatically (e.g. after a sync) when `pipe.compress` is set.
67    Only TimescaleDB hypertables are affected; all other flavors are a no-op success.
68    Failures are non-fatal and never raise.
69
70    Parameters
71    ----------
72    pipe: mrsm.Pipe
73        The pipe whose target table should have a compression policy.
74
75    Returns
76    -------
77    A `SuccessTuple` indicating success.
78    """
79    return self._rust_method('apply_compression_policy')(pipe)

Idempotently enable compression and install a compression policy for a pipe.

Intended to be called automatically (e.g. after a sync) when pipe.compress is set. Only TimescaleDB hypertables are affected; all other flavors are a no-op success. Failures are non-fatal and never raise.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table should have a compression policy.
Returns
  • A SuccessTuple indicating success.
def set_integer_now_func(self, pipe: meerschaum.Pipe, debug: bool = False) -> bool:
45def set_integer_now_func(
46    self,
47    pipe: mrsm.Pipe,
48    debug: bool = False,
49) -> bool:
50    """
51    Register an `integer_now` function if the pipe's datetime axis is an integer.
52    Returns `True` when nothing needed doing or the function was registered.
53    """
54    return self._rust_method('set_integer_now_func')(pipe)

Register an integer_now function if the pipe's datetime axis is an integer. Returns True when nothing needed doing or the function was registered.

def vacuum_pipe( self, pipe: meerschaum.Pipe, full: bool = False, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
18def vacuum_pipe(
19    self,
20    pipe: mrsm.Pipe,
21    full: bool = False,
22    debug: bool = False,
23    **kwargs: Any
24) -> SuccessTuple:
25    """
26    Reclaim dead-tuple disk space from a pipe's target table.
27
28    PostgreSQL-family tables run `VACUUM` (optionally `VACUUM FULL`); TimescaleDB hypertables
29    recurse into their chunks; MySQL/MariaDB run `OPTIMIZE TABLE`; MSSQL rebuilds the table;
30    SQLite vacuums the whole database file.
31
32    Parameters
33    ----------
34    pipe: mrsm.Pipe
35        The pipe whose target table to vacuum.
36
37    full: bool, default False
38        If `True` (PostgreSQL family only), run `VACUUM FULL`, which rewrites the table and
39        returns freed space to the operating system at the cost of an exclusive lock.
40
41    debug: bool, default False
42        Verbosity toggle.
43
44    Returns
45    -------
46    A `SuccessTuple` indicating success, including the amount of disk reclaimed.
47    """
48    return self._rust_method('vacuum_pipe')(pipe, full=full)

Reclaim dead-tuple disk space from a pipe's target table.

PostgreSQL-family tables run VACUUM (optionally VACUUM FULL); TimescaleDB hypertables recurse into their chunks; MySQL/MariaDB run OPTIMIZE TABLE; MSSQL rebuilds the table; SQLite vacuums the whole database file.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to vacuum.
  • full (bool, default False): If True (PostgreSQL family only), run VACUUM FULL, which rewrites the table and returns freed space to the operating system at the cost of an exclusive lock.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SuccessTuple indicating success, including the amount of disk reclaimed.
def analyze_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
51def analyze_pipe(
52    self,
53    pipe: mrsm.Pipe,
54    debug: bool = False,
55    **kwargs: Any
56) -> SuccessTuple:
57    """
58    Refresh the database planner's statistics for a pipe's target table.
59
60    This does not reclaim disk space; it helps the query planner choose better plans after
61    large syncs. PostgreSQL/SQLite run `ANALYZE`, MySQL/MariaDB run `ANALYZE TABLE`, and MSSQL
62    runs `UPDATE STATISTICS`.
63
64    Parameters
65    ----------
66    pipe: mrsm.Pipe
67        The pipe whose target table to analyze.
68
69    debug: bool, default False
70        Verbosity toggle.
71
72    Returns
73    -------
74    A `SuccessTuple` indicating success.
75    """
76    return self._rust_method('analyze_pipe')(pipe)

Refresh the database planner's statistics for a pipe's target table.

This does not reclaim disk space; it helps the query planner choose better plans after large syncs. PostgreSQL/SQLite run ANALYZE, MySQL/MariaDB run ANALYZE TABLE, and MSSQL runs UPDATE STATISTICS.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to analyze.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SuccessTuple indicating success.
def get_partition_info(self, pipe: meerschaum.Pipe, debug: bool = False) -> dict:
25def get_partition_info(self, pipe: mrsm.Pipe, debug: bool = False) -> dict:
26    """
27    Return a summary of a pipe's target table partitioning for `show partitions`.
28
29    Keys:
30    - `flavor`: the connector flavor.
31    - `partitioned`: whether the table is range-partitioned (native) or a TimescaleDB hypertable.
32    - `count`: the number of partitions / chunks (`None` if unknown).
33    - `interval`: the physical partition width (`timedelta`, epoch-`int`, or `None`).
34    """
35    return self._rust_method('get_partition_info')(pipe)

Return a summary of a pipe's target table partitioning for show partitions.

Keys:

  • flavor: the connector flavor.
  • partitioned: whether the table is range-partitioned (native) or a TimescaleDB hypertable.
  • count: the number of partitions / chunks (None if unknown).
  • interval: the physical partition width (timedelta, epoch-int, or None).
def partition_pipe( self, pipe: meerschaum.Pipe, chunk_minutes: int | None = None, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
38def partition_pipe(
39    self,
40    pipe: mrsm.Pipe,
41    chunk_minutes: Optional[int] = None,
42    debug: bool = False,
43    **kwargs: Any
44) -> SuccessTuple:
45    """
46    Rebuild a pipe's target table to a new partition (chunk) width.
47
48    The width is taken from `chunk_minutes` if provided, else the pipe's configured
49    `verify.chunk_minutes`. The new width is persisted to `verify.chunk_minutes`, which is the
50    authoritative partition width (see `Pipe.get_chunk_interval`).
51
52    Strategy by flavor:
53
54    - **TimescaleDB**: call `set_chunk_time_interval()`. This changes the width of FUTURE chunks
55      only; existing chunks are not rewritten.
56    - **PostgreSQL / PostGIS, MySQL / MariaDB, MSSQL**: rebuild the table by reading its data,
57      dropping it, and re-syncing at the new width. This reuses the tested `create_pipe_table_from_df`
58      and `_create_missing_partitions` paths, and (for MSSQL) frees the partition function/scheme
59      names so they can be recreated. The whole table is read into memory; for very large tables
60      consider a manual chunked rebuild.
61
62    Parameters
63    ----------
64    pipe: mrsm.Pipe
65        The partitioned pipe whose target table to repartition.
66
67    chunk_minutes: Optional[int], default None
68        The new partition width in minutes. Defaults to the pipe's `verify.chunk_minutes`.
69
70    debug: bool, default False
71        Verbosity toggle.
72
73    Returns
74    -------
75    A `SuccessTuple` indicating success.
76    """
77    result = self._rust_method('partition_pipe')(pipe, chunk_minutes=chunk_minutes)
78    if result[0] and not pipe.temporary:
79        ### v4 sets `verify.chunk_minutes` on the Python pipe (`update_parameters`);
80        ### Rust persisted it, so mirror the registered value in memory.
81        verify = (
82            (self.get_pipe_attributes(pipe, debug=debug) or {})
83            .get('parameters', {})
84            .get('verify', None)
85        )
86        if isinstance(verify, dict) and 'chunk_minutes' in verify:
87            pipe.update_parameters(
88                {'verify': {'chunk_minutes': verify['chunk_minutes']}},
89                persist=False,
90                debug=debug,
91            )
92    return result

Rebuild a pipe's target table to a new partition (chunk) width.

The width is taken from chunk_minutes if provided, else the pipe's configured verify.chunk_minutes. The new width is persisted to verify.chunk_minutes, which is the authoritative partition width (see Pipe.get_chunk_interval).

Strategy by flavor:

  • TimescaleDB: call set_chunk_time_interval(). This changes the width of FUTURE chunks only; existing chunks are not rewritten.
  • PostgreSQL / PostGIS, MySQL / MariaDB, MSSQL: rebuild the table by reading its data, dropping it, and re-syncing at the new width. This reuses the tested create_pipe_table_from_df and _create_missing_partitions paths, and (for MSSQL) frees the partition function/scheme names so they can be recreated. The whole table is read into memory; for very large tables consider a manual chunked rebuild.
Parameters
  • pipe (mrsm.Pipe): The partitioned pipe whose target table to repartition.
  • chunk_minutes (Optional[int], default None): The new partition width in minutes. Defaults to the pipe's verify.chunk_minutes.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SuccessTuple indicating success.
def fetch_pipes_keys( self, connector_keys: List[str] | None = None, metric_keys: List[str] | None = None, location_keys: List[str] | None = None, tags: List[str] | None = None, params: Dict[str, Any] | None = None, debug: bool = False) -> Dict[int, Tuple[str, str, str | None, Dict[str, Any]]]:
 58def fetch_pipes_keys(
 59    self,
 60    connector_keys: Optional[List[str]] = None,
 61    metric_keys: Optional[List[str]] = None,
 62    location_keys: Optional[List[str]] = None,
 63    tags: Optional[List[str]] = None,
 64    params: Optional[Dict[str, Any]] = None,
 65    debug: bool = False,
 66) -> Dict[
 67        int, Tuple[str, str, Union[str, None], Dict[str, Any]]
 68    ]:
 69    """
 70    Return a dictionary mapping pipe IDs to key tuples corresponding to the parameters provided.
 71
 72    Parameters
 73    ----------
 74    connector_keys: Optional[List[str]], default None
 75        List of connector_keys to search by.
 76
 77    metric_keys: Optional[List[str]], default None
 78        List of metric_keys to search by.
 79
 80    location_keys: Optional[List[str]], default None
 81        List of location_keys to search by.
 82
 83    tags: Optional[List[str]], default None
 84        List of pipes to search by.
 85
 86    params: Optional[Dict[str, Any]], default None
 87        Dictionary of additional parameters to search by.
 88        E.g. `--params pipe_id:1`
 89
 90    debug: bool, default False
 91        Verbosity toggle.
 92
 93    Returns
 94    -------
 95    A list of tuples of pipes' keys and parameters (connector_keys, metric_key, location_key, parameters).
 96    """
 97    return self._rust_method('fetch_pipes_keys')(
 98        connector_keys=connector_keys,
 99        metric_keys=metric_keys,
100        location_keys=location_keys,
101        tags=tags,
102        params=params,
103    )

Return a dictionary mapping pipe IDs to key tuples corresponding to the parameters provided.

Parameters
  • connector_keys (Optional[List[str]], default None): List of connector_keys to search by.
  • metric_keys (Optional[List[str]], default None): List of metric_keys to search by.
  • location_keys (Optional[List[str]], default None): List of location_keys to search by.
  • tags (Optional[List[str]], default None): List of pipes to search by.
  • params (Optional[Dict[str, Any]], default None): Dictionary of additional parameters to search by. E.g. --params pipe_id:1
  • debug (bool, default False): Verbosity toggle.
Returns
  • A list of tuples of pipes' keys and parameters (connector_keys, metric_key, location_key, parameters).
def create_indices( self, pipe: meerschaum.Pipe, columns: List[str] | None = None, indices: List[str] | None = None, debug: bool = False) -> bool:
118def create_indices(
119    self,
120    pipe: mrsm.Pipe,
121    columns: Optional[List[str]] = None,
122    indices: Optional[List[str]] = None,
123    debug: bool = False
124) -> bool:
125    """
126    Create a pipe's indices.
127    """
128    if pipe.__dict__.get('_skip_check_indices', False):
129        return True
130    return self._rust_method('create_indices')(pipe, columns=columns, indices=indices)

Create a pipe's indices.

def drop_indices( self, pipe: meerschaum.Pipe, columns: List[str] | None = None, indices: List[str] | None = None, debug: bool = False) -> bool:
145def drop_indices(
146    self,
147    pipe: mrsm.Pipe,
148    columns: Optional[List[str]] = None,
149    indices: Optional[List[str]] = None,
150    debug: bool = False
151) -> bool:
152    """
153    Drop a pipe's indices.
154    """
155    return self._rust_method('drop_indices')(pipe, columns=columns, indices=indices)

Drop a pipe's indices.

def get_create_index_queries( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, List[str]]:
168def get_create_index_queries(
169    self,
170    pipe: mrsm.Pipe,
171    debug: bool = False,
172) -> Dict[str, List[str]]:
173    """
174    Return a dictionary mapping columns to a `CREATE INDEX` or equivalent query.
175
176    Parameters
177    ----------
178    pipe: mrsm.Pipe
179        The pipe to which the queries will correspond.
180
181    Returns
182    -------
183    A dictionary of index names mapping to lists of queries.
184    """
185    return self._rust_method('get_create_index_queries')(pipe)

Return a dictionary mapping columns to a CREATE INDEX or equivalent query.

Parameters
  • pipe (mrsm.Pipe): The pipe to which the queries will correspond.
Returns
  • A dictionary of index names mapping to lists of queries.
def get_drop_index_queries( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, List[str]]:
188def get_drop_index_queries(
189    self,
190    pipe: mrsm.Pipe,
191    debug: bool = False,
192) -> Dict[str, List[str]]:
193    """
194    Return a dictionary mapping columns to a `DROP INDEX` or equivalent query.
195
196    Parameters
197    ----------
198    pipe: mrsm.Pipe
199        The pipe to which the queries will correspond.
200
201    Returns
202    -------
203    A dictionary of column names mapping to lists of queries.
204    """
205    return self._rust_method('get_drop_index_queries')(pipe)

Return a dictionary mapping columns to a DROP INDEX or equivalent query.

Parameters
  • pipe (mrsm.Pipe): The pipe to which the queries will correspond.
Returns
  • A dictionary of column names mapping to lists of queries.
def delete_pipe( self, pipe: meerschaum.Pipe, debug: bool = False) -> Tuple[bool, str]:
208def delete_pipe(
209    self,
210    pipe: mrsm.Pipe,
211    debug: bool = False,
212) -> SuccessTuple:
213    """
214    Delete a Pipe's registration.
215    """
216    return self._rust_method('delete_pipe')(pipe)

Delete a Pipe's registration.

def get_pipe_data( self, pipe: meerschaum.Pipe, select_columns: List[str] | None = None, omit_columns: List[str] | None = None, begin: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None, params: Dict[str, Any] | None = None, order: str = 'asc', limit: int | None = None, begin_add_minutes: int = 0, end_add_minutes: int = 0, chunksize: int | None = -1, as_iterator: bool = False, as_polars: bool | None = None, as_pandas: bool | None = None, debug: bool = False, **kw: Any) -> pandas.DataFrame | None:
219def get_pipe_data(
220    self,
221    pipe: mrsm.Pipe,
222    select_columns: Optional[List[str]] = None,
223    omit_columns: Optional[List[str]] = None,
224    begin: Union[datetime, str, None] = None,
225    end: Union[datetime, str, None] = None,
226    params: Optional[Dict[str, Any]] = None,
227    order: str = 'asc',
228    limit: Optional[int] = None,
229    begin_add_minutes: int = 0,
230    end_add_minutes: int = 0,
231    chunksize: Optional[int] = -1,
232    as_iterator: bool = False,
233    as_polars: Optional[bool] = None,
234    as_pandas: Optional[bool] = None,
235    debug: bool = False,
236    **kw: Any
237) -> Union[pd.DataFrame, None]:
238    """
239    Access a pipe's data from the SQL instance.
240
241    Parameters
242    ----------
243    pipe: mrsm.Pipe:
244        The pipe to get data from.
245
246    select_columns: Optional[List[str]], default None
247        If provided, only select these given columns.
248        Otherwise select all available columns (i.e. `SELECT *`).
249
250    omit_columns: Optional[List[str]], default None
251        If provided, remove these columns from the selection.
252
253    begin: Union[datetime, str, None], default None
254        If provided, get rows newer than or equal to this value.
255
256    end: Union[datetime, str, None], default None
257        If provided, get rows older than or equal to this value.
258
259    params: Optional[Dict[str, Any]], default None
260        Additional parameters to filter by.
261        See `meerschaum.connectors.sql.build_where`.
262
263    order: Optional[str], default 'asc'
264        The selection order for all of the indices in the query.
265        If `None`, omit the `ORDER BY` clause.
266
267    limit: Optional[int], default None
268        If specified, limit the number of rows retrieved to this value.
269
270    begin_add_minutes: int, default 0
271        The number of minutes to add to the `begin` datetime (i.e. `DATEADD`).
272
273    end_add_minutes: int, default 0
274        The number of minutes to add to the `end` datetime (i.e. `DATEADD`).
275
276    chunksize: Optional[int], default -1
277        The size of dataframe chunks to load into memory.
278
279    as_iterator: bool, default False
280        If `True`, return the chunks iterator directly.
281
282    as_polars: Optional[bool], default None
283        `True` returns Polars, `False` pandas; `None` follows the `dataframe` setting
284        (`meerschaum.utils.dataframe.get_dataframe_library()`).
285
286    as_pandas: Optional[bool], default None
287        `True` returns pandas, `False` Polars; `None` follows the setting.
288
289    debug: bool, default False
290        Verbosity toggle.
291
292    Returns
293    -------
294    A DataFrame of the requested library (an iterator of them with `as_iterator`), or `None`
295    when the table does not exist.
296
297    """
298    from meerschaum.utils.dataframe import wants_polars
299    polars = wants_polars(as_polars=as_polars, as_pandas=as_pandas)
300    result = self._rust_method('get_pipe_data')(
301        pipe,
302        select_columns=select_columns,
303        omit_columns=omit_columns,
304        begin=begin,
305        end=end,
306        params=params,
307        order=order,
308        limit=limit,
309        begin_add_minutes=begin_add_minutes,
310        end_add_minutes=end_add_minutes,
311        chunksize=chunksize,
312        as_iterator=as_iterator,
313        as_polars=polars,
314        **kw
315    )
316    if polars and as_iterator and result is not None:
317        ### The binding's chunks are pandas frames: the helper converts each to Polars.
318        from meerschaum.utils.dataframe import frame_from_arrow
319        return (frame_from_arrow(chunk, as_polars=True) for chunk in result)
320    return result

Access a pipe's data from the SQL instance.

Parameters
  • pipe (mrsm.Pipe:): The pipe to get data from.
  • select_columns (Optional[List[str]], default None): If provided, only select these given columns. Otherwise select all available columns (i.e. SELECT *).
  • omit_columns (Optional[List[str]], default None): If provided, remove these columns from the selection.
  • begin (Union[datetime, str, None], default None): If provided, get rows newer than or equal to this value.
  • end (Union[datetime, str, None], default None): If provided, get rows older than or equal to this value.
  • params (Optional[Dict[str, Any]], default None): Additional parameters to filter by. See meerschaum.connectors.sql.build_where.
  • order (Optional[str], default 'asc'): The selection order for all of the indices in the query. If None, omit the ORDER BY clause.
  • limit (Optional[int], default None): If specified, limit the number of rows retrieved to this value.
  • begin_add_minutes (int, default 0): The number of minutes to add to the begin datetime (i.e. DATEADD).
  • end_add_minutes (int, default 0): The number of minutes to add to the end datetime (i.e. DATEADD).
  • chunksize (Optional[int], default -1): The size of dataframe chunks to load into memory.
  • as_iterator (bool, default False): If True, return the chunks iterator directly.
  • as_polars (Optional[bool], default None): True returns Polars, False pandas; None follows the dataframe setting (meerschaum.utils.dataframe.get_dataframe_library()).
  • as_pandas (Optional[bool], default None): True returns pandas, False Polars; None follows the setting.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A DataFrame of the requested library (an iterator of them with as_iterator), or None
  • when the table does not exist.
def get_pipe_docs( self, pipe: meerschaum.Pipe, select_columns: List[str] | None = None, omit_columns: List[str] | None = None, begin: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None, params: Dict[str, Any] | None = None, order: str = 'asc', limit: int | None = None, debug: bool = False, **kw: Any) -> List[Dict[str, Any]]:
323def get_pipe_docs(
324    self,
325    pipe: mrsm.Pipe,
326    select_columns: Optional[List[str]] = None,
327    omit_columns: Optional[List[str]] = None,
328    begin: Union[datetime, str, None] = None,
329    end: Union[datetime, str, None] = None,
330    params: Optional[Dict[str, Any]] = None,
331    order: str = 'asc',
332    limit: Optional[int] = None,
333    debug: bool = False,
334    **kw: Any
335) -> List[Dict[str, Any]]:
336    """
337    Return a pipe's data as a list of dictionaries, bypassing pandas overhead.
338    """
339    return self._rust_method('get_pipe_docs')(
340        pipe,
341        select_columns=select_columns,
342        omit_columns=omit_columns,
343        begin=begin,
344        end=end,
345        params=params,
346        order=order,
347        limit=limit,
348    )

Return a pipe's data as a list of dictionaries, bypassing pandas overhead.

def get_pipe_data_query( self, pipe: meerschaum.Pipe, select_columns: List[str] | None = None, omit_columns: List[str] | None = None, begin: datetime.datetime | int | str | None = None, end: datetime.datetime | int | str | None = None, params: Dict[str, Any] | None = None, order: str | None = 'asc', sort_datetimes: bool = False, limit: int | None = None, begin_add_minutes: int = 0, end_add_minutes: int = 0, replace_nulls: str | None = None, skip_existing_cols_check: bool = False, debug: bool = False, **kw: Any) -> str | None:
351def get_pipe_data_query(
352    self,
353    pipe: mrsm.Pipe,
354    select_columns: Optional[List[str]] = None,
355    omit_columns: Optional[List[str]] = None,
356    begin: Union[datetime, int, str, None] = None,
357    end: Union[datetime, int, str, None] = None,
358    params: Optional[Dict[str, Any]] = None,
359    order: Optional[str] = 'asc',
360    sort_datetimes: bool = False,
361    limit: Optional[int] = None,
362    begin_add_minutes: int = 0,
363    end_add_minutes: int = 0,
364    replace_nulls: Optional[str] = None,
365    skip_existing_cols_check: bool = False,
366    debug: bool = False,
367    **kw: Any
368) -> Union[str, None]:
369    """
370    Return the `SELECT` query for retrieving a pipe's data from its instance.
371
372    Parameters
373    ----------
374    pipe: mrsm.Pipe:
375        The pipe to get data from.
376
377    select_columns: Optional[List[str]], default None
378        If provided, only select these given columns.
379        Otherwise select all available columns (i.e. `SELECT *`).
380
381    omit_columns: Optional[List[str]], default None
382        If provided, remove these columns from the selection.
383
384    begin: Union[datetime, int, str, None], default None
385        If provided, get rows newer than or equal to this value.
386
387    end: Union[datetime, str, None], default None
388        If provided, get rows older than or equal to this value.
389
390    params: Optional[Dict[str, Any]], default None
391        Additional parameters to filter by.
392        See `meerschaum.connectors.sql.build_where`.
393
394    order: Optional[str], default None
395        The selection order for all of the indices in the query.
396        If `None`, omit the `ORDER BY` clause.
397
398    sort_datetimes: bool, default False
399        Alias for `order='desc'`.
400
401    limit: Optional[int], default None
402        If specified, limit the number of rows retrieved to this value.
403
404    begin_add_minutes: int, default 0
405        The number of minutes to add to the `begin` datetime (i.e. `DATEADD`).
406
407    end_add_minutes: int, default 0
408        The number of minutes to add to the `end` datetime (i.e. `DATEADD`).
409
410    chunksize: Optional[int], default -1
411        The size of dataframe chunks to load into memory.
412
413    replace_nulls: Optional[str], default None
414        If provided, replace null values with this value.
415
416    skip_existing_cols_check: bool, default False
417        If `True`, do not verify that querying columns are actually on the table.
418
419    debug: bool, default False
420        Verbosity toggle.
421
422    Returns
423    -------
424    A `SELECT` query to retrieve a pipe's data.
425    """
426    return self._rust_method('get_pipe_data_query')(
427        pipe,
428        select_columns=select_columns,
429        omit_columns=omit_columns,
430        begin=begin,
431        end=end,
432        params=params,
433        order=order,
434        sort_datetimes=sort_datetimes,
435        limit=limit,
436        begin_add_minutes=begin_add_minutes,
437        end_add_minutes=end_add_minutes,
438        replace_nulls=replace_nulls,
439        skip_existing_cols_check=skip_existing_cols_check,
440    )

Return the SELECT query for retrieving a pipe's data from its instance.

Parameters
  • pipe (mrsm.Pipe:): The pipe to get data from.
  • select_columns (Optional[List[str]], default None): If provided, only select these given columns. Otherwise select all available columns (i.e. SELECT *).
  • omit_columns (Optional[List[str]], default None): If provided, remove these columns from the selection.
  • begin (Union[datetime, int, str, None], default None): If provided, get rows newer than or equal to this value.
  • end (Union[datetime, str, None], default None): If provided, get rows older than or equal to this value.
  • params (Optional[Dict[str, Any]], default None): Additional parameters to filter by. See meerschaum.connectors.sql.build_where.
  • order (Optional[str], default None): The selection order for all of the indices in the query. If None, omit the ORDER BY clause.
  • sort_datetimes (bool, default False): Alias for order='desc'.
  • limit (Optional[int], default None): If specified, limit the number of rows retrieved to this value.
  • begin_add_minutes (int, default 0): The number of minutes to add to the begin datetime (i.e. DATEADD).
  • end_add_minutes (int, default 0): The number of minutes to add to the end datetime (i.e. DATEADD).
  • chunksize (Optional[int], default -1): The size of dataframe chunks to load into memory.
  • replace_nulls (Optional[str], default None): If provided, replace null values with this value.
  • skip_existing_cols_check (bool, default False): If True, do not verify that querying columns are actually on the table.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SELECT query to retrieve a pipe's data.
def register_pipe( self, pipe: meerschaum.Pipe, debug: bool = False) -> Tuple[bool, str]:
23def register_pipe(
24    self,
25    pipe: mrsm.Pipe,
26    debug: bool = False,
27) -> SuccessTuple:
28    """
29    Register a new pipe.
30    A pipe's attributes must be set before registering.
31    """
32    return self._rust_method('register_pipe')(pipe)

Register a new pipe. A pipe's attributes must be set before registering.

def edit_pipe( self, pipe: meerschaum.Pipe, patch: bool = False, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
35def edit_pipe(
36    self,
37    pipe: mrsm.Pipe,
38    patch: bool = False,
39    debug: bool = False,
40    **kw : Any
41) -> SuccessTuple:
42    """
43    Persist a Pipe's parameters to its database.
44
45    Parameters
46    ----------
47    pipe: mrsm.Pipe, default None
48        The pipe to be edited.
49    patch: bool, default False
50        If patch is `True`, update the existing parameters by cascading.
51        Otherwise overwrite the parameters (default).
52    debug: bool, default False
53        Verbosity toggle.
54    """
55    return self._rust_method('edit_pipe')(pipe, patch=patch)

Persist a Pipe's parameters to its database.

Parameters
  • pipe (mrsm.Pipe, default None): The pipe to be edited.
  • patch (bool, default False): If patch is True, update the existing parameters by cascading. Otherwise overwrite the parameters (default).
  • debug (bool, default False): Verbosity toggle.
def get_pipe_id(self, pipe: meerschaum.Pipe, debug: bool = False) -> Any:
443def get_pipe_id(
444    self,
445    pipe: mrsm.Pipe,
446    debug: bool = False,
447) -> Any:
448    """
449    Get a Pipe's ID from the pipes table.
450    """
451    return self._rust_method('get_pipe_id')(pipe)

Get a Pipe's ID from the pipes table.

def get_pipe_attributes( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, Any]:
454def get_pipe_attributes(
455    self,
456    pipe: mrsm.Pipe,
457    debug: bool = False,
458) -> Dict[str, Any]:
459    """
460    Get a Pipe's attributes dictionary.
461    """
462    return self._rust_method('get_pipe_attributes')(pipe)

Get a Pipe's attributes dictionary.

def sync_pipe( self, pipe: meerschaum.Pipe, df: pandas.DataFrame | str | Dict[Any, Any] | None = None, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, chunksize: int | None = -1, check_existing: bool = True, blocking: bool = True, debug: bool = False, _check_temporary_tables: bool = True, **kw: Any) -> Tuple[bool, str]:
465def sync_pipe(
466    self,
467    pipe: mrsm.Pipe,
468    df: Union[pd.DataFrame, str, Dict[Any, Any], None] = None,
469    begin: Union[datetime, int, None] = None,
470    end: Union[datetime, int, None] = None,
471    chunksize: Optional[int] = -1,
472    check_existing: bool = True,
473    blocking: bool = True,
474    debug: bool = False,
475    _check_temporary_tables: bool = True,
476    **kw: Any
477) -> SuccessTuple:
478    """
479    Sync a pipe using a database connection.
480
481    Parameters
482    ----------
483    pipe: mrsm.Pipe
484        The Meerschaum Pipe instance into which to sync the data.
485
486    df: Union[pandas.DataFrame, str, Dict[Any, Any], List[Dict[str, Any]]]
487        An optional DataFrame or equivalent to sync into the pipe.
488        Defaults to `None`.
489
490    begin: Union[datetime, int, None], default None
491        Optionally specify the earliest datetime to search for data.
492        Defaults to `None`.
493
494    end: Union[datetime, int, None], default None
495        Optionally specify the latest datetime to search for data.
496        Defaults to `None`.
497
498    chunksize: Optional[int], default -1
499        Specify the number of rows to sync per chunk.
500        If `-1`, resort to system configuration (default is `900`).
501        A `chunksize` of `None` will sync all rows in one transaction.
502        Defaults to `-1`.
503
504    check_existing: bool, default True
505        If `True`, pull and diff with existing data from the pipe. Defaults to `True`.
506
507    blocking: bool, default True
508        If `True`, wait for sync to finish and return its result, otherwise asyncronously sync.
509        Defaults to `True`.
510
511    debug: bool, default False
512        Verbosity toggle. Defaults to False.
513
514    kw: Any
515        Catch-all for keyword arguments. `replace=True` replaces the pipe's rows of
516        `[begin, end)` (matching `params`) with the rows of `df` in one transaction; a bound
517        that is not given is the rows' own datetime range (`begin` = the earliest, `end` = just
518        after the latest), as for `pipe.sync(df, replace=True)`.
519
520    Returns
521    -------
522    A `SuccessTuple` of success (`bool`) and message (`str`).
523    """
524    if df is not None and type(df).__module__.split('.', maxsplit=1)[0] != 'pandas':
525        df = pipe.enforce_dtypes(
526            df,
527            chunksize=chunksize,
528            safe_copy=kw.get('safe_copy', False),
529            dtypes=pipe.get_dtypes(debug=debug),
530            debug=debug,
531        )
532    replace_kw = (
533        {'replace': True, 'params': kw.get('params', None)}
534        if kw.get('replace', False)
535        else {}
536    )
537    result = self._rust_method('sync_pipe')(
538        pipe,
539        df,
540        begin=begin,
541        end=end,
542        chunksize=chunksize,
543        check_existing=check_existing,
544        **replace_kw
545    )
546    if hasattr(pipe, '_clear_cache_key'):
547        for cache_key in (
548            '_exists', '_columns_types', '_columns_types_timestamp',
549            '_columns_indices', '_columns_indices_timestamp',
550        ):
551            pipe._clear_cache_key(cache_key, debug=debug)
552    return result

Sync a pipe using a database connection.

Parameters
  • pipe (mrsm.Pipe): The Meerschaum Pipe instance into which to sync the data.
  • df (Union[pandas.DataFrame, str, Dict[Any, Any], List[Dict[str, Any]]]): An optional DataFrame or equivalent to sync into the pipe. Defaults to None.
  • begin (Union[datetime, int, None], default None): Optionally specify the earliest datetime to search for data. Defaults to None.
  • end (Union[datetime, int, None], default None): Optionally specify the latest datetime to search for data. Defaults to None.
  • chunksize (Optional[int], default -1): Specify the number of rows to sync per chunk. If -1, resort to system configuration (default is 900). A chunksize of None will sync all rows in one transaction. Defaults to -1.
  • check_existing (bool, default True): If True, pull and diff with existing data from the pipe. Defaults to True.
  • blocking (bool, default True): If True, wait for sync to finish and return its result, otherwise asyncronously sync. Defaults to True.
  • debug (bool, default False): Verbosity toggle. Defaults to False.
  • kw (Any): Catch-all for keyword arguments. replace=True replaces the pipe's rows of [begin, end) (matching params) with the rows of df in one transaction; a bound that is not given is the rows' own datetime range (begin = the earliest, end = just after the latest), as for pipe.sync(df, replace=True).
Returns
  • A SuccessTuple of success (bool) and message (str).
def sync_pipe_inplace( self, pipe: meerschaum.Pipe, params: Dict[str, Any] | None = None, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, chunksize: int | None = -1, check_existing: bool = True, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
555def sync_pipe_inplace(
556    self,
557    pipe: 'mrsm.Pipe',
558    params: Optional[Dict[str, Any]] = None,
559    begin: Union[datetime, int, None] = None,
560    end: Union[datetime, int, None] = None,
561    chunksize: Optional[int] = -1,
562    check_existing: bool = True,
563    debug: bool = False,
564    **kw: Any
565) -> SuccessTuple:
566    """
567    If a pipe's connector is the same as its instance connector,
568    it's more efficient to sync the pipe in-place rather than reading data into Pandas.
569
570    Parameters
571    ----------
572    pipe: mrsm.Pipe
573        The pipe whose connector is the same as its instance.
574
575    params: Optional[Dict[str, Any]], default None
576        Optional params dictionary to build the `WHERE` clause.
577        See `meerschaum.utils.sql.build_where`.
578
579    begin: Union[datetime, int, None], default None
580        Optionally specify the earliest datetime to search for data.
581        Defaults to `None`.
582
583    end: Union[datetime, int, None], default None
584        Optionally specify the latest datetime to search for data.
585        Defaults to `None`.
586
587    chunksize: Optional[int], default -1
588        Specify the number of rows to sync per chunk.
589        If `-1`, resort to system configuration (default is `900`).
590        A `chunksize` of `None` will sync all rows in one transaction.
591        Defaults to `-1`.
592
593    check_existing: bool, default True
594        If `True`, pull and diff with existing data from the pipe.
595
596    debug: bool, default False
597        Verbosity toggle.
598
599    Returns
600    -------
601    A SuccessTuple.
602    """
603    if self.flavor == 'duckdb':
604        return pipe.sync(
605            params=params,
606            begin=begin,
607            end=end,
608            chunksize=chunksize,
609            check_existing=check_existing,
610            debug=debug,
611            _inplace=False,
612            **kw
613        )
614    from meerschaum import _core
615    result = _core.sync_pipe_inplace(
616        self._core_connector(),
617        pipe,
618        params,
619        begin,
620        end,
621        check_existing,
622    )
623    ### The in-place sync may have added or retyped columns behind the pipe's cache.
624    pipe._clear_cache_key('_columns_types', debug=debug)
625    pipe._clear_cache_key('_columns_indices', debug=debug)
626    return result

If a pipe's connector is the same as its instance connector, it's more efficient to sync the pipe in-place rather than reading data into Pandas.

Parameters
  • pipe (mrsm.Pipe): The pipe whose connector is the same as its instance.
  • params (Optional[Dict[str, Any]], default None): Optional params dictionary to build the WHERE clause. See meerschaum.utils.sql.build_where.
  • begin (Union[datetime, int, None], default None): Optionally specify the earliest datetime to search for data. Defaults to None.
  • end (Union[datetime, int, None], default None): Optionally specify the latest datetime to search for data. Defaults to None.
  • chunksize (Optional[int], default -1): Specify the number of rows to sync per chunk. If -1, resort to system configuration (default is 900). A chunksize of None will sync all rows in one transaction. Defaults to -1.
  • check_existing (bool, default True): If True, pull and diff with existing data from the pipe.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SuccessTuple.
def get_sync_time( self, pipe: meerschaum.Pipe, params: Dict[str, Any] | None = None, newest: bool = True, remote: bool = False, debug: bool = False) -> datetime.datetime | int | None:
629def get_sync_time(
630    self,
631    pipe: 'mrsm.Pipe',
632    params: Optional[Dict[str, Any]] = None,
633    newest: bool = True,
634    remote: bool = False,
635    debug: bool = False,
636) -> Union[datetime, int, None]:
637    """Get a Pipe's most recent datetime value.
638
639    Parameters
640    ----------
641    pipe: mrsm.Pipe
642        The pipe to get the sync time for.
643
644    params: Optional[Dict[str, Any]], default None
645        Optional params dictionary to build the `WHERE` clause.
646        See `meerschaum.utils.sql.build_where`.
647
648    newest: bool, default True
649        If `True`, get the most recent datetime (honoring `params`).
650        If `False`, get the oldest datetime (ASC instead of DESC).
651
652    remote: bool, default False
653        If `True`, return the sync time for the remote fetch definition.
654
655    Returns
656    -------
657    A `datetime` object (or `int` if using an integer axis) if the pipe exists, otherwise `None`.
658    """
659    return self._rust_method('get_sync_time')(pipe, params=params, newest=newest, remote=remote)

Get a Pipe's most recent datetime value.

Parameters
  • pipe (mrsm.Pipe): The pipe to get the sync time for.
  • params (Optional[Dict[str, Any]], default None): Optional params dictionary to build the WHERE clause. See meerschaum.utils.sql.build_where.
  • newest (bool, default True): If True, get the most recent datetime (honoring params). If False, get the oldest datetime (ASC instead of DESC).
  • remote (bool, default False): If True, return the sync time for the remote fetch definition.
Returns
  • A datetime object (or int if using an integer axis) if the pipe exists, otherwise None.
def pipe_exists(self, pipe: meerschaum.Pipe, debug: bool = False) -> bool:
662def pipe_exists(
663    self,
664    pipe: mrsm.Pipe,
665    debug: bool = False
666) -> bool:
667    """
668    Check that a Pipe's table exists.
669
670    Parameters
671    ----------
672    pipe: mrsm.Pipe:
673        The pipe to check.
674
675    debug: bool, default False
676        Verbosity toggle.
677
678    Returns
679    -------
680    A `bool` corresponding to whether a pipe's table exists.
681
682    """
683    return self._rust_method('pipe_exists')(pipe)

Check that a Pipe's table exists.

Parameters
  • pipe (mrsm.Pipe:): The pipe to check.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A bool corresponding to whether a pipe's table exists.
def get_pipe_rowcount( self, pipe: meerschaum.Pipe, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, remote: bool = False, debug: bool = False) -> int | None:
686def get_pipe_rowcount(
687    self,
688    pipe: mrsm.Pipe,
689    begin: Union[datetime, int, None] = None,
690    end: Union[datetime, int, None] = None,
691    params: Optional[Dict[str, Any]] = None,
692    remote: bool = False,
693    debug: bool = False
694) -> Union[int, None]:
695    """
696    Get the rowcount for a pipe in accordance with given parameters.
697
698    Parameters
699    ----------
700    pipe: mrsm.Pipe
701        The pipe to query with.
702
703    begin: Union[datetime, int, None], default None
704        The begin datetime value.
705
706    end: Union[datetime, int, None], default None
707        The end datetime value.
708
709    params: Optional[Dict[str, Any]], default None
710        See `meerschaum.utils.sql.build_where`.
711
712    remote: bool, default False
713        If `True`, get the rowcount for the remote table.
714
715    debug: bool, default False
716        Verbosity toggle.
717
718    Returns
719    -------
720    An `int` for the number of rows if the `pipe` exists, otherwise `None`.
721
722    """
723    return self._rust_method('get_pipe_rowcount')(pipe, begin=begin, end=end, params=params, remote=remote)

Get the rowcount for a pipe in accordance with given parameters.

Parameters
  • pipe (mrsm.Pipe): The pipe to query with.
  • begin (Union[datetime, int, None], default None): The begin datetime value.
  • end (Union[datetime, int, None], default None): The end datetime value.
  • params (Optional[Dict[str, Any]], default None): See meerschaum.utils.sql.build_where.
  • remote (bool, default False): If True, get the rowcount for the remote table.
  • debug (bool, default False): Verbosity toggle.
Returns
  • An int for the number of rows if the pipe exists, otherwise None.
def drop_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kw) -> Tuple[bool, str]:
726def drop_pipe(
727    self,
728    pipe: mrsm.Pipe,
729    debug: bool = False,
730    **kw
731) -> SuccessTuple:
732    """
733    Drop a pipe's tables but maintain its registration.
734
735    Parameters
736    ----------
737    pipe: mrsm.Pipe
738        The pipe to drop.
739
740    Returns
741    -------
742    A `SuccessTuple` indicated success.
743    """
744    return self._rust_method('drop_pipe')(pipe)

Drop a pipe's tables but maintain its registration.

Parameters
  • pipe (mrsm.Pipe): The pipe to drop.
Returns
  • A SuccessTuple indicated success.
def clear_pipe( self, pipe: meerschaum.Pipe, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, debug: bool = False, **kw) -> Tuple[bool, str]:
747def clear_pipe(
748    self,
749    pipe: mrsm.Pipe,
750    begin: Union[datetime, int, None] = None,
751    end: Union[datetime, int, None] = None,
752    params: Optional[Dict[str, Any]] = None,
753    debug: bool = False,
754    **kw
755) -> SuccessTuple:
756    """
757    Delete a pipe's data within a bounded or unbounded interval without dropping the table.
758
759    Parameters
760    ----------
761    pipe: mrsm.Pipe
762        The pipe to clear.
763        
764    begin: Union[datetime, int, None], default None
765        Beginning datetime. Inclusive.
766
767    end: Union[datetime, int, None], default None
768         Ending datetime. Exclusive.
769
770    params: Optional[Dict[str, Any]], default None
771         See `meerschaum.utils.sql.build_where`.
772
773    """
774    return self._rust_method('clear_pipe')(pipe, begin=begin, end=end, params=params)

Delete a pipe's data within a bounded or unbounded interval without dropping the table.

Parameters
  • pipe (mrsm.Pipe): The pipe to clear.
  • begin (Union[datetime, int, None], default None): Beginning datetime. Inclusive.
  • end (Union[datetime, int, None], default None): Ending datetime. Exclusive.
  • params (Optional[Dict[str, Any]], default None): See meerschaum.utils.sql.build_where.
def deduplicate_pipe( self, pipe: meerschaum.Pipe, begin: datetime.datetime | int | None = None, end: datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, debug: bool = False, **kwargs: Any) -> Tuple[bool, str]:
861def deduplicate_pipe(
862    self,
863    pipe: mrsm.Pipe,
864    begin: Union[datetime, int, None] = None,
865    end: Union[datetime, int, None] = None,
866    params: Optional[Dict[str, Any]] = None,
867    debug: bool = False,
868    **kwargs: Any
869) -> SuccessTuple:
870    """
871    Delete duplicate values within a pipe's table.
872
873    Parameters
874    ----------
875    pipe: mrsm.Pipe
876        The pipe whose table to deduplicate.
877
878    begin: Union[datetime, int, None], default None
879        If provided, only deduplicate values greater than or equal to this value.
880
881    end: Union[datetime, int, None], default None
882        If provided, only deduplicate values less than this value.
883
884    params: Optional[Dict[str, Any]], default None
885        If provided, further limit deduplication to values which match this query dictionary.
886
887    debug: bool, default False
888        Verbosity toggle.
889
890    Returns
891    -------
892    A `SuccessTuple` indicating success.
893    """
894    return self._rust_method('deduplicate_pipe')(pipe, begin=begin, end=end, params=params)

Delete duplicate values within a pipe's table.

Parameters
  • pipe (mrsm.Pipe): The pipe whose table to deduplicate.
  • begin (Union[datetime, int, None], default None): If provided, only deduplicate values greater than or equal to this value.
  • end (Union[datetime, int, None], default None): If provided, only deduplicate values less than this value.
  • params (Optional[Dict[str, Any]], default None): If provided, further limit deduplication to values which match this query dictionary.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A SuccessTuple indicating success.
def get_pipe_table( self, pipe: meerschaum.Pipe, debug: bool = False) -> "Union['sqlalchemy.Table', None]":
777def get_pipe_table(
778    self,
779    pipe: mrsm.Pipe,
780    debug: bool = False,
781) -> Union['sqlalchemy.Table', None]:
782    """
783    Return the `sqlalchemy.Table` object for a `mrsm.Pipe`.
784
785    Parameters
786    ----------
787    pipe: mrsm.Pipe:
788        The pipe in question.
789
790    Returns
791    -------
792    A `sqlalchemy.Table` object. 
793
794    """
795    from meerschaum.utils.sql import get_sqlalchemy_table
796    if not pipe.exists(debug=debug):
797        return None
798
799    return get_sqlalchemy_table(
800        pipe.target,
801        connector=self,
802        schema=self.get_pipe_schema(pipe),
803        debug=debug,
804        refresh=True,
805    )

Return the sqlalchemy.Table object for a mrsm.Pipe.

Parameters
  • pipe (mrsm.Pipe:): The pipe in question.
Returns
  • A sqlalchemy.Table object.
def get_pipe_columns_types( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, str]:
808def get_pipe_columns_types(
809    self,
810    pipe: mrsm.Pipe,
811    debug: bool = False,
812) -> Dict[str, str]:
813    """
814    Get the pipe's columns and types.
815
816    Parameters
817    ----------
818    pipe: mrsm.Pipe:
819        The pipe to get the columns for.
820
821    Returns
822    -------
823    A dictionary of columns names (`str`) and types (`str`).
824
825    Examples
826    --------
827    >>> conn.get_pipe_columns_types(pipe)
828    {
829      'dt': 'TIMESTAMP WITHOUT TIMEZONE',
830      'id': 'BIGINT',
831      'val': 'DOUBLE PRECISION',
832    }
833    >>> 
834    """
835    return self._rust_method('get_pipe_columns_types')(pipe)

Get the pipe's columns and types.

Parameters
  • pipe (mrsm.Pipe:): The pipe to get the columns for.
Returns
  • A dictionary of columns names (str) and types (str).
Examples
>>> conn.get_pipe_columns_types(pipe)
{
  'dt': 'TIMESTAMP WITHOUT TIMEZONE',
  'id': 'BIGINT',
  'val': 'DOUBLE PRECISION',
}
>>>
def get_pipe_schema(self, pipe: meerschaum.Pipe) -> str | None:
897def get_pipe_schema(self, pipe: mrsm.Pipe) -> Union[str, None]:
898    """
899    Return the schema to use for this pipe.
900    First check `pipe.parameters['schema']`, then check `self.schema`.
901
902    Parameters
903    ----------
904    pipe: mrsm.Pipe
905        The pipe which may contain a configured schema.
906
907    Returns
908    -------
909    A schema string or `None` if nothing is configured.
910    """
911    if self.flavor in ('sqlite', 'geopackage'):
912        return self.schema
913    return pipe.parameters.get('schema', self.schema)

Return the schema to use for this pipe. First check pipe.parameters['schema'], then check self.schema.

Parameters
  • pipe (mrsm.Pipe): The pipe which may contain a configured schema.
Returns
  • A schema string or None if nothing is configured.
def get_pipe_columns_indices( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, List[Dict[str, str]]]:
838def get_pipe_columns_indices(
839    self,
840    pipe: mrsm.Pipe,
841    debug: bool = False,
842) -> Dict[str, List[Dict[str, str]]]:
843    """
844    Return a dictionary mapping columns to the indices created on those columns.
845
846    Parameters
847    ----------
848    pipe: mrsm.Pipe
849        The pipe to be queried against.
850
851    Returns
852    -------
853    A dictionary mapping columns names to lists of dictionaries.
854    The dictionaries in the lists contain the name and type of the indices.
855    """
856    if pipe.__dict__.get('_skip_check_indices', False):
857        return {}
858    return self._rust_method('get_pipe_columns_indices')(pipe)

Return a dictionary mapping columns to the indices created on those columns.

Parameters
  • pipe (mrsm.Pipe): The pipe to be queried against.
Returns
  • A dictionary mapping columns names to lists of dictionaries.
  • The dictionaries in the lists contain the name and type of the indices.
@staticmethod
def get_temporary_target( target: str, transact_id: str | None = None, label: str | None = None, separator: str | None = None) -> str:
916@staticmethod
917def get_temporary_target(
918    target: str,
919    transact_id: Optional[str] = None,
920    label: Optional[str] = None,
921    separator: Optional[str] = None,
922) -> str:
923    """
924    Return a unique(ish) temporary target for a pipe.
925    """
926    from meerschaum.utils.misc import generate_password
927    temp_target_cf = (
928        mrsm.get_config('system', 'connectors', 'sql', 'instance', 'temporary_target') or {}
929    )
930    transaction_id_len = temp_target_cf.get('transaction_id_length', 3)
931    transact_id = transact_id or generate_password(transaction_id_len)
932    temp_prefix = temp_target_cf.get('prefix', '_')
933    separator = separator or temp_target_cf.get('separator', '_')
934    return (
935        temp_prefix
936        + target
937        + separator
938        + transact_id
939        + ((separator + label) if label else '')
940    )

Return a unique(ish) temporary target for a pipe.

def create_pipe_indices( self, pipe: meerschaum.Pipe, columns: List[str] | None = None, debug: bool = False) -> Tuple[bool, str]:
106def create_pipe_indices(
107    self,
108    pipe: mrsm.Pipe,
109    columns: Optional[List[str]] = None,
110    debug: bool = False,
111) -> SuccessTuple:
112    """
113    Create a pipe's indices.
114    """
115    return self._rust_method('create_pipe_indices')(pipe, columns=columns)

Create a pipe's indices.

def drop_pipe_indices( self, pipe: meerschaum.Pipe, columns: List[str] | None = None, debug: bool = False) -> Tuple[bool, str]:
133def drop_pipe_indices(
134    self,
135    pipe: mrsm.Pipe,
136    columns: Optional[List[str]] = None,
137    debug: bool = False,
138) -> SuccessTuple:
139    """
140    Drop a pipe's indices.
141    """
142    return self._rust_method('drop_pipe_indices')(pipe, columns=columns)

Drop a pipe's indices.

def get_pipe_index_names(self, pipe: meerschaum.Pipe) -> Dict[str, str]:
158def get_pipe_index_names(self, pipe: mrsm.Pipe) -> Dict[str, str]:
159    """
160    Return a dictionary mapping index keys to their names on the database.
161
162    Returns
163    -------
164    A dictionary of index keys to column names.
165    """
166    return self._rust_method('get_pipe_index_names')(pipe)

Return a dictionary mapping index keys to their names on the database.

Returns
  • A dictionary of index keys to column names.
def get_plugins_pipe(self) -> meerschaum.Pipe:
17def get_plugins_pipe(self) -> mrsm.Pipe:
18    """
19    Return the internal metadata plugins pipe.
20    """
21    users_pipe = self.get_users_pipe()
22    user_id_dtype = users_pipe.dtypes.get('user_id', 'int')
23    return mrsm.Pipe(
24        'mrsm', 'plugins',
25        instance=self,
26        temporary=True,
27        static=True,
28        null_indices=False,
29        columns={
30            'primary': 'plugin_id',
31            'user_id': 'user_id',    
32        },
33        dtypes={
34            'plugin_name': 'string',
35            'user_id': user_id_dtype,
36            'attributes': 'json',
37            'version': 'string',
38        },
39        indices={
40            'unique': 'plugin_name',
41        },
42    )

Return the internal metadata plugins pipe.

def register_plugin( self, plugin: meerschaum.Plugin, force: bool = False, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
45def register_plugin(
46    self,
47    plugin: 'mrsm.core.Plugin',
48    force: bool = False,
49    debug: bool = False,
50    **kw: Any
51) -> SuccessTuple:
52    """Register a new plugin to the plugins table."""
53    return self._rust_method('register_plugin')(plugin, force=force)

Register a new plugin to the plugins table.

def delete_plugin( self, plugin: meerschaum.Plugin, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
133def delete_plugin(
134    self,
135    plugin: 'mrsm.core.Plugin',
136    debug: bool = False,
137    **kw: Any
138) -> SuccessTuple:
139    """Delete a plugin from the plugins table."""
140    return self._rust_method('delete_plugin')(plugin)

Delete a plugin from the plugins table.

def get_plugin_id( self, plugin: meerschaum.Plugin, debug: bool = False) -> int | None:
55def get_plugin_id(
56    self,
57    plugin: 'mrsm.core.Plugin',
58    debug: bool = False
59) -> Optional[int]:
60    """
61    Return a plugin's ID.
62    """
63    return self._rust_method('get_plugin_id')(plugin)

Return a plugin's ID.

def get_plugin_version( self, plugin: meerschaum.Plugin, debug: bool = False) -> str | None:
66def get_plugin_version(
67    self,
68    plugin: 'mrsm.core.Plugin',
69    debug: bool = False
70) -> Optional[str]:
71    """
72    Return a plugin's version.
73    """
74    return self._rust_method('get_plugin_version')(plugin)

Return a plugin's version.

def get_plugins( self, user_id: int | None = None, search_term: str | None = None, debug: bool = False, **kw: Any) -> List[str]:
107def get_plugins(
108    self,
109    user_id: Optional[int] = None,
110    search_term: Optional[str] = None,
111    debug: bool = False,
112    **kw: Any
113) -> List[str]:
114    """
115    Return a list of all registered plugins.
116
117    Parameters
118    ----------
119    user_id: Optional[int], default None
120        If specified, filter plugins by a specific `user_id`.
121
122    search_term: Optional[str], default None
123        If specified, add a `WHERE plugin_name LIKE '{search_term}%'` clause to filter the plugins.
124
125
126    Returns
127    -------
128    A list of plugin names.
129    """
130    return self._rust_method('get_plugins')(user_id=user_id, search_term=search_term)

Return a list of all registered plugins.

Parameters
  • user_id (Optional[int], default None): If specified, filter plugins by a specific user_id.
  • search_term (Optional[str], default None): If specified, add a WHERE plugin_name LIKE '{search_term}%' clause to filter the plugins.
Returns
  • A list of plugin names.
def get_plugin_user_id( self, plugin: meerschaum.Plugin, debug: bool = False) -> int | None:
76def get_plugin_user_id(
77    self,
78    plugin: 'mrsm.core.Plugin',
79    debug: bool = False
80) -> Optional[int]:
81    """
82    Return a plugin's user ID.
83    """
84    return self._rust_method('get_plugin_user_id')(plugin)

Return a plugin's user ID.

def get_plugin_username( self, plugin: meerschaum.Plugin, debug: bool = False) -> str | None:
86def get_plugin_username(
87    self,
88    plugin: 'mrsm.core.Plugin',
89    debug: bool = False
90) -> Optional[str]:
91    """
92    Return the username of a plugin's owner.
93    """
94    return self._rust_method('get_plugin_username')(plugin)

Return the username of a plugin's owner.

def get_plugin_attributes( self, plugin: meerschaum.Plugin, debug: bool = False) -> Dict[str, Any]:
 97def get_plugin_attributes(
 98    self,
 99    plugin: 'mrsm.core.Plugin',
100    debug: bool = False
101) -> Dict[str, Any]:
102    """
103    Return the attributes of a plugin.
104    """
105    return self._rust_method('get_plugin_attributes')(plugin)

Return the attributes of a plugin.

def get_users_pipe(self) -> meerschaum.Pipe:
16def get_users_pipe(self) -> mrsm.Pipe:
17    """
18    Return the internal metadata pipe for users management.
19    """
20    if '_users_pipe' in self.__dict__:
21        return self._users_pipe
22
23    cache_connector = self.__dict__.get('_cache_connector', None)
24    self._users_pipe = mrsm.Pipe(
25        'mrsm', 'users',
26        ### FIX (intentional v4 change, rc15 FX1): v4 left out `instance=self`, so the users pipe
27        ### (and `get_username()`, which reads it) was on the default instance.
28        instance=self,
29        temporary=True,
30        cache=True,
31        cache_connector_keys=cache_connector,
32        static=True,
33        null_indices=False,
34        enforce=False,
35        autoincrement=True,
36        columns={
37            'primary': 'user_id',
38        },
39        dtypes={
40            'user_id': 'int',
41            'username': 'string',
42            'attributes': 'json',
43            'user_type': 'string',
44        },
45        indices={
46            'unique': 'username',
47        },
48    )
49    return self._users_pipe

Return the internal metadata pipe for users management.

def register_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
52def register_user(
53    self,
54    user: mrsm.core.User,
55    debug: bool = False,
56    **kw: Any
57) -> SuccessTuple:
58    """Register a new user."""
59    return self._rust_method('register_user')(user)

Register a new user.

def get_user_id( self, user: meerschaum.core.User._User.User, debug: bool = False) -> int | None:
106def get_user_id(
107    self,
108    user: 'mrsm.core.User',
109    debug: bool = False
110) -> Optional[int]:
111    """If a user is registered, return the `user_id`."""
112    return self._rust_method('get_user_id')(user)

If a user is registered, return the user_id.

def get_users(self, debug: bool = False, **kw: Any) -> List[str]:
133def get_users(
134    self,
135    debug: bool = False,
136    **kw: Any
137) -> List[str]:
138    """
139    Get the registered usernames.
140    """
141    return self._rust_method('get_users')()

Get the registered usernames.

def edit_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
 96def edit_user(
 97    self,
 98    user: 'mrsm.core.User',
 99    debug: bool = False,
100    **kw: Any
101) -> SuccessTuple:
102    """Update an existing user's metadata."""
103    return self._rust_method('edit_user')(user)

Update an existing user's metadata.

def delete_user( self, user: meerschaum.core.User._User.User, debug: bool = False) -> Tuple[bool, str]:
124def delete_user(
125    self,
126    user: 'mrsm.core.User',
127    debug: bool = False
128) -> SuccessTuple:
129    """Delete a user's record from the users table."""
130    return self._rust_method('delete_user')(user)

Delete a user's record from the users table.

def get_user_password_hash( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> str | None:
144def get_user_password_hash(
145    self,
146    user: 'mrsm.core.User',
147    debug: bool = False,
148    **kw: Any
149) -> Optional[str]:
150    """
151    Return the password has for a user.
152    **NOTE**: This may be dangerous and is only allowed if the security settings explicity allow it.
153    """
154    return self._rust_method('get_user_password_hash')(user)

Return the password has for a user. NOTE: This may be dangerous and is only allowed if the security settings explicity allow it.

def get_user_type( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> str | None:
157def get_user_type(
158    self,
159    user: 'mrsm.core.User',
160    debug: bool = False,
161    **kw: Any
162) -> Optional[str]:
163    """
164    Return the user's type.
165    """
166    return self._rust_method('get_user_type')(user)

Return the user's type.

def get_user_attributes( self, user: meerschaum.core.User._User.User, debug: bool = False) -> Dict[str, Any] | None:
114def get_user_attributes(
115    self,
116    user: 'mrsm.core.User',
117    debug: bool = False
118) -> Union[Dict[str, Any], None]:
119    """
120    Return the user's attributes.
121    """
122    return self._rust_method('get_user_attributes')(user)

Return the user's attributes.

@classmethod
def from_uri( cls, uri: str, label: str | None = None, as_dict: bool = False) -> SQLConnector | Dict[str, str | int]:
17@classmethod
18def from_uri(
19    cls,
20    uri: str,
21    label: Optional[str] = None,
22    as_dict: bool = False,
23) -> Union[
24    'meerschaum.connectors.SQLConnector',
25    Dict[str, Union[str, int]],
26]:
27    """
28    Create a new SQLConnector from a URI string.
29
30    Parameters
31    ----------
32    uri: str
33        The URI connection string.
34
35    label: Optional[str], default None
36        If provided, use this as the connector label.
37        Otherwise use the determined database name.
38
39    as_dict: bool, default False
40        If `True`, return a dictionary of the keyword arguments
41        necessary to create a new `SQLConnector`, otherwise create a new object.
42
43    Returns
44    -------
45    A new SQLConnector object or a dictionary of attributes (if `as_dict` is `True`).
46    """
47
48    params = cls.parse_uri(uri)
49    params['uri'] = uri
50    flavor = params.get('flavor', None)
51    if not flavor or flavor not in cls.flavor_configs:
52        error(f"Invalid flavor '{flavor}' detected from the provided URI.")
53
54    if 'database' not in params:
55        error("Unable to determine the database from the provided URI.")
56
57    if flavor in ('sqlite', 'duckdb', 'geopackage'):
58        if params['database'] == ':memory:':
59            params['label'] = label or f'memory_{flavor}'
60        else:
61            params['label'] = label or params['database'].split(os.path.sep)[-1].lower()
62    else:
63        params['label'] = label or (
64            (
65                (params['username'] + '@' if 'username' in params else '')
66                + params.get('host', '')
67                + ('/' if 'host' in params else '')
68                + params.get('database', '')
69            ).lower()
70        )
71
72    return cls(**params) if not as_dict else params

Create a new SQLConnector from a URI string.

Parameters
  • uri (str): The URI connection string.
  • label (Optional[str], default None): If provided, use this as the connector label. Otherwise use the determined database name.
  • as_dict (bool, default False): If True, return a dictionary of the keyword arguments necessary to create a new SQLConnector, otherwise create a new object.
Returns
  • A new SQLConnector object or a dictionary of attributes (if as_dict is True).
@staticmethod
def parse_uri(uri: str) -> Dict[str, Any]:
 77@staticmethod
 78def parse_uri(uri: str) -> Dict[str, Any]:
 79    """
 80    Parse a URI string into a dictionary of parameters.
 81
 82    Parameters
 83    ----------
 84    uri: str
 85        The database connection URI.
 86
 87    Returns
 88    -------
 89    A dictionary of attributes.
 90
 91    Examples
 92    --------
 93    >>> parse_uri('sqlite:////home/foo/bar.db')
 94    {'database': '/home/foo/bar.db', 'flavor': 'sqlite'}
 95    >>> parse_uri(
 96    ...     'mssql+pyodbc://sa:supersecureSECRETPASSWORD123!@localhost:1439'
 97    ...     + '/master?driver=ODBC+Driver+17+for+SQL+Server'
 98    ... )
 99    {'host': 'localhost', 'database': 'master', 'username': 'sa',
100    'password': 'supersecureSECRETPASSWORD123!', 'port': 1439, 'flavor': 'mssql',
101    'driver': 'ODBC Driver 17 for SQL Server'}
102    >>> 
103    """
104    ### The Rust grammar is SQLAlchemy 2.0's (golden-tested) and its message is SQLAlchemy's:
105    ### a malformed URI raises `ValueError` rather than `sqlalchemy.exc.ArgumentError`
106    ### (see PARITY.md, Wave 13 H6).
107    from meerschaum import _core
108    return _core.parse_sql_uri(uri)

Parse a URI string into a dictionary of parameters.

Parameters
  • uri (str): The database connection URI.
Returns
  • A dictionary of attributes.
Examples
>>> parse_uri('sqlite:////home/foo/bar.db')
{'database': '/home/foo/bar.db', 'flavor': 'sqlite'}
>>> parse_uri(
...     'mssql+pyodbc://sa:supersecureSECRETPASSWORD123!@localhost:1439'
...     + '/master?driver=ODBC+Driver+17+for+SQL+Server'
... )
{'host': 'localhost', 'database': 'master', 'username': 'sa',
'password': 'supersecureSECRETPASSWORD123!', 'port': 1439, 'flavor': 'mssql',
'driver': 'ODBC Driver 17 for SQL Server'}
>>>
class APIConnector(meerschaum.connectors.InstanceConnector):
 23class APIConnector(InstanceConnector):
 24    """
 25    Connect to a Meerschaum API instance.
 26
 27    The requests are made by the Rust client (`meerschaum._core.ApiConnector`, one per connector
 28    and process): the same routes, query parameters and bodies as v4. Nothing here imports
 29    `requests` or `httpx`.
 30    """
 31
 32    IS_THREAD_SAFE: bool = False
 33    OPTIONAL_ATTRIBUTES: List[str] = ['port', 'client_secret', 'client_id', 'api_key']
 34
 35    from ._request import (
 36        make_request,
 37        get,
 38        post,
 39        put,
 40        patch,
 41        delete,
 42        wget,
 43    )
 44    from ._actions import (
 45        get_actions,
 46        do_action,
 47        do_action_async,
 48        do_action_legacy,
 49    )
 50    from ._misc import get_mrsm_version, get_chaining_status
 51    from ._metrics import get_engine_metrics, initialize_engine_metrics
 52    from ._pipes import (
 53        get_pipe_instance_keys,
 54        register_pipe,
 55        fetch_pipes_keys,
 56        edit_pipe,
 57        sync_pipe,
 58        delete_pipe,
 59        delete_pipe_cache,
 60        get_pipe_data,
 61        get_pipe_id,
 62        get_pipe_attributes,
 63        get_sync_time,
 64        pipe_exists,
 65        create_metadata,
 66        get_pipe_rowcount,
 67        drop_pipe,
 68        clear_pipe,
 69        get_pipe_columns_types,
 70        get_pipe_columns_indices,
 71        get_pipe_index_names,
 72        get_pipe_docs,
 73        get_pipe_size,
 74        compress_pipe,
 75        decompress_pipe,
 76        vacuum_pipe,
 77        analyze_pipe,
 78        partition_pipe,
 79    )
 80    from ._fetch import fetch
 81    from ._plugins import (
 82        register_plugin,
 83        install_plugin,
 84        delete_plugin,
 85        get_plugins,
 86        get_plugin_attributes,
 87        get_plugin_version,
 88    )
 89    from ._login import login, test_connection
 90    from ._users import (
 91        register_user,
 92        get_user_id,
 93        get_users,
 94        edit_user,
 95        delete_user,
 96        get_user_password_hash,
 97        get_user_type,
 98        get_user_attributes,
 99    )
100    from ._tokens import (
101        register_token,
102        get_token_model,
103        get_tokens,
104        edit_token,
105        invalidate_token,
106        get_token_scopes,
107        token_exists,
108        delete_token,
109    )
110    from ._uri import from_uri
111    from ._jobs import (
112        get_jobs,
113        get_job,
114        get_job_metadata,
115        get_job_properties,
116        get_job_exists,
117        delete_job,
118        start_job,
119        create_job,
120        stop_job,
121        pause_job,
122        get_logs,
123        get_job_stop_time,
124        monitor_logs,
125        monitor_logs_async,
126        get_job_is_blocking_on_stdin,
127        get_job_prompt_kwargs,
128        get_job_began,
129        get_job_ended,
130        get_job_paused,
131        get_job_status,
132    )
133    ### The access model (rc15): teams, permissions, access and locks are the server's. The
134    ### `InstanceConnector` methods reach its routes through the Rust client; these answer in
135    ### one request.
136    from ._access import (
137        get_team_pipes_count,
138        get_team_jobs_count,
139        get_users_permissions,
140        get_user_permissions_record,
141        get_pipe_access,
142    )
143
144    def __init__(
145        self,
146        label: Optional[str] = None,
147        wait: bool = False,
148        debug: bool = False,
149        **kw
150    ):
151        if 'uri' in kw:
152            from_uri_params = self.from_uri(kw['uri'], as_dict=True)
153            label = label or from_uri_params.get('label', None)
154            _ = from_uri_params.pop('label', None)
155            kw.update(from_uri_params)
156
157        super().__init__('api', label=label, **kw)
158        if 'protocol' not in self.__dict__:
159            self.protocol = (
160                'https' if self.__dict__.get('uri', '').startswith('https')
161                else 'http'
162            )
163
164        if 'uri' not in self.__dict__:
165            self.verify_attributes(required_attributes)
166        else:
167            conn_attrs = self.from_uri(self.__dict__['uri'], as_dict=True)
168            self.__dict__.update({
169                key: val
170                for key, val in conn_attrs.items()
171                if key not in ('label', 'protocol')
172            })
173
174        self.url = (
175            self.protocol + '://' +
176            self.host
177            + (
178                (':' + str(self.port))
179                if self.__dict__.get('port', None)
180                else ''
181            )
182        )
183        self._rust_client_obj = None
184        self._instance_keys = self.__dict__.get('instance_keys', None)
185
186    def _rust_client(self):
187        """
188        Return the `meerschaum._core.ApiConnector` of this connector (built once per process:
189        a forked child builds its own, which holds its own HTTP connections and token).
190        """
191        import os
192        attributes = {
193            key: value
194            for key, value in self.__dict__.items()
195            if not str(key).startswith('_')
196            and isinstance(value, (str, int, float, bool))
197        }
198        client = self.__dict__.get('_rust_client_obj', None)
199        if (
200            client is not None
201            and self.__dict__.get('_rust_client_pid', None) == os.getpid()
202            and self.__dict__.get('_rust_client_attributes', None) == attributes
203        ):
204            return client
205
206        ### A new process, or credentials / address changed (`conn.password = ...`): build a client
207        ### with the current attributes (it logs in again; the native flows find it by them).
208        from meerschaum import _core
209        client = _core.ApiConnector(self.label, attributes)
210        self.__dict__['_rust_client_obj'] = client
211        self.__dict__['_rust_client_pid'] = os.getpid()
212        self.__dict__['_rust_client_attributes'] = attributes
213        return client
214
215    def __getstate__(self):
216        state = self.__dict__.copy()
217        for key in ('_rust_client_obj', '_rust_client_pid', '_rust_client_attributes', '_session'):
218            state.pop(key, None)
219        state['_rust_client_obj'] = None
220        return state
221
222    def __setstate__(self, state):
223        self.__dict__.update(state)
224
225    @property
226    def URI(self) -> str:
227        """
228        Return the fully qualified URI.
229        """
230        return self._rust_client().uri
231
232    @property
233    def session(self):
234        """
235        A `requests.Session` (not used by the connector, which makes its requests in Rust).
236        It exists for plugins which still reach for it and needs `requests` installed.
237        """
238        if self.__dict__.get('_session', None) is None:
239            from meerschaum.utils.packages import attempt_import
240            requests = attempt_import('requests', lazy=False)
241            if requests is None:
242                raise ImportError("Failed to import requests. Is requests installed?")
243            self._session = requests.Session()
244        return self._session
245
246    @property
247    def token(self):
248        """
249        Return the API key, or the session token (logging in when it is missing or expiring).
250        """
251        return self._rust_client().token
252
253    @property
254    def _token(self) -> Union[str, None]:
255        """
256        The stored session token (v4's attribute): `None` before a login, and it never logs in.
257        Assign `None` to force a new login.
258        """
259        return self._rust_client().stored_token
260
261    @_token.setter
262    def _token(self, value: Union[str, None]) -> None:
263        self._rust_client().stored_token = value
264
265    @property
266    def _expires(self):
267        """
268        When the stored session token expires (v4's attribute, a naive `datetime`), or `None`.
269        """
270        import datetime
271        value = self._rust_client().stored_expires
272        return None if value is None else datetime.datetime.fromisoformat(value)
273
274    @_expires.setter
275    def _expires(self, value) -> None:
276        import datetime
277        if value is not None:
278            if isinstance(value, str):
279                value = datetime.datetime.fromisoformat(value)
280            if not isinstance(value, datetime.datetime):
281                raise TypeError(
282                    f"`_expires` must be a datetime, an ISO string or None, not {type(value).__name__}."
283                )
284            ### Stored as naive UTC, like v4's: an aware value is converted, not just stripped.
285            if value.tzinfo is not None:
286                value = value.astimezone(datetime.timezone.utc).replace(tzinfo=None)
287            value = value.isoformat()
288        self._rust_client().stored_expires = value
289
290    @property
291    def instance_keys(self) -> Union[str, None]:
292        """
293        Return the instance keys to be sent alongside pipe requests.
294        """
295        return self._instance_keys
296
297    @property
298    def login_scheme(self) -> str:
299        """
300        Return the login scheme to use based on the configured credentials.
301        """
302        return self._rust_client().login_scheme

Connect to a Meerschaum API instance.

The requests are made by the Rust client (meerschaum._core.ApiConnector, one per connector and process): the same routes, query parameters and bodies as v4. Nothing here imports requests or httpx.

APIConnector( label: str | None = None, wait: bool = False, debug: bool = False, **kw)
144    def __init__(
145        self,
146        label: Optional[str] = None,
147        wait: bool = False,
148        debug: bool = False,
149        **kw
150    ):
151        if 'uri' in kw:
152            from_uri_params = self.from_uri(kw['uri'], as_dict=True)
153            label = label or from_uri_params.get('label', None)
154            _ = from_uri_params.pop('label', None)
155            kw.update(from_uri_params)
156
157        super().__init__('api', label=label, **kw)
158        if 'protocol' not in self.__dict__:
159            self.protocol = (
160                'https' if self.__dict__.get('uri', '').startswith('https')
161                else 'http'
162            )
163
164        if 'uri' not in self.__dict__:
165            self.verify_attributes(required_attributes)
166        else:
167            conn_attrs = self.from_uri(self.__dict__['uri'], as_dict=True)
168            self.__dict__.update({
169                key: val
170                for key, val in conn_attrs.items()
171                if key not in ('label', 'protocol')
172            })
173
174        self.url = (
175            self.protocol + '://' +
176            self.host
177            + (
178                (':' + str(self.port))
179                if self.__dict__.get('port', None)
180                else ''
181            )
182        )
183        self._rust_client_obj = None
184        self._instance_keys = self.__dict__.get('instance_keys', None)

Set the given keyword arguments as attributes.

Parameters
  • type (str): The type of the connector (e.g. sql, api, plugin).
  • label (str): The label for the connector.
Examples

Run mrsm edit config and to edit connectors in the YAML file:

meerschaum:
    connections:
        {type}:
            {label}:
                ### attributes go here
IS_THREAD_SAFE: bool = False
OPTIONAL_ATTRIBUTES: List[str] = ['port', 'client_secret', 'client_id', 'api_key']
url
URI: str
225    @property
226    def URI(self) -> str:
227        """
228        Return the fully qualified URI.
229        """
230        return self._rust_client().uri

Return the fully qualified URI.

session
232    @property
233    def session(self):
234        """
235        A `requests.Session` (not used by the connector, which makes its requests in Rust).
236        It exists for plugins which still reach for it and needs `requests` installed.
237        """
238        if self.__dict__.get('_session', None) is None:
239            from meerschaum.utils.packages import attempt_import
240            requests = attempt_import('requests', lazy=False)
241            if requests is None:
242                raise ImportError("Failed to import requests. Is requests installed?")
243            self._session = requests.Session()
244        return self._session

A requests.Session (not used by the connector, which makes its requests in Rust). It exists for plugins which still reach for it and needs requests installed.

token
246    @property
247    def token(self):
248        """
249        Return the API key, or the session token (logging in when it is missing or expiring).
250        """
251        return self._rust_client().token

Return the API key, or the session token (logging in when it is missing or expiring).

instance_keys: str | None
290    @property
291    def instance_keys(self) -> Union[str, None]:
292        """
293        Return the instance keys to be sent alongside pipe requests.
294        """
295        return self._instance_keys

Return the instance keys to be sent alongside pipe requests.

login_scheme: str
297    @property
298    def login_scheme(self) -> str:
299        """
300        Return the login scheme to use based on the configured credentials.
301        """
302        return self._rust_client().login_scheme

Return the login scheme to use based on the configured credentials.

def make_request( self, method: str, r_url: str, headers: Dict[str, Any] | None = None, use_token: bool = True, debug: bool = False, **kwargs: Any) -> meerschaum._core.ApiResponse:
21def make_request(
22    self,
23    method: str,
24    r_url: str,
25    headers: Optional[Dict[str, Any]] = None,
26    use_token: bool = True,
27    debug: bool = False,
28    **kwargs: Any
29) -> 'meerschaum._core.ApiResponse':
30    """
31    Make a request to the API and return a response object
32    (`status_code`, `ok`, `text`, `content`, `headers`, `json()`, `raise_for_status()`).
33
34    Parameters
35    ----------
36    method: str
37        The name of the request method (e.g. `'GET'`, `'POST'`).
38
39    r_url: str
40        The relative URL for the request (e.g. `'/pipes'`).
41
42    headers: Optional[Dict[str, Any]], default None
43        Headers to override the default headers.
44
45    use_token: bool, default True
46        If `True`, add the authorization token to the headers.
47
48    debug: bool, default False
49        Verbosity toggle.
50
51    kwargs: Any
52        `params` (the query), `json`, `data`, `files` and `timeout` are sent as `requests` would.
53    """
54    if method.upper() not in METHODS:
55        raise ValueError(f"Method '{method}' is not supported.")
56
57    if debug:
58        dprint(f"[{self}] Sending a '{method.upper()}' request to {self.url}{r_url}")
59
60    return self._rust_client().make_request(
61        method,
62        r_url,
63        headers=headers,
64        use_token=use_token,
65        **kwargs
66    )

Make a request to the API and return a response object (status_code, ok, text, content, headers, json(), raise_for_status()).

Parameters
  • method (str): The name of the request method (e.g. 'GET', 'POST').
  • r_url (str): The relative URL for the request (e.g. '/pipes').
  • headers (Optional[Dict[str, Any]], default None): Headers to override the default headers.
  • use_token (bool, default True): If True, add the authorization token to the headers.
  • debug (bool, default False): Verbosity toggle.
  • kwargs (Any): params (the query), json, data, files and timeout are sent as requests would.
def get(self, r_url: str, **kwargs: Any) -> meerschaum._core.ApiResponse:
69def get(self, r_url: str, **kwargs: Any) -> 'meerschaum._core.ApiResponse':
70    """
71    Send a `GET` request (see `make_request()`).
72    """
73    return self.make_request('GET', r_url, **kwargs)

Send a GET request (see make_request()).

def post(self, r_url: str, **kwargs: Any) -> meerschaum._core.ApiResponse:
76def post(self, r_url: str, **kwargs: Any) -> 'meerschaum._core.ApiResponse':
77    """
78    Send a `POST` request (see `make_request()`).
79    """
80    return self.make_request('POST', r_url, **kwargs)

Send a POST request (see make_request()).

def put(self, r_url: str, **kwargs: Any) -> meerschaum._core.ApiResponse:
90def put(self, r_url: str, **kwargs: Any) -> 'meerschaum._core.ApiResponse':
91    """
92    Send a `PUT` request (see `make_request()`).
93    """
94    return self.make_request('PUT', r_url, **kwargs)

Send a PUT request (see make_request()).

def patch(self, r_url: str, **kwargs: Any) -> meerschaum._core.ApiResponse:
83def patch(self, r_url: str, **kwargs: Any) -> 'meerschaum._core.ApiResponse':
84    """
85    Send a `PATCH` request (see `make_request()`).
86    """
87    return self.make_request('PATCH', r_url, **kwargs)

Send a PATCH request (see make_request()).

def delete(self, r_url: str, **kwargs: Any) -> meerschaum._core.ApiResponse:
 97def delete(self, r_url: str, **kwargs: Any) -> 'meerschaum._core.ApiResponse':
 98    """
 99    Send a `DELETE` request (see `make_request()`).
100    """
101    return self.make_request('DELETE', r_url, **kwargs)

Send a DELETE request (see make_request()).

def wget( self, r_url: str, dest: str | pathlib.Path | None = None, headers: Dict[str, Any] | None = None, use_token: bool = True, debug: bool = False, **kw: Any) -> pathlib.Path:
104def wget(
105    self,
106    r_url: str,
107    dest: Optional[Union[str, pathlib.Path]] = None,
108    headers: Optional[Dict[str, Any]] = None,
109    use_token: bool = True,
110    debug: bool = False,
111    **kw: Any
112) -> pathlib.Path:
113    """Mimic wget: download a resource of the API (with the token) to `dest`."""
114    from meerschaum.utils.misc import wget
115    if headers is None:
116        headers = {}
117    if use_token:
118        headers.update({'Authorization': f'Bearer {self.token}'})
119    request_url = urllib.parse.urljoin(self.url, r_url)
120    if debug:
121        dprint(
122            f"[{self}] Downloading {request_url}"
123            + (f' to {dest}' if dest is not None else '')
124            + "..."
125        )
126    return wget(request_url, dest=dest, headers=headers, **kw)

Mimic wget: download a resource of the API (with the token) to dest.

def get_actions(self):
18def get_actions(self):
19    """
20    Get available actions from the API instance.
21    Returns the response (v4 does: `.json()` is the list of action names).
22    """
23    from meerschaum._internal.static import STATIC_CONFIG
24    return self.get(STATIC_CONFIG['api']['endpoints']['actions'])

Get available actions from the API instance. Returns the response (v4 does: .json() is the list of action names).

def do_action(self, sysargs: List[str]) -> Tuple[bool, str]:
27def do_action(self, sysargs: List[str]) -> SuccessTuple:
28    """
29    Execute a Meerschaum action remotely.
30    """
31    return self._rust_client().do_action(sysargs)

Execute a Meerschaum action remotely.

async def do_action_async( self, sysargs: List[str], callback_function: Callable[[str], NoneType] = functools.partial(<built-in function print>, end='')) -> Tuple[bool, str]:
34async def do_action_async(
35    self,
36    sysargs: List[str],
37    callback_function: Callable[[str], None] = partial(print, end=''),
38) -> SuccessTuple:
39    """
40    Execute an action as a temporary remote job.
41    """
42    from meerschaum.connectors.api._jobs import _LoopCallback
43    loop = asyncio.get_running_loop()
44    client = self._rust_client()
45    return await loop.run_in_executor(
46        None,
47        lambda: client.do_action(sysargs, _LoopCallback(loop, callback_function)),
48    )

Execute an action as a temporary remote job.

def do_action_legacy( self, action: List[str] | None = None, sysargs: List[str] | None = None, debug: bool = False, **kw) -> Tuple[bool, str]:
51def do_action_legacy(
52    self,
53    action: Optional[List[str]] = None,
54    sysargs: Optional[List[str]] = None,
55    debug: bool = False,
56    **kw
57) -> SuccessTuple:
58    """
59    NOTE: This method is deprecated.
60    Please use `do_action()` or `do_action_async()`.
61
62    Execute a Meerschaum action remotely.
63
64    If `sysargs` are provided, parse those instead.
65    Otherwise infer everything from keyword arguments.
66
67    Examples
68    --------
69    >>> conn = mrsm.get_connector('api:main')
70    >>> conn.do_action(['show', 'pipes'])
71    (True, "Success")
72    >>> conn.do_action(['show', 'arguments'], name='test')
73    (True, "Success")
74    """
75    if action is None:
76        action = []
77
78    if sysargs is not None and action and action[0] == '':
79        from meerschaum._internal.arguments import parse_arguments
80        json_dict = parse_arguments(sysargs)
81        action = list(json_dict.pop('action', None) or [])
82        kw = json_dict
83    else:
84        action = list(action)
85        if debug:
86            kw['debug'] = debug
87
88    return self._rust_client().do_action_legacy(action, kw)

NOTE: This method is deprecated. Please use do_action() or do_action_async().

Execute a Meerschaum action remotely.

If sysargs are provided, parse those instead. Otherwise infer everything from keyword arguments.

Examples
>>> conn = mrsm.get_connector('api:main')
>>> conn.do_action(['show', 'pipes'])
(True, "Success")
>>> conn.do_action(['show', 'arguments'], name='test')
(True, "Success")
def get_mrsm_version(self, **kw) -> str | None:
15def get_mrsm_version(self, **kw) -> Optional[str]:
16    """
17    Return the Meerschaum version of the API instance.
18    """
19    return self._rust_client().get_mrsm_version()

Return the Meerschaum version of the API instance.

def get_chaining_status(self, **kw) -> bool | None:
22def get_chaining_status(self, **kw) -> Optional[bool]:
23    """
24    Fetch the chaining status of the API instance.
25    """
26    return self._rust_client().get_chaining_status()

Fetch the chaining status of the API instance.

def get_engine_metrics(self, **kwargs):
5def get_engine_metrics(self, **kwargs):
6    """Read bounded sync and Pipeline measurements visible to this credential."""
7    from meerschaum.core.Pipeline._metrics import get_engine_metrics as read
8    return read(instance=self, **kwargs)

Read bounded sync and Pipeline measurements visible to this credential.

def initialize_engine_metrics(self):
11def initialize_engine_metrics(self):
12    """Initialize metrics storage on the served instance (full administrator only)."""
13    from meerschaum.core.Pipeline._metrics import initialize_engine_metrics as initialize
14    return initialize(instance=self)

Initialize metrics storage on the served instance (full administrator only).

def get_pipe_instance_keys(self, pipe: meerschaum.Pipe) -> str | None:
34def get_pipe_instance_keys(self, pipe: mrsm.Pipe) -> Union[str, None]:
35    """
36    Return the configured instance keys for a pipe if set,
37    else fall back to the default `instance_keys` for this `APIConnector`.
38    """
39    return self._rust_client().get_pipe_instance_keys(pipe)

Return the configured instance keys for a pipe if set, else fall back to the default instance_keys for this APIConnector.

def register_pipe( self, pipe: meerschaum.Pipe, debug: bool = False) -> Tuple[bool, str]:
42def register_pipe(
43    self,
44    pipe: mrsm.Pipe,
45    debug: bool = False,
46) -> SuccessTuple:
47    """
48    Submit a POST to the API to register a new Pipe object.
49    Returns a tuple of (success_bool, response_dict).
50    """
51    return self._rust_client().register_pipe(pipe)

Submit a POST to the API to register a new Pipe object. Returns a tuple of (success_bool, response_dict).

def fetch_pipes_keys( self, connector_keys: List[str] | None = None, metric_keys: List[str] | None = None, location_keys: List[str] | None = None, tags: List[str] | None = None, params: Dict[str, Any] | None = None, debug: bool = False) -> Dict[int | str, Tuple[str, str, str | None, Dict[str, Any]]] | List[Tuple[str, str, str | None]]:
 67def fetch_pipes_keys(
 68    self,
 69    connector_keys: Optional[List[str]] = None,
 70    metric_keys: Optional[List[str]] = None,
 71    location_keys: Optional[List[str]] = None,
 72    tags: Optional[List[str]] = None,
 73    params: Optional[Dict[str, Any]] = None,
 74    debug: bool = False
 75) -> Union[
 76    Dict[Union[int, str], Tuple[str, str, Union[str, None], Dict[str, Any]]],
 77    List[Tuple[str, str, Union[str, None]]],
 78]:
 79    """
 80    Fetch registered Pipes' keys from the API.
 81
 82    Parameters
 83    ----------
 84    connector_keys: Optional[List[str]], default None
 85        The connector keys for the query.
 86
 87    metric_keys: Optional[List[str]], default None
 88        The metric keys for the query.
 89
 90    location_keys: Optional[List[str]], default None
 91        The location keys for the query.
 92
 93    tags: Optional[List[str]], default None
 94        A list of tags for the query.
 95
 96    params: Optional[Dict[str, Any]], default None
 97        A parameters dictionary for filtering against the `pipes` table
 98        (e.g. `{'connector_keys': 'plugin:foo'}`).
 99        Not recommeded to be used.
100
101    Returns
102    -------
103    A dictionary mapping pipe IDs to key tuples, or a list of key tuples for older servers.
104    """
105    return self._rust_client().fetch_pipes_keys(
106        connector_keys,
107        metric_keys,
108        location_keys,
109        tags,
110        params,
111    )

Fetch registered Pipes' keys from the API.

Parameters
  • connector_keys (Optional[List[str]], default None): The connector keys for the query.
  • metric_keys (Optional[List[str]], default None): The metric keys for the query.
  • location_keys (Optional[List[str]], default None): The location keys for the query.
  • tags (Optional[List[str]], default None): A list of tags for the query.
  • params (Optional[Dict[str, Any]], default None): A parameters dictionary for filtering against the pipes table (e.g. {'connector_keys': 'plugin:foo'}). Not recommeded to be used.
Returns
  • A dictionary mapping pipe IDs to key tuples, or a list of key tuples for older servers.
def edit_pipe( self, pipe: meerschaum.Pipe, patch: bool = False, debug: bool = False) -> Tuple[bool, str]:
54def edit_pipe(
55    self,
56    pipe: mrsm.Pipe,
57    patch: bool = False,
58    debug: bool = False,
59) -> SuccessTuple:
60    """
61    Submit a PATCH to the API to edit an existing Pipe object.
62    Returns a tuple of (success_bool, response_dict).
63    """
64    return self._rust_client().edit_pipe(pipe, patch=patch)

Submit a PATCH to the API to edit an existing Pipe object. Returns a tuple of (success_bool, response_dict).

def sync_pipe( self, pipe: meerschaum.Pipe, df: "Optional[Union['pd.DataFrame', Dict[Any, Any], str]]" = None, chunksize: int | None = -1, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
114def sync_pipe(
115    self,
116    pipe: mrsm.Pipe,
117    df: Optional[Union['pd.DataFrame', Dict[Any, Any], str]] = None,
118    chunksize: Optional[int] = -1,
119    debug: bool = False,
120    **kw: Any
121) -> SuccessTuple:
122    """
123    Sync a DataFrame (or documents, or a dictionary of lists) into a Pipe.
124    Keyword arguments are sent to the API as query parameters.
125    """
126    if isinstance(df, str):
127        df = json.loads(df)
128    return self._rust_client().sync_pipe(pipe, df, chunksize=chunksize, **kw)

Sync a DataFrame (or documents, or a dictionary of lists) into a Pipe. Keyword arguments are sent to the API as query parameters.

def delete_pipe( self, pipe: meerschaum.Pipe | None = None, debug: bool = False) -> Tuple[bool, str]:
141def delete_pipe(
142    self,
143    pipe: Optional[mrsm.Pipe] = None,
144    debug: bool = False,
145) -> SuccessTuple:
146    """Delete a Pipe and drop its table."""
147    if pipe is None:
148        raise ValueError("Pipe cannot be None.")
149    return self._rust_client().delete_pipe(pipe)

Delete a Pipe and drop its table.

def delete_pipe_cache( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
131def delete_pipe_cache(
132    self,
133    pipe: mrsm.Pipe,
134    debug: bool = False,
135    **kw: Any
136) -> SuccessTuple:
137    """Invalidate the server-side cache for a pipe."""
138    return self._rust_client().delete_pipe_cache(pipe)

Invalidate the server-side cache for a pipe.

def get_pipe_data( self, pipe: meerschaum.Pipe, select_columns: List[str] | None = None, omit_columns: List[str] | None = None, begin: str | datetime.datetime | int | None = None, end: str | datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, as_chunks: bool = False, debug: bool = False, as_polars: bool | None = None, as_pandas: bool | None = None, **kw: Any) -> "Union['pd.DataFrame', None]":
152def get_pipe_data(
153    self,
154    pipe: mrsm.Pipe,
155    select_columns: Optional[List[str]] = None,
156    omit_columns: Optional[List[str]] = None,
157    begin: Union[str, datetime, int, None] = None,
158    end: Union[str, datetime, int, None] = None,
159    params: Optional[Dict[str, Any]] = None,
160    as_chunks: bool = False,
161    debug: bool = False,
162    as_polars: Optional[bool] = None,
163    as_pandas: Optional[bool] = None,
164    **kw: Any
165) -> Union['pd.DataFrame', None]:
166    """
167    Fetch data from the API, as a DataFrame of the `meerschaum:dataframe` library
168    (`as_polars=True` / `as_pandas=True` choose one).
169    """
170    from meerschaum.utils.dataframe import frame_from_arrow, frame_library_of
171    df = self._rust_client().get_pipe_data(
172        pipe,
173        select_columns=select_columns,
174        omit_columns=omit_columns,
175        begin=begin,
176        end=end,
177        params=params,
178        as_chunks=as_chunks,
179    )
180    if frame_library_of(df) is None:
181        return df
182    return frame_from_arrow(df, as_polars=as_polars, as_pandas=as_pandas)

Fetch data from the API, as a DataFrame of the meerschaum:dataframe library (as_polars=True / as_pandas=True choose one).

def get_pipe_id( self, pipe: meerschaum.Pipe, debug: bool = False) -> int | str | None:
185def get_pipe_id(
186    self,
187    pipe: mrsm.Pipe,
188    debug: bool = False,
189) -> Union[int, str, None]:
190    """Get a Pipe's ID from the API."""
191    return self._rust_client().get_pipe_id(pipe)

Get a Pipe's ID from the API.

def get_pipe_attributes( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, Any]:
194def get_pipe_attributes(
195    self,
196    pipe: mrsm.Pipe,
197    debug: bool = False,
198) -> Dict[str, Any]:
199    """
200    Get a Pipe's attributes from the API.
201
202    Parameters
203    ----------
204    pipe: meerschaum.Pipe
205        The pipe whose attributes we are fetching.
206
207    Returns
208    -------
209    A dictionary of a pipe's attributes.
210    If the pipe does not exist, return an empty dictionary.
211    """
212    return self._rust_client().get_pipe_attributes(pipe)

Get a Pipe's attributes from the API.

Parameters
Returns
  • A dictionary of a pipe's attributes.
  • If the pipe does not exist, return an empty dictionary.
def get_sync_time( self, pipe: meerschaum.Pipe, params: Dict[str, Any] | None = None, newest: bool = True, debug: bool = False) -> datetime.datetime | int | None:
215def get_sync_time(
216    self,
217    pipe: mrsm.Pipe,
218    params: Optional[Dict[str, Any]] = None,
219    newest: bool = True,
220    debug: bool = False,
221) -> Union[datetime, int, None]:
222    """
223    Get a Pipe's most recent datetime value from the API.
224
225    Parameters
226    ----------
227    pipe: meerschaum.Pipe
228        The pipe to select from.
229
230    params: Optional[Dict[str, Any]], default None
231        Optional params dictionary to build the WHERE clause.
232
233    newest: bool, default True
234        If `True`, get the most recent datetime (honoring `params`).
235        If `False`, get the oldest datetime (ASC instead of DESC).
236
237    Returns
238    -------
239    The most recent (or oldest if `newest` is `False`) datetime of a pipe,
240    rounded down to the closest minute.
241    """
242    return self._rust_client().get_sync_time(pipe, params=params, newest=newest)

Get a Pipe's most recent datetime value from the API.

Parameters
  • pipe (meerschaum.Pipe): The pipe to select from.
  • params (Optional[Dict[str, Any]], default None): Optional params dictionary to build the WHERE clause.
  • newest (bool, default True): If True, get the most recent datetime (honoring params). If False, get the oldest datetime (ASC instead of DESC).
Returns
  • The most recent (or oldest if newest is False) datetime of a pipe,
  • rounded down to the closest minute.
def pipe_exists(self, pipe: meerschaum.Pipe, debug: bool = False) -> bool:
245def pipe_exists(
246    self,
247    pipe: mrsm.Pipe,
248    debug: bool = False
249) -> bool:
250    """
251    Check the API to see if a Pipe exists.
252
253    Parameters
254    ----------
255    pipe: 'meerschaum.Pipe'
256        The pipe which were are querying.
257
258    Returns
259    -------
260    A bool indicating whether a pipe's underlying table exists.
261    """
262    return self._rust_client().pipe_exists(pipe)

Check the API to see if a Pipe exists.

Parameters
Returns
  • A bool indicating whether a pipe's underlying table exists.
def create_metadata(self, debug: bool = False) -> bool:
265def create_metadata(
266    self,
267    debug: bool = False
268) -> bool:
269    """
270    Create metadata tables.
271
272    Returns
273    -------
274    A bool indicating success.
275    """
276    return self._rust_client().create_metadata()

Create metadata tables.

Returns
  • A bool indicating success.
def get_pipe_rowcount( self, pipe: meerschaum.Pipe, begin: str | datetime.datetime | int | None = None, end: str | datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, remote: bool = False, debug: bool = False) -> int:
279def get_pipe_rowcount(
280    self,
281    pipe: mrsm.Pipe,
282    begin: Union[str, datetime, int, None] = None,
283    end: Union[str, datetime, int, None] = None,
284    params: Optional[Dict[str, Any]] = None,
285    remote: bool = False,
286    debug: bool = False,
287) -> int:
288    """
289    Get a pipe's row count from the API.
290
291    Parameters
292    ----------
293    pipe: 'meerschaum.Pipe':
294        The pipe whose row count we are counting.
295
296    begin: Union[str, datetime, int, None], default None
297        If provided, bound the count by this datetime.
298
299    end: Union[str, datetime, int, None], default None
300        If provided, bound the count by this datetime.
301
302    params: Optional[Dict[str, Any]], default None
303        If provided, bound the count by these parameters.
304
305    remote: bool, default False
306        If `True`, return the rowcount for the fetch definition.
307
308    Returns
309    -------
310    The number of rows in the pipe's table, bound the given parameters.
311    If the table does not exist, return 0.
312    """
313    return self._rust_client().get_pipe_rowcount(
314        pipe,
315        begin=begin,
316        end=end,
317        params=params,
318        remote=remote,
319    )

Get a pipe's row count from the API.

Parameters
  • pipe ('meerschaum.Pipe':): The pipe whose row count we are counting.
  • begin (Union[str, datetime, int, None], default None): If provided, bound the count by this datetime.
  • end (Union[str, datetime, int, None], default None): If provided, bound the count by this datetime.
  • params (Optional[Dict[str, Any]], default None): If provided, bound the count by these parameters.
  • remote (bool, default False): If True, return the rowcount for the fetch definition.
Returns
  • The number of rows in the pipe's table, bound the given parameters.
  • If the table does not exist, return 0.
def drop_pipe( self, pipe: meerschaum.Pipe, debug: bool = False) -> Tuple[bool, str]:
322def drop_pipe(
323    self,
324    pipe: mrsm.Pipe,
325    debug: bool = False
326) -> SuccessTuple:
327    """
328    Drop a pipe's table but maintain its registration.
329
330    Parameters
331    ----------
332    pipe: meerschaum.Pipe:
333        The pipe to be dropped.
334
335    Returns
336    -------
337    A success tuple (bool, str).
338    """
339    if pipe is None:
340        raise ValueError("Pipe cannot be None.")
341    return self._rust_client().drop_pipe(pipe)

Drop a pipe's table but maintain its registration.

Parameters
Returns
  • A success tuple (bool, str).
def clear_pipe( self, pipe: meerschaum.Pipe, begin: str | datetime.datetime | int | None = None, end: str | datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, debug: bool = False, **kw) -> Tuple[bool, str]:
344def clear_pipe(
345    self,
346    pipe: mrsm.Pipe,
347    begin: Union[str, datetime, int, None] = None,
348    end: Union[str, datetime, int, None] = None,
349    params: Optional[Dict[str, Any]] = None,
350    debug: bool = False,
351    **kw
352) -> SuccessTuple:
353    """
354    Delete rows in a pipe's table.
355
356    Parameters
357    ----------
358    pipe: meerschaum.Pipe
359        The pipe with rows to be deleted.
360
361    begin: Union[str, datetime, int, None], default None
362        Delete rows at or after this datetime.
363
364    end: Union[str, datetime, int, None], default None
365        Delete rows before this datetime.
366
367    params: Optional[Dict[str, Any]], default None
368        Delete rows matching these parameters.
369
370    Returns
371    -------
372    A success tuple.
373    """
374    return self._rust_client().clear_pipe(pipe, begin=begin, end=end, params=params)

Delete rows in a pipe's table.

Parameters
  • pipe (meerschaum.Pipe): The pipe with rows to be deleted.
  • begin (Union[str, datetime, int, None], default None): Delete rows at or after this datetime.
  • end (Union[str, datetime, int, None], default None): Delete rows before this datetime.
  • params (Optional[Dict[str, Any]], default None): Delete rows matching these parameters.
Returns
  • A success tuple.
def get_pipe_columns_types( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, str] | None:
377def get_pipe_columns_types(
378    self,
379    pipe: mrsm.Pipe,
380    debug: bool = False,
381) -> Union[Dict[str, str], None]:
382    """
383    Fetch the columns and types of the pipe's table.
384
385    Parameters
386    ----------
387    pipe: meerschaum.Pipe
388        The pipe whose columns to be queried.
389
390    Returns
391    -------
392    A dictionary mapping column names to their database types, or `None` on failure.
393    """
394    return self._rust_client().get_pipe_columns_types(pipe)

Fetch the columns and types of the pipe's table.

Parameters
Returns
  • A dictionary mapping column names to their database types, or None on failure.
def get_pipe_columns_indices( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, List[Dict[str, str]]] | None:
397def get_pipe_columns_indices(
398    self,
399    pipe: mrsm.Pipe,
400    debug: bool = False,
401) -> Union[Dict[str, List[Dict[str, str]]], None]:
402    """
403    Fetch the index information for a pipe.
404
405    Parameters
406    ----------
407    pipe: mrsm.Pipe
408        The pipe whose columns to be queried.
409
410    Returns
411    -------
412    A dictionary mapping column names to lists of dictionaries with index `name` and `type`,
413    or `None` on failure.
414    """
415    return self._rust_client().get_pipe_columns_indices(pipe)

Fetch the index information for a pipe.

Parameters
  • pipe (mrsm.Pipe): The pipe whose columns to be queried.
Returns
  • A dictionary mapping column names to lists of dictionaries with index name and type,
  • or None on failure.
def get_pipe_index_names( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, str]:
509def get_pipe_index_names(
510    self,
511    pipe: mrsm.Pipe,
512    debug: bool = False,
513) -> Dict[str, str]:
514    """Return the index names of a pipe's table."""
515    return self._rust_client().get_pipe_index_names(pipe)

Return the index names of a pipe's table.

def get_pipe_docs( self, pipe: meerschaum.Pipe, select_columns: List[str] | None = None, omit_columns: List[str] | None = None, begin: str | datetime.datetime | int | None = None, end: str | datetime.datetime | int | None = None, params: Dict[str, Any] | None = None, order: str = 'asc', limit: int | None = None, debug: bool = False, **kw: Any) -> List[Dict[str, Any]]:
418def get_pipe_docs(
419    self,
420    pipe: mrsm.Pipe,
421    select_columns: Optional[List[str]] = None,
422    omit_columns: Optional[List[str]] = None,
423    begin: Union[str, datetime, int, None] = None,
424    end: Union[str, datetime, int, None] = None,
425    params: Optional[Dict[str, Any]] = None,
426    order: str = 'asc',
427    limit: Optional[int] = None,
428    debug: bool = False,
429    **kw: Any
430) -> List[Dict[str, Any]]:
431    """Fetch a pipe's data as a list of documents from the API."""
432    return self._rust_client().get_pipe_docs(
433        pipe,
434        select_columns=select_columns,
435        omit_columns=omit_columns,
436        begin=begin,
437        end=end,
438        params=params,
439        order=order,
440        limit=limit,
441    )

Fetch a pipe's data as a list of documents from the API.

def get_pipe_size( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> int | None:
444def get_pipe_size(
445    self,
446    pipe: mrsm.Pipe,
447    debug: bool = False,
448    **kw: Any
449) -> Union[int, None]:
450    """
451    Return the size of the pipe's table in bytes, or `None` if it is not available.
452    """
453    return self._rust_client().get_pipe_size(pipe)

Return the size of the pipe's table in bytes, or None if it is not available.

def compress_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
456def compress_pipe(
457    self,
458    pipe: mrsm.Pipe,
459    debug: bool = False,
460    **kw: Any
461) -> SuccessTuple:
462    """Compress a pipe's table on the API instance."""
463    return self._rust_client().compress_pipe(pipe)

Compress a pipe's table on the API instance.

def decompress_pipe( self, pipe: meerschaum.Pipe, no_policy: bool = False, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
466def decompress_pipe(
467    self,
468    pipe: mrsm.Pipe,
469    no_policy: bool = False,
470    debug: bool = False,
471    **kw: Any
472) -> SuccessTuple:
473    """Decompress a pipe's table on the API instance."""
474    return self._rust_client().decompress_pipe(pipe, no_policy=no_policy)

Decompress a pipe's table on the API instance.

def vacuum_pipe( self, pipe: meerschaum.Pipe, full: bool = False, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
477def vacuum_pipe(
478    self,
479    pipe: mrsm.Pipe,
480    full: bool = False,
481    debug: bool = False,
482    **kw: Any
483) -> SuccessTuple:
484    """Vacuum a pipe's table on the API instance."""
485    return self._rust_client().vacuum_pipe(pipe, full=full)

Vacuum a pipe's table on the API instance.

def analyze_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
499def analyze_pipe(
500    self,
501    pipe: mrsm.Pipe,
502    debug: bool = False,
503    **kw: Any
504) -> SuccessTuple:
505    """Analyze a pipe's table on the API instance."""
506    return self._rust_client().analyze_pipe(pipe)

Analyze a pipe's table on the API instance.

def partition_pipe( self, pipe: meerschaum.Pipe, chunk_minutes: int | None = None, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
488def partition_pipe(
489    self,
490    pipe: mrsm.Pipe,
491    chunk_minutes: Optional[int] = None,
492    debug: bool = False,
493    **kw: Any
494) -> SuccessTuple:
495    """Repartition a pipe's table on the API instance."""
496    return self._rust_client().partition_pipe(pipe, chunk_minutes=chunk_minutes)

Repartition a pipe's table on the API instance.

def fetch( self, pipe: meerschaum.Pipe, begin: datetime.datetime | str | int = '', end: datetime.datetime | int = None, params: Dict[str, Any] | None = None, debug: bool = False, **kw: Any) -> "Iterator['pd.DataFrame']":
19def fetch(
20    self,
21    pipe: mrsm.Pipe,
22    begin: Union[datetime, str, int] = '',
23    end: Union[datetime, int] = None,
24    params: Optional[Dict[str, Any]] = None,
25    debug: bool = False,
26    **kw: Any
27) -> Iterator['pd.DataFrame']:
28    """Get the Pipe data from the remote Pipe (`parameters['fetch']['pipe']`)."""
29    from meerschaum.config._patch import apply_patch_to_config
30    import copy
31
32    fetch_params = pipe.parameters.get('fetch', {})
33    if not fetch_params:
34        warn(f"Missing 'fetch' parameters for {pipe}.", stack=False)
35        return None
36
37    pipe_meta = dict(fetch_params.get('pipe', {}))
38    ### Legacy: check for `connector_keys`, etc. at the root.
39    if not pipe_meta:
40        ck, mk, lk = (
41            fetch_params.get('connector_keys', None),
42            fetch_params.get('metric_key', None),
43            fetch_params.get('location_key', None),
44        )
45        if not ck or not mk:
46            warn(f"Missing `fetch:pipe` keys for {pipe}.", stack=False)
47            return None
48        pipe_meta.update({
49            'connector': ck,
50            'metric': mk,
51            'location': lk,
52        })
53
54    pipe_meta['instance'] = self
55    source_pipe = mrsm.Pipe(**pipe_meta)
56    _params = copy.deepcopy(params) if params is not None else {}
57    _params = apply_patch_to_config(_params, fetch_params.get('params', {}))
58    select_columns = fetch_params.get('select_columns', [])
59    omit_columns = fetch_params.get('omit_columns', [])
60    return source_pipe.get_data(
61        select_columns=select_columns,
62        omit_columns=omit_columns,
63        begin=begin,
64        end=end,
65        params=_params,
66        debug=debug,
67        as_iterator=True,
68    )

Get the Pipe data from the remote Pipe (parameters['fetch']['pipe']).

def register_plugin( self, plugin: meerschaum.Plugin, make_archive: bool = True, debug: bool = False) -> Tuple[bool, str]:
24def register_plugin(
25    self,
26    plugin: mrsm.core.Plugin,
27    make_archive: bool = True,
28    debug: bool = False,
29) -> SuccessTuple:
30    """Register a plugin and upload its archive."""
31    import os
32    archive_path = plugin.make_tar(debug=debug) if make_archive else plugin.archive_path
33    try:
34        with open(archive_path, 'rb') as f:
35            archive = f.read()
36    except Exception:
37        return False, f"Failed to register plugin '{plugin}'."
38    return self._rust_client().register_plugin(
39        plugin,
40        archive,
41        os.path.basename(str(archive_path)),
42    )

Register a plugin and upload its archive.

def install_plugin( self, name: str, skip_deps: bool = False, force: bool = False, debug: bool = False) -> Tuple[bool, str]:
45def install_plugin(
46    self,
47    name: str,
48    skip_deps: bool = False,
49    force: bool = False,
50    debug: bool = False
51) -> SuccessTuple:
52    """Download and attempt to install a plugin from the API."""
53    import os
54    import pathlib
55    from meerschaum.core import Plugin
56    import meerschaum.config.paths as paths
57
58    dest = pathlib.Path(os.path.join(paths.PLUGINS_TEMP_RESOURCES_PATH, name + '.tar.gz'))
59    try:
60        archive = self._rust_client().download_plugin(name)
61    except ValueError as e:
62        return False, str(e)
63    dest.parent.mkdir(parents=True, exist_ok=True)
64    dest.write_bytes(archive)
65    plugin = Plugin(name, archive_path=dest, repo_connector=self)
66    return plugin.install(skip_deps=skip_deps, force=force, debug=debug)

Download and attempt to install a plugin from the API.

def delete_plugin( self, plugin: meerschaum.Plugin, debug: bool = False) -> Tuple[bool, str]:
110def delete_plugin(
111    self,
112    plugin: mrsm.core.Plugin,
113    debug: bool = False
114) -> SuccessTuple:
115    """Delete a plugin from an API repository."""
116    return self._rust_client().delete_plugin(plugin)

Delete a plugin from an API repository.

def get_plugins( self, user_id: int | None = None, search_term: str | None = None, debug: bool = False) -> List[str]:
69def get_plugins(
70    self,
71    user_id: Optional[int] = None,
72    search_term: Optional[str] = None,
73    debug: bool = False
74) -> List[str]:
75    """Return a list of registered plugin names.
76
77    Parameters
78    ----------
79    user_id: Optional[int], default None
80        If specified, return all plugins from a certain user.
81
82    search_term: Optional[str], default None
83        If specified, return plugins beginning with this string.
84
85    Returns
86    -------
87    A list of plugin names.
88    """
89    return self._rust_client().get_plugins(user_id=user_id, search_term=search_term)

Return a list of registered plugin names.

Parameters
  • user_id (Optional[int], default None): If specified, return all plugins from a certain user.
  • search_term (Optional[str], default None): If specified, return plugins beginning with this string.
Returns
  • A list of plugin names.
def get_plugin_attributes( self, plugin: meerschaum.Plugin, debug: bool = False) -> Dict[str, Any]:
92def get_plugin_attributes(
93    self,
94    plugin: mrsm.core.Plugin,
95    debug: bool = False
96) -> Dict[str, Any]:
97    """Return a plugin's attributes."""
98    return self._rust_client().get_plugin_attributes(plugin)

Return a plugin's attributes.

def get_plugin_version( self, plugin: meerschaum.Plugin, debug: bool = False) -> str | None:
101def get_plugin_version(
102    self,
103    plugin: mrsm.core.Plugin,
104    debug: bool = False
105) -> Union[str, None]:
106    """Return a plugin's version."""
107    return self._rust_client().get_plugin_version(plugin)

Return a plugin's version.

def login( self, debug: bool = False, warn: bool = True, **kw: Any) -> Tuple[bool, str]:
15def login(
16    self,
17    debug: bool = False,
18    warn: bool = True,
19    **kw: Any
20) -> SuccessTuple:
21    """Log in and set the session token."""
22    if warn and self.__dict__.get('_warn', None) is False:
23        warn = False
24    return self._rust_client().login(warn=warn)

Log in and set the session token.

def test_connection(self, **kw: Any) -> bool | None:
27def test_connection(
28    self,
29    **kw: Any
30) -> Union[bool, None]:
31    """
32    Test if a successful connection to the API may be made.
33
34    The API instance must answer `/chaining`; keyword arguments are those of `retry_connect()`:
35    `max_retries` (default 1), `retry_wait` (default 0), `warn` (default `False`),
36    `enforce_chaining` and `enforce_login` (default `False`).
37    """
38    import time
39    from meerschaum.utils.warnings import warn as _warn
40    max_retries = kw.get('max_retries', 1)
41    retry_wait = kw.get('retry_wait', 0)
42    warn = kw.get('warn', False)
43    enforce_chaining = kw.get('enforce_chaining', False)
44    enforce_login = kw.get('enforce_login', False)
45
46    try:
47        retries = 0
48        chaining_status = None
49        while retries < max_retries:
50            connected = None
51            if not isinstance(chaining_status, bool):
52                chaining_status = self.get_chaining_status()
53                if chaining_status is False:
54                    if enforce_chaining:
55                        if warn:
56                            _warn(
57                                f"Meerschaum instance '{self}' does not allow chaining "
58                                + "and cannot be used as the parent for this instance.",
59                                stack=False,
60                            )
61                        return False
62                    chaining_status = True
63
64            if chaining_status:
65                connected = self.login(warn=warn)[0] if enforce_login else True
66                if not connected and warn:
67                    _warn(f"Unable to login to '{self}'!", stack=False)
68
69            if connected:
70                return True
71
72            if warn:
73                _warn(f"Connection to '{self}' failed.", stack=False)
74            if retry_wait > 0:
75                time.sleep(retry_wait)
76            retries += 1
77        return False
78    except Exception:
79        return False

Test if a successful connection to the API may be made.

The API instance must answer /chaining; keyword arguments are those of retry_connect(): max_retries (default 1), retry_wait (default 0), warn (default False), enforce_chaining and enforce_login (default False).

def register_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
37def register_user(
38    self,
39    user: mrsm.core.User,
40    debug: bool = False,
41    **kw: Any
42) -> SuccessTuple:
43    """Register a new user."""
44    return self._rust_client().register_user(user)

Register a new user.

def get_user_id( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> int | str | uuid.UUID | None:
47def get_user_id(
48    self,
49    user: mrsm.core.User,
50    debug: bool = False,
51    **kw: Any
52) -> Union[int, str, UUID, None]:
53    """Get a user's ID."""
54    user_id = self._rust_client().get_user_id(user)
55    if isinstance(user_id, str):
56        try:
57            return UUID(user_id) if len(user_id) == 36 else user_id
58        except ValueError:
59            return user_id
60    return user_id

Get a user's ID.

def get_users(self, debug: bool = False, **kw: Any) -> List[str]:
18def get_users(
19    self,
20    debug: bool = False,
21    **kw: Any
22) -> List[str]:
23    """Return a list of registered usernames."""
24    return self._rust_client().get_users()

Return a list of registered usernames.

def edit_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
27def edit_user(
28    self,
29    user: mrsm.core.User,
30    debug: bool = False,
31    **kw: Any
32) -> SuccessTuple:
33    """Edit an existing user."""
34    return self._rust_client().edit_user(user)

Edit an existing user.

def delete_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
63def delete_user(
64    self,
65    user: mrsm.core.User,
66    debug: bool = False,
67    **kw: Any
68) -> SuccessTuple:
69    """Delete a user."""
70    return self._rust_client().delete_user(user)

Delete a user.

def get_user_password_hash( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> str | None:
87def get_user_password_hash(
88    self,
89    user: mrsm.core.User,
90    debug: bool = False,
91    **kw: Any
92) -> Optional[str]:
93    """If configured, get a user's password hash."""
94    return self._rust_client().get_user_password_hash(user)

If configured, get a user's password hash.

def get_user_type( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> str | None:
 97def get_user_type(
 98    self,
 99    user: mrsm.core.User,
100    debug: bool = False,
101    **kw: Any
102) -> Optional[str]:
103    """If configured, get a user's type."""
104    return self._rust_client().get_user_type(user)

If configured, get a user's type.

def get_user_attributes( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw) -> Any:
73def get_user_attributes(
74    self,
75    user: mrsm.core.User,
76    debug: bool = False,
77    **kw
78) -> Any:
79    """Get a user's attributes."""
80    return self._rust_client().get_user_attributes(user)

Get a user's attributes.

def register_token(self, token: 'Token', debug: bool = False) -> Tuple[bool, str]:
20def register_token(self, token: 'Token', debug: bool = False) -> mrsm.SuccessTuple:
21    """
22    Register the provided token to the API.
23    """
24    return self._rust_client().register_token(token)

Register the provided token to the API.

def get_token_model( self, token_id: uuid.UUID, debug: bool = False) -> 'Union[TokenModel, None]':
27def get_token_model(self, token_id: uuid.UUID, debug: bool = False) -> 'Union[TokenModel, None]':
28    """
29    Return a token's model from the API instance.
30    """
31    from meerschaum.models import TokenModel
32    data = self._rust_client().get_token_model(_token_id(token_id))
33    if data is None:
34        return None
35    return TokenModel(**data)

Return a token's model from the API instance.

def get_tokens( self, labels: List[str] | None = None, debug: bool = False) -> List[ForwardRef('Token')]:
38def get_tokens(self, labels: Optional[List[str]] = None, debug: bool = False) -> List['Token']:
39    """
40    Return the tokens registered to the current user.
41    """
42    from meerschaum.core import Token
43    return [
44        Token(instance=self, **payload)
45        for payload in self._rust_client().get_tokens(labels)
46    ]

Return the tokens registered to the current user.

def edit_token(self, token: 'Token', debug: bool = False) -> Tuple[bool, str]:
49def edit_token(self, token: 'Token', debug: bool = False) -> mrsm.SuccessTuple:
50    """
51    Persist the token's in-memory state to the API.
52    """
53    return self._rust_client().edit_token(token)

Persist the token's in-memory state to the API.

def invalidate_token(self, token: 'Token', debug: bool = False) -> Tuple[bool, str]:
56def invalidate_token(self, token: 'Token', debug: bool = False) -> mrsm.SuccessTuple:
57    """
58    Invalidate the token, disabling it for future requests.
59    """
60    return self._rust_client().invalidate_token(token)

Invalidate the token, disabling it for future requests.

def get_token_scopes( self, token_id: uuid.UUID | ForwardRef('Token'), debug: bool = False) -> List[str]:
63def get_token_scopes(self, token_id: Union[uuid.UUID, 'Token'], debug: bool = False) -> List[str]:
64    """
65    Return the scopes for a token.
66    """
67    return self._rust_client().get_token_scopes(_token_id(token_id))

Return the scopes for a token.

def token_exists( self, token_id: uuid.UUID | ForwardRef('Token'), debug: bool = False) -> bool:
70def token_exists(self, token_id: Union[uuid.UUID, 'Token'], debug: bool = False) -> bool:
71    """
72    Return `True` if a token exists.
73    """
74    return self._rust_client().token_exists(_token_id(token_id))

Return True if a token exists.

def delete_token(self, token: 'Token', debug: bool = False) -> Tuple[bool, str]:
77def delete_token(self, token: 'Token', debug: bool = False) -> mrsm.SuccessTuple:
78    """
79    Delete the token from the API.
80    """
81    return self._rust_client().delete_token(token)

Delete the token from the API.

@classmethod
def from_uri( cls, uri: str, label: str | None = None, as_dict: bool = False) -> APIConnector | Dict[str, str | int]:
13@classmethod
14def from_uri(
15    cls,
16    uri: str,
17    label: Optional[str] = None,
18    as_dict: bool = False,
19) -> Union[
20        'meerschaum.connectors.APIConnector',
21        Dict[str, Union[str, int]],
22    ]:
23    """
24    Create a new APIConnector from a URI string.
25
26    Parameters
27    ----------
28    uri: str
29        The URI connection string.
30
31    label: Optional[str], default None
32        If provided, use this as the connector label.
33        Otherwise use the determined database name.
34
35    as_dict: bool, default False
36        If `True`, return a dictionary of the keyword arguments
37        necessary to create a new `APIConnector`, otherwise create a new object.
38
39    Returns
40    -------
41    A new APIConnector object or a dictionary of attributes (if `as_dict` is `True`).
42    """
43    from meerschaum import _core
44    params = _core.api_uri_params(uri, label)
45    return cls(**params) if not as_dict else params

Create a new APIConnector from a URI string.

Parameters
  • uri (str): The URI connection string.
  • label (Optional[str], default None): If provided, use this as the connector label. Otherwise use the determined database name.
  • as_dict (bool, default False): If True, return a dictionary of the keyword arguments necessary to create a new APIConnector, otherwise create a new object.
Returns
  • A new APIConnector object or a dictionary of attributes (if as_dict is True).
def get_jobs(self, debug: bool = False) -> Dict[str, ForwardRef('Job')]:
17def get_jobs(self, debug: bool = False) -> Dict[str, 'Job']:
18    """
19    Return a dictionary of remote jobs.
20    """
21    from meerschaum.jobs import Job
22    return {
23        name: Job(
24            name,
25            job_meta['sysargs'],
26            executor_keys=str(self),
27            _properties=job_meta['daemon']['properties'],
28        )
29        for name, job_meta in self._rust_client().get_jobs_metadata().items()
30    }

Return a dictionary of remote jobs.

def get_job(self, name: str, debug: bool = False) -> 'Job':
33def get_job(self, name: str, debug: bool = False) -> 'Job':
34    """
35    Return a single Job object.
36    """
37    from meerschaum.jobs import Job
38    metadata = self.get_job_metadata(name, debug=debug)
39    if not metadata:
40        raise ValueError(f"Job '{name}' does not exist.")
41
42    return Job(
43        name,
44        metadata['sysargs'],
45        executor_keys=str(self),
46        _properties=metadata['daemon']['properties'],
47    )

Return a single Job object.

def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]:
50def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]:
51    """
52    Return the metadata for a single job.
53    """
54    return self._rust_client().get_job_metadata(name)

Return the metadata for a single job.

def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]:
57def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]:
58    """
59    Return the daemon properties for a single job.
60    """
61    return self._rust_client().get_job_properties(name)

Return the daemon properties for a single job.

def get_job_exists(self, name: str, debug: bool = False) -> bool:
92def get_job_exists(self, name: str, debug: bool = False) -> bool:
93    """
94    Return whether a job exists.
95    """
96    return self._rust_client().get_job_exists(name)

Return whether a job exists.

def delete_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
 99def delete_job(self, name: str, debug: bool = False) -> SuccessTuple:
100    """
101    Delete a job.
102    """
103    return self._rust_client().delete_job(name)

Delete a job.

def start_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
106def start_job(self, name: str, debug: bool = False) -> SuccessTuple:
107    """
108    Start a job.
109    """
110    return self._rust_client().start_job(name)

Start a job.

def create_job( self, name: str, sysargs: List[str], properties: Dict[str, str] | None = None, debug: bool = False) -> Tuple[bool, str]:
113def create_job(
114    self,
115    name: str,
116    sysargs: List[str],
117    properties: Optional[Dict[str, str]] = None,
118    debug: bool = False,
119) -> SuccessTuple:
120    """
121    Create a job.
122    """
123    return self._rust_client().create_job(name, sysargs, properties)

Create a job.

def stop_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
126def stop_job(self, name: str, debug: bool = False) -> SuccessTuple:
127    """
128    Stop a job.
129    """
130    return self._rust_client().stop_job(name)

Stop a job.

def pause_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
133def pause_job(self, name: str, debug: bool = False) -> SuccessTuple:
134    """
135    Pause a job.
136    """
137    return self._rust_client().pause_job(name)

Pause a job.

def get_logs(self, name: str, debug: bool = False) -> str:
140def get_logs(self, name: str, debug: bool = False) -> str:
141    """
142    Return the logs for a job.
143    """
144    return self._rust_client().get_logs(name)

Return the logs for a job.

def get_job_stop_time(self, name: str, debug: bool = False) -> datetime.datetime | None:
147def get_job_stop_time(self, name: str, debug: bool = False) -> Union[datetime, None]:
148    """
149    Return the job's manual stop time.
150    """
151    data = self._rust_client().get_job_stop_time(name)
152    if data is None:
153        return None
154
155    return datetime.fromisoformat(data)

Return the job's manual stop time.

def monitor_logs( self, name: str, callback_function: Callable[[Any], Any], input_callback_function: Callable[[NoneType], str], stop_callback_function: Callable[[NoneType], str], stop_on_exit: bool = False, strip_timestamps: bool = False, accept_input: bool = True, debug: bool = False):
223def monitor_logs(
224    self,
225    name: str,
226    callback_function: Callable[[Any], Any],
227    input_callback_function: Callable[[None], str],
228    stop_callback_function: Callable[[None], str],
229    stop_on_exit: bool = False,
230    strip_timestamps: bool = False,
231    accept_input: bool = True,
232    debug: bool = False,
233):
234    """
235    Monitor a job's log files and execute a callback with the changes.
236    """
237    return asyncio.run(
238        self.monitor_logs_async(
239            name,
240            callback_function,
241            input_callback_function=input_callback_function,
242            stop_callback_function=stop_callback_function,
243            stop_on_exit=stop_on_exit,
244            strip_timestamps=strip_timestamps,
245            accept_input=accept_input,
246            debug=debug
247        )
248    )

Monitor a job's log files and execute a callback with the changes.

async def monitor_logs_async( self, name: str, callback_function: Callable[[Any], Any], input_callback_function: Callable[[], str], stop_callback_function: Callable[[Tuple[bool, str]], str], stop_on_exit: bool = False, strip_timestamps: bool = False, accept_input: bool = True, debug: bool = False):
176async def monitor_logs_async(
177    self,
178    name: str,
179    callback_function: Callable[[Any], Any],
180    input_callback_function: Callable[[], str],
181    stop_callback_function: Callable[[SuccessTuple], str],
182    stop_on_exit: bool = False,
183    strip_timestamps: bool = False,
184    accept_input: bool = True,
185    debug: bool = False,
186):
187    """
188    Monitor a job's log files and await a callback with the changes.
189
190    The stream runs in the Rust client; coroutine callbacks are awaited on this event loop.
191    Raise `meerschaum.jobs.StopMonitoringLogs` from a callback to stop monitoring.
192    """
193    import threading
194    loop = asyncio.get_running_loop()
195    client = self._rust_client()
196    cancelled = threading.Event()
197    try:
198        await loop.run_in_executor(
199            None,
200            lambda: client.monitor_logs(
201                name,
202                _LoopCallback(loop, callback_function),
203                input_callback_function=(
204                    _LoopCallback(loop, input_callback_function)
205                    if input_callback_function is not None
206                    else None
207                ),
208                stop_callback_function=(
209                    _LoopCallback(loop, stop_callback_function)
210                    if stop_callback_function is not None
211                    else None
212                ),
213                stop_on_exit=stop_on_exit,
214                strip_timestamps=strip_timestamps,
215                stop_event=cancelled,
216            ),
217        )
218    finally:
219        ### A cancelled task ends the stream (the worker thread checks every 100 ms).
220        cancelled.set()

Monitor a job's log files and await a callback with the changes.

The stream runs in the Rust client; coroutine callbacks are awaited on this event loop. Raise meerschaum.jobs.StopMonitoringLogs from a callback to stop monitoring.

def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool:
251def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool:
252    """
253    Return whether a remote job is blocking on stdin.
254    """
255    return self._rust_client().get_job_is_blocking_on_stdin(name)

Return whether a remote job is blocking on stdin.

def get_job_prompt_kwargs(self, name: str, debug: bool = False) -> Dict[str, Any]:
258def get_job_prompt_kwargs(self, name: str, debug: bool = False) -> Dict[str, Any]:
259    """
260    Return the kwargs to the blocking `prompt()`, if available.
261    """
262    return self._rust_client().get_job_prompt_kwargs(name)

Return the kwargs to the blocking prompt(), if available.

def get_job_began(self, name: str, debug: bool = False) -> str | None:
71def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
72    """
73    Return a job's `began` timestamp, if it exists.
74    """
75    return self._rust_client().get_job_began(name)

Return a job's began timestamp, if it exists.

def get_job_ended(self, name: str, debug: bool = False) -> str | None:
78def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
79    """
80    Return a job's `ended` timestamp, if it exists.
81    """
82    return self._rust_client().get_job_ended(name)

Return a job's ended timestamp, if it exists.

def get_job_paused(self, name: str, debug: bool = False) -> str | None:
85def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]:
86    """
87    Return a job's `paused` timestamp, if it exists.
88    """
89    return self._rust_client().get_job_paused(name)

Return a job's paused timestamp, if it exists.

def get_job_status(self, name: str, debug: bool = False) -> str:
64def get_job_status(self, name: str, debug: bool = False) -> str:
65    """
66    Return the job's status.
67    """
68    return self._rust_client().get_job_status(name)

Return the job's status.

def get_team_pipes_count(self, name: str, debug: bool = False, **kwargs: Any) -> int:
42def get_team_pipes_count(self, name: str, debug: bool = False, **kwargs: Any) -> int:
43    """
44    Return how many pipes (and saved Pipelines) name a team, as far as you can see them (what
45    deleting the team gives to `Everyone`): `GET /teams/{name}?counts=true`.
46    """
47    response = self.get(f"/teams/{_segment(name)}", params={'counts': 'true'}, debug=debug)
48    return int(_body(response).get('pipes', 0) or 0)

Return how many pipes (and saved Pipelines) name a team, as far as you can see them (what deleting the team gives to Everyone): GET /teams/{name}?counts=true.

def get_team_jobs_count(self, name: str, debug: bool = False, **kwargs: Any) -> int:
51def get_team_jobs_count(self, name: str, debug: bool = False, **kwargs: Any) -> int:
52    """
53    Return how many of the server's jobs belong to a team: `GET /teams/{name}?counts=true`.
54    """
55    response = self.get(f"/teams/{_segment(name)}", params={'counts': 'true'}, debug=debug)
56    return int(_body(response).get('jobs', 0) or 0)

Return how many of the server's jobs belong to a team: GET /teams/{name}?counts=true.

def get_users_permissions(self, debug: bool = False, **kwargs: Any) -> List[Dict[str, Any]]:
59def get_users_permissions(self, debug: bool = False, **kwargs: Any) -> List[Dict[str, Any]]:
60    """
61    Return every user as the server describes them: `username`, `user_type`, `role`,
62    `permissions`, `teams` (`[{'name', 'role'}]`), `primary_team` and `email` (`None` unless it
63    is yours, you are an administrator, or you manage a team the user is in):
64    `GET /users/permissions`.
65    """
66    response = self.get('/users/permissions', debug=debug)
67    return list(_body(response).get('users', None) or [])

Return every user as the server describes them: username, user_type, role, permissions, teams ([{'name', 'role'}]), primary_team and email (None unless it is yours, you are an administrator, or you manage a team the user is in): GET /users/permissions.

def get_user_permissions_record(self, user: Any, debug: bool = False, **kwargs: Any) -> Dict[str, Any]:
70def get_user_permissions_record(
71    self,
72    user: Any,
73    debug: bool = False,
74    **kwargs: Any
75) -> Dict[str, Any]:
76    """
77    Return one user as `get_users_permissions()` describes them (`{}` for an unknown user):
78    `GET /users/{username}/permissions`.
79    """
80    username = str(getattr(user, 'username', user))
81    response = self.get(f"/users/{_segment(username)}/permissions", debug=debug)
82    if response.status_code == 404:
83        return {}
84    return dict(_body(response).get('user', None) or {})

Return one user as get_users_permissions() describes them ({} for an unknown user): GET /users/{username}/permissions.

def get_pipe_access( self, pipe: meerschaum.Pipe, debug: bool = False, **kwargs: Any) -> Dict[str, Any]:
 87def get_pipe_access(self, pipe: mrsm.Pipe, debug: bool = False, **kwargs: Any) -> Dict[str, Any]:
 88    """
 89    Return a pipe's `access` (`owner`, `team`, `visibility`, `edit`), its `locks` and what you
 90    `may` do with it (`edit`, `manage_access`, `lock`): `GET /pipes/{keys}/access`.
 91    """
 92    from meerschaum.connectors.api._pipes import pipe_r_url
 93    if pipe.connector_keys == 'mrsm' and pipe.metric_key == 'pipeline':
 94        r_url = f"/pipelines/{_segment(pipe.location_key)}/access"
 95    else:
 96        r_url = pipe_r_url(pipe) + '/access'
 97    response = self.get(
 98        r_url,
 99        params={'instance_keys': self.get_pipe_instance_keys(pipe)},
100        debug=debug,
101    )
102    body = _body(response)
103    return {key: body.get(key, None) for key in ('access', 'locks', 'may')}

Return a pipe's access (owner, team, visibility, edit), its locks and what you may do with it (edit, manage_access, lock): GET /pipes/{keys}/access.

def get_connector( type: str = None, label: str = None, refresh: bool = False, debug: bool = False, _load_plugins: bool = True, _warn_undefined: bool = True, **kw: Any) -> Connector:
 76def get_connector(
 77    type: str = None,
 78    label: str = None,
 79    refresh: bool = False,
 80    debug: bool = False,
 81    _load_plugins: bool = True,
 82    _warn_undefined: bool = True,
 83    **kw: Any
 84) -> Connector:
 85    """
 86    Return existing connector or create new connection and store for reuse.
 87    
 88    You can create new connectors if enough parameters are provided for the given type and flavor.
 89
 90    Parameters
 91    ----------
 92    type: Optional[str], default None
 93        Connector type (sql, api, etc.).
 94        Defaults to the type of the configured `instance_connector`.
 95
 96    label: Optional[str], default None
 97        Connector label (e.g. main). Defaults to `'main'`.
 98
 99    refresh: bool, default False
100        Refresh the Connector instance / construct new object. Defaults to `False`.
101
102    kw: Any
103        Other arguments to pass to the Connector constructor.
104        If the Connector has already been constructed and new arguments are provided,
105        `refresh` is set to `True` and the old Connector is replaced.
106
107    Returns
108    -------
109    A new Meerschaum connector (e.g. `meerschaum.connectors.api.APIConnector`,
110    `meerschaum.connectors.sql.SQLConnector`).
111    
112    Examples
113    --------
114    The following parameters would create a new
115    `meerschaum.connectors.sql.SQLConnector` that isn't in the configuration file.
116
117    ```
118    >>> conn = get_connector(
119    ...     type = 'sql',
120    ...     label = 'newlabel',
121    ...     flavor = 'sqlite',
122    ...     database = '/file/path/to/database.db'
123    ... )
124    >>>
125    ```
126
127    """
128    from meerschaum.connectors.parse import parse_instance_keys
129    from meerschaum.config import get_config
130    from meerschaum._internal.static import STATIC_CONFIG
131    from meerschaum.utils.warnings import warn
132    global _loaded_plugin_connectors
133    if isinstance(type, str) and not label and ':' in type:
134        type, label = type.split(':', maxsplit=1)
135
136    if _load_plugins:
137        with _locks['_loaded_plugin_connectors']:
138            if not _loaded_plugin_connectors:
139                load_plugin_connectors()
140                _load_builtin_custom_connectors()
141                _loaded_plugin_connectors = True
142
143    if type is None and label is None:
144        default_instance_keys = get_config('meerschaum', 'instance', patch=True)
145        ### recursive call to get_connector
146        return parse_instance_keys(default_instance_keys)
147
148    ### NOTE: the default instance connector may not be main.
149    ### Only fall back to 'main' if the type is provided by the label is omitted.
150    label = label if label is not None else STATIC_CONFIG['connectors']['default_label']
151
152    ### A type declared by a native (Rust) plugin is registered on first use.
153    if type not in connectors and _load_plugins and isinstance(type, str):
154        _load_native_connector_type(type)
155
156    ### type might actually be a label. Check if so and raise a warning.
157    if type not in connectors:
158        possibilities, poss_msg = [], ""
159        for _type in get_config('meerschaum', 'connectors'):
160            if type in get_config('meerschaum', 'connectors', _type):
161                possibilities.append(f"{_type}:{type}")
162        if len(possibilities) > 0:
163            poss_msg = " Did you mean"
164            for poss in possibilities[:-1]:
165                poss_msg += f" '{poss}',"
166            if poss_msg.endswith(','):
167                poss_msg = poss_msg[:-1]
168            if len(possibilities) > 1:
169                poss_msg += " or"
170            poss_msg += f" '{possibilities[-1]}'?"
171
172        warn(f"Cannot create Connector of type '{type}'." + poss_msg, stack=False)
173        return None
174
175    if 'sql' not in types:
176        from meerschaum.connectors.plugin import PluginConnector
177        from meerschaum.connectors.valkey import ValkeyConnector
178        with _locks['types']:
179            types.update({
180                'api': APIConnector,
181                'sql': SQLConnector,
182                'plugin': PluginConnector,
183                'valkey': ValkeyConnector,
184            })
185
186    ### determine if we need to call the constructor
187    if not refresh:
188        ### see if any user-supplied arguments differ from the existing instance
189        if label in connectors[type]:
190            warning_message = None
191            for attribute, value in kw.items():
192                if attribute not in connectors[type][label].meta:
193                    import inspect
194                    cls = connectors[type][label].__class__
195                    cls_init_signature = inspect.signature(cls)
196                    cls_init_params = cls_init_signature.parameters
197                    if attribute not in cls_init_params:
198                        warning_message = (
199                            f"Received new attribute '{attribute}' not present in connector " +
200                            f"{connectors[type][label]}.\n"
201                        )
202                elif connectors[type][label].__dict__[attribute] != value:
203                    warning_message = (
204                        f"Mismatched values for attribute '{attribute}' in connector "
205                        + f"'{connectors[type][label]}'.\n" +
206                        f"  - Keyword value: '{value}'\n" +
207                        f"  - Existing value: '{connectors[type][label].__dict__[attribute]}'\n"
208                    )
209            if warning_message is not None:
210                warning_message += (
211                    "\nSetting `refresh` to True and recreating connector with type:"
212                    + f" '{type}' and label '{label}'."
213                )
214                refresh = True
215                warn(warning_message)
216        else: ### connector doesn't yet exist
217            refresh = True
218
219    ### only create an object if refresh is True
220    ### (can be manually specified, otherwise determined above)
221    if refresh:
222        ### A connector defined nowhere: say where connectors come from (not "Missing flavor").
223        undefined = _undefined_connector_message(type, label, kw)
224        if undefined is not None:
225            if _warn_undefined:
226                warn(undefined, stack=False)
227            return None
228        with _locks['connectors']:
229            try:
230                ### will raise an error if configuration is incorrect / missing
231                conn = types[type](label=label, **kw)
232                connectors[type][label] = conn
233            except InvalidAttributesError as ie:
234                warn(
235                    f"Incorrect attributes for connector '{type}:{label}'.\n"
236                    + str(ie),
237                    stack = False,
238                )
239                conn = None
240            except Exception as e:
241                from meerschaum.utils.formatting import print_exception
242                print_exception()
243                warn(
244                    f"Exception when creating connector '{type}:{label}'.\n" + str(e),
245                    stack = False,
246                )
247                conn = None
248        if conn is None:
249            return None
250
251    return connectors[type][label]

Return existing connector or create new connection and store for reuse.

You can create new connectors if enough parameters are provided for the given type and flavor.

Parameters
  • type (Optional[str], default None): Connector type (sql, api, etc.). Defaults to the type of the configured instance_connector.
  • label (Optional[str], default None): Connector label (e.g. main). Defaults to 'main'.
  • refresh (bool, default False): Refresh the Connector instance / construct new object. Defaults to False.
  • kw (Any): Other arguments to pass to the Connector constructor. If the Connector has already been constructed and new arguments are provided, refresh is set to True and the old Connector is replaced.
Returns
Examples

The following parameters would create a new meerschaum.connectors.sql.SQLConnector that isn't in the configuration file.

>>> conn = get_connector(
...     type = 'sql',
...     label = 'newlabel',
...     flavor = 'sqlite',
...     database = '/file/path/to/database.db'
... )
>>>
def is_connected(keys: str, **kw) -> bool:
348def is_connected(keys: str, **kw) -> bool:
349    """
350    Check if the connector keys correspond to an active connection.
351    If the connector has not been created, it will immediately return `False`.
352    If the connector exists but cannot communicate with the source, return `False`.
353    
354    **NOTE:** Only works with instance connectors (`SQLConnectors` and `APIConnectors`).
355    Keyword arguments are passed to `meerschaum.connectors.poll.retry_connect`.
356
357    Parameters
358    ----------
359    keys:
360        The keys to the connector (e.g. `'sql:main'`).
361        
362    Returns
363    -------
364    A `bool` corresponding to whether a successful connection may be made.
365
366    """
367    import warnings
368    if ':' not in keys:
369        warn(f"Invalid connector keys '{keys}'")
370
371    try:
372        typ, label = keys.split(':')
373    except Exception:
374        return False
375    if typ not in instance_types:
376        return False
377    if label not in connectors.get(typ, {}):
378        return False
379
380    from meerschaum.connectors.parse import parse_instance_keys
381    conn = parse_instance_keys(keys)
382    try:
383        with warnings.catch_warnings():
384            warnings.filterwarnings('ignore')
385            return conn.test_connection(**kw)
386    except Exception:
387        return False

Check if the connector keys correspond to an active connection. If the connector has not been created, it will immediately return False. If the connector exists but cannot communicate with the source, return False.

NOTE: Only works with instance connectors (SQLConnectors and APIConnectors). Keyword arguments are passed to meerschaum.connectors.poll.retry_connect.

Parameters
  • keys:: The keys to the connector (e.g. 'sql:main').
Returns
  • A bool corresponding to whether a successful connection may be made.
class allow_protected:
277class allow_protected:
278    """
279    Allow writes to protected connectors (`protected: true`) inside a `with` block.
280
281    Every mutation the core makes through a protected connector (sync, clear, drop, delete,
282    register, edit, `verify schema`, `SQLConnector.exec()` / `to_sql()`) is refused unless writes
283    are allowed: by `--allow-protected` on the command line, by `MRSM_ALLOW_PROTECTED=1` in the
284    environment, or inside this context manager. The allowance is process-wide while the block
285    runs (other threads see it too); child processes need `MRSM_ALLOW_PROTECTED=1`.
286
287    Examples
288    --------
289    >>> import meerschaum as mrsm
290    >>> from meerschaum.connectors import allow_protected
291    >>> pipe = mrsm.Pipe('plugin:noaa', 'weather', instance='sql:prod')
292    >>> with allow_protected():
293    ...     success, msg = pipe.sync(df)
294    """
295
296    def __enter__(self) -> 'allow_protected':
297        from meerschaum import _core
298        _core.allow_protected_enter()
299        return self
300
301    def __exit__(self, *exc: Any) -> None:
302        from meerschaum import _core
303        _core.allow_protected_exit()

Allow writes to protected connectors (protected: true) inside a with block.

Every mutation the core makes through a protected connector (sync, clear, drop, delete, register, edit, verify schema, SQLConnector.exec() / to_sql()) is refused unless writes are allowed: by --allow-protected on the command line, by MRSM_ALLOW_PROTECTED=1 in the environment, or inside this context manager. The allowance is process-wide while the block runs (other threads see it too); child processes need MRSM_ALLOW_PROTECTED=1.

Examples
>>> import meerschaum as mrsm
>>> from meerschaum.connectors import allow_protected
>>> pipe = mrsm.Pipe('plugin:noaa', 'weather', instance='sql:prod')
>>> with allow_protected():
...     success, msg = pipe.sync(df)