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
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
>>>
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.
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 )
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
SuccessTupleof the result.
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.
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
idto fetch.
Returns
- The
idfor the pipe's document orNone.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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
--tagswhich are stored underparameters: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')]
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
boolindicating the table exists.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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 is900). AchunksizeofNonewill 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 toTrue. - debug (bool, default False): Verbosity toggle. Defaults to False.
Returns
- A
SuccessTupleof success (bool) and message (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
SuccessTupleindicating success.
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
paramsfilter.
Returns
- A
SuccessTupleindicating success.
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
datetimevalue to search from (inclusive). - end (datetime | int | None, default None):
The lastest
datetimevalue 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.
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
datetimevalue to search from (inclusive). - end (datetime | int | None, default None):
The lastest
datetimevalue 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.
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
datetimeorintvalue of thedatetimeaxis.
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.
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'}]}
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
intof the number of bytes occupied by the target table, - or
Noneif the size cannot be determined.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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.
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.
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).
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.
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.
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.
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).
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).
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.
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').
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).
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']).
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).
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']).
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/
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 thebootstrap connectorscommand. - wait (bool, default False):
If
True, block until a database connection has been made. Defaults toFalse. - connect (bool, default False):
If
True, immediately attempt to connect the database and raise a warning if the connection fails. Defaults toFalse. - 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.
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
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).
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).
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).
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.
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.
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.
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.
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
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.
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.
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.
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.
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()orselect()) 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
:nameplaceholders (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, declaredjsonandgeometrycolumns. chunksize (Optional[int], default -1): How many chunks to read at a time.
Nonewill 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_hookis 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-chunksfor an example. NOTE:as_iteratorMUST be False (default). as_hook_results (bool, default False): If
True, return aListof the outputs of the hook function. Only applicable ifchunk_hookis not None.NOTE:
as_iteratorMUST beFalse(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
chunksizeof1000andchunksof100. - 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.chunksizemust not beNone(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, returnNoneinstead of raising when the query fails, without a warning (Meerschaum 4's behavior). By default, database errors raise. - as_polars (Optional[bool], default None):
Truereturns Polars frames,Falsepandas;Nonefollows thedataframesetting (meerschaum.utils.dataframe.get_dataframe_library()). - as_pandas (Optional[bool], default None):
Truereturns pandas frames,FalsePolars;Nonefollows the setting.
Returns
- A DataFrame (default case), or an iterator, or a list of dataframes / iterators.
Raises the database error if the query fails (
Nonewithsilent=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).
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
- query (str): The SQL query to execute.
- *args (Any):
The arguments passed to
meerschaum.connectors.sql.SQLConnector.execifuse_pandasisFalse(default) or tomeerschaum.connectors.sql.SQLConnector.read. - use_pandas (bool, default False):
If
True, usemeerschaum.connectors.SQLConnector.read, otherwise usemeerschaum.connectors.sql.SQLConnector.exec(default). NOTE: This is alwaysTruefor DuckDB. - **kw (Any):
See
args.silent=TruereturnsNoneinstead of raising when the query fails.
Returns
- Any value returned from the query (
Nonewhen 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 (
Nonewithsilent=True). - On a protected connector without the allowance, the query must be one read statement
- (else
PermissionError), and it runs read-only, as inread().
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
queryis a list or tuple, callself.exec_queries()instead. - args (Any):
The statement's parameters: a dictionary for
:nameplaceholders, 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, returnNoneinstead 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 ifqueryis 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 ifqueryis a list of strings. - with_connection (bool, default False):
If
True, return a tuple including the connection object. This does not apply ifqueryis a list of strings.
Returns
- The
sqlalchemyresult object, or a tuple with the connection ifwith_connectionis provided. - Raises the database error if the query fails (
Nonewithsilent=True), and PermissionErroron a protected connector unless writes are allowed- (
--allow-protected,MRSM_ALLOW_PROTECTED=1,meerschaum.connectors.allow_protected()).
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)
An alias for meerschaum.connectors.sql.SQLConnector.exec.
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 abool. Defaults toFalse. - as_dict (bool, default False):
If
True, return a dictionary of transaction information. The keys aresuccess,msg,start,end,duration,num_rows,chunksize,method, andtarget. - kw (Any):
Additional arguments will be passed to the DataFrame's
to_sqlfunction
Returns
- Either a
boolor aSuccessTuple(depends onas_tuple).
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_errorisTrue, rollback the transaction if a query fails. - silent (bool, default False):
If
True, suppress warnings.
Returns
- A list of SQLAlchemy results.
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.Connectionobject.
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(default1),retry_waitseconds between attempts (default0) andwarn(print the error of a failed attempt, defaultFalse).
Returns
Trueif a connection is made, otherwiseFalse.
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
fetchmetadata.- 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
beginto search for data (optional).
- How many minutes before
- pipe.columns['datetime']: str
- begin (Union[datetime, int, str, None], default None):
Most recent datatime to search for data.
If
backtrack_minutesis provided, subtractbacktrack_minutes. - end (Union[datetime, int, str, None], default None):
The latest datetime to search for data.
If
endisNone, 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.
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.
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.
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
intof the number of bytes occupied by the target table, - or
Noneif the size could not be determined.
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
SuccessTupleindicating success, including the amount of disk reclaimed.
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
SuccessTupleindicating success, including the change in disk size.
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
SuccessTupleindicating success.
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.
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), runVACUUM 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
SuccessTupleindicating success, including the amount of disk reclaimed.
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
SuccessTupleindicating success.
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 (Noneif unknown).interval: the physical partition width (timedelta, epoch-int, orNone).
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_dfand_create_missing_partitionspaths, 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
SuccessTupleindicating success.
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).
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.
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.
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.
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.
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.
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 theORDER BYclause. - 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
begindatetime (i.e.DATEADD). - end_add_minutes (int, default 0):
The number of minutes to add to the
enddatetime (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):
Truereturns Polars,Falsepandas;Nonefollows thedataframesetting (meerschaum.utils.dataframe.get_dataframe_library()). - as_pandas (Optional[bool], default None):
Truereturns pandas,FalsePolars;Nonefollows the setting. - debug (bool, default False): Verbosity toggle.
Returns
- A DataFrame of the requested library (an iterator of them with
as_iterator), orNone - when the table does not exist.
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.
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 theORDER BYclause. - 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
begindatetime (i.e.DATEADD). - end_add_minutes (int, default 0):
The number of minutes to add to the
enddatetime (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
SELECTquery to retrieve a pipe's data.
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.
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.
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.
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.
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 is900). AchunksizeofNonewill 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 toTrue. - blocking (bool, default True):
If
True, wait for sync to finish and return its result, otherwise asyncronously sync. Defaults toTrue. - debug (bool, default False): Verbosity toggle. Defaults to False.
- kw (Any):
Catch-all for keyword arguments.
replace=Truereplaces the pipe's rows of[begin, end)(matchingparams) with the rows ofdfin one transaction; a bound that is not given is the rows' own datetime range (begin= the earliest,end= just after the latest), as forpipe.sync(df, replace=True).
Returns
- A
SuccessTupleof success (bool) and message (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
WHEREclause. Seemeerschaum.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 is900). AchunksizeofNonewill 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.
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
WHEREclause. Seemeerschaum.utils.sql.build_where. - newest (bool, default True):
If
True, get the most recent datetime (honoringparams). IfFalse, 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
datetimeobject (orintif using an integer axis) if the pipe exists, otherwiseNone.
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
boolcorresponding to whether a pipe's table exists.
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
intfor the number of rows if thepipeexists, otherwiseNone.
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
SuccessTupleindicated success.
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.
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
SuccessTupleindicating success.
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.Tableobject.
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',
}
>>>
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
Noneif nothing is configured.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 newSQLConnector, otherwise create a new object.
Returns
- A new SQLConnector object or a dictionary of attributes (if
as_dictisTrue).
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'}
>>>
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.
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)
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.
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.
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).
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.
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.
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,filesandtimeoutare sent asrequestswould.
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()).
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()).
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()).
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()).
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()).
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.
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).
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.
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.
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")
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.
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.
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.
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).
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.
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).
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
pipestable (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.
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).
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.
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.
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.
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).
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.
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
- pipe (meerschaum.Pipe): The pipe whose attributes we are fetching.
Returns
- A dictionary of a pipe's attributes.
- If the pipe does not exist, return an empty dictionary.
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 (honoringparams). IfFalse, get the oldest datetime (ASC instead of DESC).
Returns
- The most recent (or oldest if
newestisFalse) datetime of a pipe, - rounded down to the closest minute.
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
- pipe ('meerschaum.Pipe'): The pipe which were are querying.
Returns
- A bool indicating whether a pipe's underlying table exists.
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.
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.
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
- pipe (meerschaum.Pipe:): The pipe to be dropped.
Returns
- A success 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.
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
- pipe (meerschaum.Pipe): The pipe whose columns to be queried.
Returns
- A dictionary mapping column names to their database types, or
Noneon failure.
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
nameandtype, - or
Noneon failure.
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.
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.
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.
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.
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.
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.
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.
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.
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']).
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 newAPIConnector, otherwise create a new object.
Returns
- A new APIConnector object or a dictionary of attributes (if
as_dictisTrue).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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,
refreshis set toTrueand the old Connector is replaced.
Returns
- A new Meerschaum connector (e.g.
meerschaum.connectors.api.APIConnector, meerschaum.connectors.sql.SQLConnector).
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'
... )
>>>
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
boolcorresponding to whether a successful connection may be made.
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)