meerschaum

Meerschaum Python API
Welcome to the Meerschaum Python API technical documentation! Here you can find information about the classes and functions provided by the meerschaum package. Visit meerschaum.io for general usage documentation.
Root Module
For your convenience, the following classes and functions may be imported from the root meerschaum namespace:
Classes
Examples
Build a Connector
Get existing connectors or build a new one in-memory with the meerschaum.get_connector() factory function:
import meerschaum as mrsm
sql_conn = mrsm.get_connector(
'sql:temp',
flavor='sqlite',
database='/tmp/tmp.db',
)
df = sql_conn.read("SELECT 1 AS foo")
print(df)
# shape: (1, 1)
# ┌─────┐
# │ foo │
# │ --- │
# │ i64 │
# ╞═════╡
# │ 1 │
# └─────┘
sql_conn.to_sql(df, 'foo')
print(sql_conn.read('foo'))
# foo
# 0 1
Create a Custom Connector Class
Decorate your connector classes with meerschaum.make_connector() to designate it as a custom connector:
from datetime import datetime, timezone
from random import randint
import meerschaum as mrsm
from meerschaum.utils.dtypes import round_time
@mrsm.make_connector
class FooConnector(mrsm.Connector):
REQUIRED_ATTRIBUTES = ['username', 'password']
def fetch(
self,
begin: datetime | None = None,
end: datetime | None = None,
):
now = begin or round_time(datetime.now(timezone.utc))
return [
{'ts': now, 'id': 1, 'vl': randint(1, 100)},
{'ts': now, 'id': 2, 'vl': randint(1, 100)},
{'ts': now, 'id': 3, 'vl': randint(1, 100)},
]
foo_conn = mrsm.get_connector(
'foo:bar',
username='foo',
password='bar',
)
docs = foo_conn.fetch()
Build a Pipe
Build a meerschaum.Pipe in-memory:
from datetime import datetime
import meerschaum as mrsm
pipe = mrsm.Pipe(
foo_conn, 'demo',
instance=sql_conn,
columns={'datetime': 'ts', 'id': 'id'},
tags=['production'],
)
pipe.sync(begin=datetime(2024, 1, 1))
df = pipe.get_data()
print(df)
# shape: (3, 3)
# ┌─────────────────────┬─────┬─────┐
# │ ts ┆ id ┆ vl │
# │ --- ┆ --- ┆ --- │
# │ datetime[μs] ┆ i64 ┆ i64 │
# ╞═════════════════════╪═════╪═════╡
# │ 2024-01-01 00:00:00 ┆ 1 ┆ 97 │
# │ 2024-01-01 00:00:00 ┆ 2 ┆ 18 │
# │ 2024-01-01 00:00:00 ┆ 3 ┆ 96 │
# └─────────────────────┴─────┴─────┘
DataFrames are Polars by default (pip install meerschaum[polars]); set dataframe: pandas in
meerschaum.yaml (a root created before 5.0.0rc15 has it) or pass as_pandas=True for pandas.
Add temporary=True to skip registering the pipe in the pipes table.
Query an Integer-Axis Pipe by Datetime
When a pipe's datetime axis is an integer epoch, set precision so datetime bounds can be translated to the axis's integer value. This lets you pass a datetime begin / end to meerschaum.Pipe.get_data() (and to actions like show data, clear, and deduplicate):
from datetime import datetime, timezone
import meerschaum as mrsm
pipe = mrsm.Pipe(
'demo', 'epoch',
instance='sql:temp',
columns={'datetime': 'ts'},
dtypes={'ts': 'int'},
precision='millisecond',
)
pipe.sync([{'ts': 1780099200000}])
### The datetime is translated to the epoch value `1780099200000`.
df = pipe.get_data(begin='2026-05-30')
Integer bounds (begin=1780099200000) still pass through unchanged. A datetime bound on a non-epoch integer axis (no precision set) raises a ValueError. Convert directly with meerschaum.utils.dtypes.datetime_to_int():
from datetime import datetime, timezone
from meerschaum.utils.dtypes import datetime_to_int
datetime_to_int(datetime(2026, 5, 30, tzinfo=timezone.utc), 'millisecond')
# 1780099200000
Get Registered Pipes
The meerschaum.get_pipes() function returns a dictionary hierarchy of pipes by connector, metric, and location:
import meerschaum as mrsm
pipes = mrsm.get_pipes(instance='sql:temp')
pipe = pipes['foo:bar']['demo'][None]
Add as_list=True to flatten the hierarchy:
import meerschaum as mrsm
pipes = mrsm.get_pipes(
tags=['production'],
instance=sql_conn,
as_list=True,
)
print(pipes)
# [Pipe('foo:bar', 'demo', instance='sql:temp')]
Filter by the dtype of the datetime index column with datetime_dtypes. Accepted values are 'datetime', 'int', and 'None'; prefix with '_' to negate:
import meerschaum as mrsm
### Only pipes with a timestamp datetime index:
timestamp_pipes = mrsm.get_pipes(datetime_dtypes=['datetime'], as_list=True)
### Only pipes with an integer datetime index:
int_pipes = mrsm.get_pipes(datetime_dtypes=['int'], as_list=True)
### Exclude pipes without a datetime index:
datetime_pipes = mrsm.get_pipes(datetime_dtypes=['_None'], as_list=True)
Sync Pipes in Dependency Order with a Pipeline
A meerschaum.Pipeline syncs a set of pipes in dependency order through the Syncing Engine. Plan it first (a dry run), sync it, and save it under a name to load it again later:
import meerschaum as mrsm
pipeline = mrsm.Pipeline(pipe, parents=True) # this pipe and every pipe it reads
print(pipeline.plan()) # what would sync, and why
run = pipeline.sync()
print(run.success, run.inserted, run.updated)
success, msg = pipeline.save('demo')
pipeline = mrsm.Pipeline('demo', instance='sql:temp')
print(mrsm.get_pipelines(instance='sql:temp'))
# [Pipeline('demo', instance='sql:temp')]
print(mrsm.get_runs(instance='sql:temp', pipeline='demo', limit=1))
Import Plugins
You can import a plugin's module through meerschaum.Plugin.module:
import meerschaum as mrsm
plugin = mrsm.Plugin('noaa')
with mrsm.Venv(plugin):
noaa = plugin.module
If your plugin has submodules, use meerschaum.plugins.from_plugin_import:
from meerschaum.plugins import from_plugin_import
get_defined_pipes = from_plugin_import('compose.utils.pipes', 'get_defined_pipes')
Import multiple plugins with meerschaum.plugins.import_plugins:
from meerschaum.plugins import import_plugins
noaa, compose = import_plugins('noaa', 'compose')
Create a Job
Create a meerschaum.Job with name and sysargs:
import meerschaum as mrsm
job = mrsm.Job('syncing-engine', 'sync pipes --loop')
success, msg = job.start()
Pass executor_keys as the connectors keys of an API instance to create a remote job:
import meerschaum as mrsm
job = mrsm.Job(
'foo',
'sync pipes -s daily',
executor_keys='api:main',
)
Import from a Virtual Environment
Use the meerschaum.Venv context manager to activate a virtual environment:
import meerschaum as mrsm
with mrsm.Venv('noaa'):
import requests
print(requests.__file__)
# /home/bmeares/.config/meerschaum/venvs/noaa/lib/python3.12/site-packages/requests/__init__.py
To import packages which may not be installed, use meerschaum.attempt_import():
import meerschaum as mrsm
requests = mrsm.attempt_import('requests', venv='noaa')
print(requests.__file__)
# /home/bmeares/.config/meerschaum/venvs/noaa/lib/python3.12/site-packages/requests/__init__.py
Run Actions
Run sysargs with meerschaum.entry():
import meerschaum as mrsm
success, msg = mrsm.entry('show pipes + show version : x2')
Use meerschaum.actions.get_action() to access an action function directly:
from meerschaum.actions import get_action
show_pipes = get_action(['show', 'pipes'])
success, msg = show_pipes(connector_keys=['plugin:noaa'])
Get a dictionary of available subactions with meerschaum.actions.get_subactions():
from meerschaum.actions import get_subactions
subactions = get_subactions('show')
success, msg = subactions['pipes']()
Create a Plugin
Run bootstrap plugin to create a new plugin:
mrsm bootstrap plugin example
This will create example.py in your plugins directory (default ~/.config/meerschaum/plugins/, Windows: %APPDATA%\Meerschaum\plugins). You may paste the example code from the "Create a Custom Action" example below.
Open your plugin with edit plugin:
mrsm edit plugin example
Run edit plugin and paste the example code below to try out the features.
See the writing plugins guide for more in-depth documentation.
Create a Custom Action
Decorate a function with meerschaum.actions.make_action to designate it as an action. Subactions will be automatically detected if not decorated:
from meerschaum.actions import make_action
@make_action
def sing():
print('What would you like me to sing?')
return True, "Success"
def sing_tune():
return False, "I don't know that song!"
def sing_song():
print('Hello, World!')
return True, "Success"
Use meerschaum.plugins.add_plugin_argument() to create new parameters for your action:
from meerschaum.plugins import make_action, add_plugin_argument
add_plugin_argument(
'--song', type=str, help='What song to sing.',
)
@make_action
def sing_melody(action=None, song=None):
to_sing = action[0] if action else song
if not to_sing:
return False, "Please tell me what to sing!"
return True, f'~I am singing {to_sing}~'
mrsm sing melody lalala
mrsm sing melody --song do-re-mi
Add a Page to the Web Dashboard
Use the decorators meerschaum.plugins.dash_plugin() and meerschaum.plugins.web_page() to add new pages to the web dashboard:
from meerschaum.plugins import dash_plugin, web_page
@dash_plugin
def init_dash(dash_app):
import dash.html as html
import dash_bootstrap_components as dbc
from dash import Input, Output, no_update
### Routes to '/dash/my-page'
@web_page('/my-page', login_required=False)
def my_page():
return dbc.Container([
html.H1("Hello, World!"),
dbc.Button("Click me", id='my-button'),
html.Div(id="my-output-div"),
])
@dash_app.callback(
Output('my-output-div', 'children'),
Input('my-button', 'n_clicks'),
)
def my_button_click(n_clicks):
if not n_clicks:
return no_update
return html.P(f'You clicked {n_clicks} times!')
Submodules
meerschaum.actions
Access functions for actions and subactions.
meerschaum.actions.actionsmeerschaum.actions.get_action()meerschaum.actions.get_completer()meerschaum.actions.get_main_action_name()meerschaum.actions.get_subactions()
meerschaum.config
Read and write the Meerschaum configuration registry.
meerschaum.config.get_config()meerschaum.config.get_plugin_config()meerschaum.config.write_config()meerschaum.config.write_plugin_config()meerschaum.config.environment
Patch configuration and connectors from environment variables.
meerschaum.config.environment.apply_environment_patches()meerschaum.config.environment.apply_environment_config()meerschaum.config.environment.apply_environment_uris()meerschaum.config.environment.apply_connector_uri()meerschaum.config.environment.get_connector_env_regex()meerschaum.config.environment.get_connector_env_vars()meerschaum.config.environment.get_env_vars()meerschaum.config.environment.get_daemon_env_vars()meerschaum.config.environment.replace_env()
meerschaum.connectors
Build connectors to interact with databases and fetch data.
meerschaum.connectors.get_connector()meerschaum.connectors.make_connector()meerschaum.connectors.is_connected()meerschaum.connectors.poll.retry_connect()meerschaum.connectors.Connectormeerschaum.connectors.sql.SQLConnectormeerschaum.connectors.api.APIConnectormeerschaum.connectors.valkey.ValkeyConnector
meerschaum.jobs
Start background jobs.
meerschaum.jobs.Jobmeerschaum.jobs.Executormeerschaum.jobs.systemd.SystemdExecutormeerschaum.jobs.get_jobs()meerschaum.jobs.get_filtered_jobs()meerschaum.jobs.get_running_jobs()meerschaum.jobs.get_stopped_jobs()meerschaum.jobs.get_paused_jobs()meerschaum.jobs.get_restart_jobs()meerschaum.jobs.make_executor()meerschaum.jobs.check_restart_jobs()meerschaum.jobs.start_check_jobs_thread()meerschaum.jobs.stop_check_jobs_thread()meerschaum.jobs.get_executor_keys_from_context()
meerschaum.plugins
Access plugin modules and other API utilties.
meerschaum.plugins.Pluginmeerschaum.plugins.api_plugin()meerschaum.plugins.dash_plugin()meerschaum.plugins.import_plugins()meerschaum.plugins.reload_plugins()meerschaum.plugins.get_plugins()meerschaum.plugins.get_data_plugins()meerschaum.plugins.add_plugin_argument()meerschaum.plugins.pre_sync_hook()meerschaum.plugins.post_sync_hook()
meerschaum.utils
Utility functions are available in several submodules:
meerschaum.utils.daemon.daemon_entry()meerschaum.utils.daemon.daemon_action()meerschaum.utils.daemon.get_daemons()meerschaum.utils.daemon.get_daemon_ids()meerschaum.utils.daemon.get_running_daemons()meerschaum.utils.daemon.get_paused_daemons()meerschaum.utils.daemon.get_stopped_daemons()meerschaum.utils.daemon.get_filtered_daemons()meerschaum.utils.daemon.run_daemon()meerschaum.utils.daemon.Daemonmeerschaum.utils.daemon.FileDescriptorInterceptormeerschaum.utils.daemon.RotatingFile
meerschaum.utils.daemon
Manage background jobs.
meerschaum.utils.dataframe.add_missing_cols_to_df()meerschaum.utils.dataframe.chunksize_to_npartitions()meerschaum.utils.dataframe.df_from_literal()meerschaum.utils.dataframe.df_is_chunk_generator()meerschaum.utils.dataframe.enforce_dtypes()meerschaum.utils.dataframe.frame_from_arrow()meerschaum.utils.dataframe.get_dataframe_library()meerschaum.utils.dataframe.filter_unseen_df()meerschaum.utils.dataframe.get_bool_cols()meerschaum.utils.dataframe.get_bytes_cols()meerschaum.utils.dataframe.get_datetime_bound_from_df()meerschaum.utils.dataframe.get_date_cols()meerschaum.utils.dataframe.get_datetime_cols()meerschaum.utils.dataframe.get_datetime_cols_types()meerschaum.utils.dataframe.get_first_valid_dask_partition()meerschaum.utils.dataframe.get_geometry_cols()meerschaum.utils.dataframe.get_geometry_cols_types()meerschaum.utils.dataframe.get_json_cols()meerschaum.utils.dataframe.get_numeric_cols()meerschaum.utils.dataframe.get_special_cols()meerschaum.utils.dataframe.get_unhashable_cols()meerschaum.utils.dataframe.get_unique_index_values()meerschaum.utils.dataframe.get_uuid_cols()meerschaum.utils.dataframe.parse_df_datetimes()meerschaum.utils.dataframe.query_df()meerschaum.utils.dataframe.resolve_dataframe_library()meerschaum.utils.dataframe.to_json()meerschaum.utils.dataframe.to_pandas()meerschaum.utils.dataframe.to_polars()meerschaum.utils.dataframe.to_simple_lines()meerschaum.utils.dataframe.parse_simple_lines()
meerschaum.utils.dataframe
Manipulate dataframes.
meerschaum.utils.dtypes.are_dtypes_equal()meerschaum.utils.dtypes.attempt_cast_to_bytes()meerschaum.utils.dtypes.attempt_cast_to_geometry()meerschaum.utils.dtypes.attempt_cast_to_numeric()meerschaum.utils.dtypes.attempt_cast_to_uuid()meerschaum.utils.dtypes.coerce_timezone()meerschaum.utils.dtypes.datetime_to_int()meerschaum.utils.dtypes.deserialize_base64()meerschaum.utils.dtypes.deserialize_bytes_string()meerschaum.utils.dtypes.deserialize_geometry()meerschaum.utils.dtypes.encode_bytes_for_bytea()meerschaum.utils.dtypes.geometry_is_gpkg()meerschaum.utils.dtypes.geometry_is_wkt()meerschaum.utils.dtypes.get_current_timestamp()meerschaum.utils.dtypes.get_geometry_type_srid()meerschaum.utils.dtypes.is_dtype_numeric()meerschaum.utils.dtypes.is_dtype_special()meerschaum.utils.dtypes.json_serialize_value()meerschaum.utils.dtypes.none_if_null()meerschaum.utils.dtypes.project_geometry()meerschaum.utils.dtypes.quantize_decimal()meerschaum.utils.dtypes.serialize_bytes()meerschaum.utils.dtypes.serialize_datetime()meerschaum.utils.dtypes.serialize_date()meerschaum.utils.dtypes.serialize_decimal()meerschaum.utils.dtypes.serialize_geometry()meerschaum.utils.dtypes.to_datetime()meerschaum.utils.dtypes.to_pandas_dtype()meerschaum.utils.dtypes.value_is_null()meerschaum.utils.dtypes.get_next_precision_unit()meerschaum.utils.dtypes.round_time()
meerschaum.utils.dtypes
Work with data types.
meerschaum.utils.formatting.colored()meerschaum.utils.formatting.extract_stats_from_message()meerschaum.utils.formatting.fill_ansi()meerschaum.utils.formatting.format_bytes()meerschaum.utils.formatting.format_dataframe()meerschaum.utils.formatting.get_console()meerschaum.utils.formatting.highlight_pipes()meerschaum.utils.formatting.make_header()meerschaum.utils.formatting.pipe_repr()meerschaum.utils.formatting.pprint()meerschaum.utils.formatting.pprint_df()meerschaum.utils.formatting.pprint_pipes()meerschaum.utils.formatting.print_options()meerschaum.utils.formatting.print_pipes_results()meerschaum.utils.formatting.print_tuple()meerschaum.utils.formatting.translate_rich_to_termcolor()
meerschaum.utils.formatting
Format output text.
meerschaum.utils.misc.items_str()meerschaum.utils.misc.is_int()meerschaum.utils.misc.is_uuid()meerschaum.utils.misc.interval_str()meerschaum.utils.misc.filter_keywords()meerschaum.utils.misc.generate_password()meerschaum.utils.misc.string_to_dict()meerschaum.utils.misc.iterate_chunks()meerschaum.utils.misc.timed_input()meerschaum.utils.misc.replace_pipes_in_dict()meerschaum.utils.misc.is_valid_email()meerschaum.utils.misc.string_width()meerschaum.utils.misc.replace_password()meerschaum.utils.misc.parse_config_substitution()meerschaum.utils.misc.edit_file()meerschaum.utils.misc.get_in_ex_params()meerschaum.utils.misc.separate_negation_values()meerschaum.utils.misc.flatten_list()meerschaum.utils.misc.make_symlink()meerschaum.utils.misc.is_symlink()meerschaum.utils.misc.wget()meerschaum.utils.misc.add_method_to_class()meerschaum.utils.misc.is_pipe_registered()meerschaum.utils.misc.get_cols_lines()meerschaum.utils.misc.sorted_dict()meerschaum.utils.misc.flatten_pipes_dict()meerschaum.utils.misc.dict_from_od()meerschaum.utils.misc.remove_ansi()meerschaum.utils.misc.get_connector_labels()meerschaum.utils.misc.json_serialize_datetime()meerschaum.utils.misc.async_wrap()meerschaum.utils.misc.is_docker_available()meerschaum.utils.misc.is_android()meerschaum.utils.misc.is_bcp_available()meerschaum.utils.misc.truncate_string_sections()meerschaum.utils.misc.safely_extract_tar()meerschaum.utils.misc.get_directory_size()
meerschaum.utils.misc
Miscellaneous utility functions.
meerschaum.utils.packages.attempt_import()meerschaum.utils.packages.get_module_path()meerschaum.utils.packages.manually_import_module()meerschaum.utils.packages.get_install_no_version()meerschaum.utils.packages.determine_version()meerschaum.utils.packages.need_update()meerschaum.utils.packages.get_pip()meerschaum.utils.packages.pip_install()meerschaum.utils.packages.pip_uninstall()meerschaum.utils.packages.completely_uninstall_package()meerschaum.utils.packages.run_python_package()meerschaum.utils.packages.lazy_import()meerschaum.utils.packages.pandas_name()meerschaum.utils.packages.import_pandas()meerschaum.utils.packages.import_rich()meerschaum.utils.packages.import_dcc()meerschaum.utils.packages.import_html()meerschaum.utils.packages.get_modules_from_package()meerschaum.utils.packages.import_children()meerschaum.utils.packages.reload_package()meerschaum.utils.packages.reload_meerschaum()meerschaum.utils.packages.is_installed()meerschaum.utils.packages.venv_contains_package()meerschaum.utils.packages.package_venv()meerschaum.utils.packages.ensure_readline()meerschaum.utils.packages.get_prerelease_dependencies()
meerschaum.utils.packages
Manage Python packages.
meerschaum.utils.pipes
Utilities for working with pipe objects.
meerschaum.utils.sql.build_where()meerschaum.utils.sql.clean()meerschaum.utils.sql.get_sqlalchemy_table()meerschaum.utils.sql.dateadd_str()meerschaum.utils.sql.test_connection()meerschaum.utils.sql.get_distinct_col_count()meerschaum.utils.sql.sql_item_name()meerschaum.utils.sql.pg_capital()meerschaum.utils.sql.oracle_capital()meerschaum.utils.sql.truncate_item_name()meerschaum.utils.sql.table_exists()meerschaum.utils.sql.get_table_cols_types()meerschaum.utils.sql.get_table_cols_indices()meerschaum.utils.sql.get_update_queries()meerschaum.utils.sql.get_null_replacement()meerschaum.utils.sql.get_db_version()meerschaum.utils.sql.get_rename_table_queries()meerschaum.utils.sql.get_create_table_queries()meerschaum.utils.sql.wrap_query_with_cte()meerschaum.utils.sql.format_cte_subquery()meerschaum.utils.sql.session_execute()meerschaum.utils.sql.get_reset_autoincrement_queries()meerschaum.utils.sql.get_postgis_geo_columns_types()meerschaum.utils.sql.get_create_schema_if_not_exists_queries()
meerschaum.utils.sql
Build SQL queries.
meerschaum.utils.threading
Manage threads and process-wide stop signals.
meerschaum.utils.venv.Venvmeerschaum.utils.venv.activate_venv()meerschaum.utils.venv.deactivate_venv()meerschaum.utils.venv.get_module_venv()meerschaum.utils.venv.get_venvs()meerschaum.utils.venv.init_venv()meerschaum.utils.venv.inside_venv()meerschaum.utils.venv.is_venv_active()meerschaum.utils.venv.venv_exec()meerschaum.utils.venv.venv_executable()meerschaum.utils.venv.venv_exists()meerschaum.utils.venv.venv_target_path()meerschaum.utils.venv.verify_venv()
meerschaum.utils.venv
Manage virtual environments.
meerschaum.utils.warnings
Print warnings, errors, info, and debug messages.
1#! /usr/bin/env python 2# -*- coding: utf-8 -*- 3# vim:fenc=utf-8 4 5""" 6Copyright 2020–2026 Bennett Meares 7 8Licensed under the Apache License, Version 2.0 (the "License"); 9you may not use this file except in compliance with the License. 10You may obtain a copy of the License at 11 12 http://www.apache.org/licenses/LICENSE-2.0 13 14Unless required by applicable law or agreed to in writing, software 15distributed under the License is distributed on an "AS IS" BASIS, 16WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17See the License for the specific language governing permissions and 18limitations under the License. 19""" 20 21import sys as _sys 22 23 24def _cli_launch() -> bool: 25 """ 26 Whether this process is the `mrsm` command line, which does not need the façade. 27 28 `import meerschaum` is EAGER, exactly as it always was (the façade is imported here, under this 29 package's import lock, so threads which import `meerschaum.*` and read `meerschaum.Pipe` at the 30 same time cannot deadlock). The one exception is the CLI process: `mrsm` (the console script 31 `_meerschaum_mrsm`, which sets `sys._meerschaum_cli`) and `python -m meerschaum` (which jobs 32 run; `sys.orig_argv` starts with `-m meerschaum`) only need the extension, and importing the 33 façade costs about 0.12 s. There the façade is imported on first use instead (PEP 562). 34 """ 35 if getattr(_sys, '_meerschaum_cli', False): 36 return True 37 argv = getattr(_sys, 'orig_argv', None) or [] 38 skip = False 39 for index, arg in enumerate(argv[1:], start=1): 40 if skip: 41 skip = False 42 continue 43 if arg == '-m': 44 return argv[index + 1:index + 2] == ['meerschaum'] 45 if arg == '-mmeerschaum': 46 return True 47 if arg in ('-X', '-W', '--check-hash-based-pycs'): 48 skip = True 49 continue 50 if not arg.startswith('-'): 51 return False 52 return False 53 54 55_LAZY = _cli_launch() 56if not _LAZY: 57 import atexit 58 59 from meerschaum.utils.typing import SuccessTuple 60 from meerschaum.utils.packages import attempt_import 61 from meerschaum.core.Pipe import Pipe 62 from meerschaum.core.Pipe._sync import SyncResult 63 from meerschaum.core.Pipe._status import get_sync_statuses 64 from meerschaum.core.Pipeline import Pipeline, Plan, RunResult, get_pipelines, get_runs 65 from meerschaum.core.Pipeline._metrics import get_engine_metrics, initialize_engine_metrics 66 from meerschaum.core.Team import Team, get_teams 67 from meerschaum.plugins import Plugin 68 from meerschaum.utils.venv import Venv 69 from meerschaum.jobs import Job, make_executor 70 from meerschaum.connectors import get_connector, Connector, InstanceConnector, make_connector 71 from meerschaum.utils import get_pipes 72 from meerschaum.utils.formatting import pprint 73 from meerschaum._internal.docs import index as __doc__ 74 from meerschaum.config import __version__, get_config 75 from meerschaum._internal.entry import entry 76 from meerschaum.__main__ import _close_pools 77 78 atexit.register(_close_pools) 79 80else: 81 import importlib as _importlib 82 83 _FACADE = 'meerschaum._internal._facade' 84 _FACADE_NAMES = ( 85 'atexit', 86 'SuccessTuple', 87 'attempt_import', 88 'Pipe', 89 'SyncResult', 90 'get_sync_statuses', 91 'Pipeline', 92 'Plan', 93 'RunResult', 94 'get_pipelines', 95 'get_runs', 96 'get_engine_metrics', 97 'initialize_engine_metrics', 98 'Team', 99 'get_teams', 100 'Plugin', 101 'Venv', 102 'Job', 103 'make_executor', 104 'get_connector', 105 'Connector', 106 'InstanceConnector', 107 'make_connector', 108 'get_pipes', 109 'pprint', 110 '__doc__', 111 '__version__', 112 'get_config', 113 'entry', 114 '_close_pools', 115 ) 116 117 118__pdoc__ = {'gui': False, 'api': False, 'core': False, '_internal': False} 119__all__ = ( 120 "get_pipes", 121 "get_pipelines", 122 "get_runs", 123 "get_engine_metrics", 124 "initialize_engine_metrics", 125 "get_sync_statuses", 126 "get_teams", 127 "get_connector", 128 "get_config", 129 "Pipe", 130 "Pipeline", 131 "Plan", 132 "RunResult", 133 "SyncResult", 134 "Team", 135 "Plugin", 136 "SuccessTuple", 137 "Venv", 138 "Job", 139 "make_executor", 140 "pprint", 141 "attempt_import", 142 "actions", 143 "config", 144 "connectors", 145 "jobs", 146 "plugins", 147 "utils", 148 "Connector", 149 "InstanceConnector", 150 "make_connector", 151 "entry", 152) 153 154 155if _LAZY: 156 def _facade_module(): 157 """ 158 Import the façade (once) and return its module: complete, or partial when called while the 159 façade itself is being imported (a circular import sees the names bound so far, as it did 160 when they were bound in this module's body). 161 """ 162 return _importlib.import_module(_FACADE) 163 164 165 def _bind_facade(facade) -> None: 166 """Bind the façade's names in this namespace once its import has finished.""" 167 spec = getattr(facade, '__spec__', None) 168 if getattr(spec, '_initializing', False): 169 return 170 namespace = globals() 171 for name in _FACADE_NAMES: 172 if name in vars(facade): 173 namespace[name] = vars(facade)[name] 174 175 176 def __getattr__(name: str): 177 """Resolve a façade name, importing the façade on first use (PEP 562).""" 178 if name.startswith('__') and name.endswith('__') and name != '__version__': 179 ### Probes (`__wrapped__`, `__file__`, ...) never import anything. 180 raise AttributeError(f"module 'meerschaum' has no attribute '{name}'") 181 submodule = _sys.modules.get(f'meerschaum.{name}') 182 if submodule is not None: 183 ### A submodule being imported right now (`import meerschaum.config._paths as _paths` reads 184 ### `meerschaum.config` before the import of it has finished): never a reason to load the façade. 185 return submodule 186 if name == '__version__': 187 ### The version needs the config package, not the façade. 188 from meerschaum.config._version import __version__ 189 return __version__ 190 if name not in _FACADE_NAMES and name not in __all__: 191 ### Anything else is a submodule (`from meerschaum import _core`, `mrsm.core`): import just 192 ### that one; the façade stays unloaded. 193 try: 194 return _importlib.import_module(f'meerschaum.{name}') 195 except ModuleNotFoundError as e: 196 if e.name != f'meerschaum.{name}': 197 raise 198 raise AttributeError(f"module 'meerschaum' has no attribute '{name}'") 199 facade = _facade_module() 200 _bind_facade(facade) 201 namespace = globals() 202 if name in namespace: 203 return namespace[name] 204 if name in vars(facade) and name in _FACADE_NAMES: 205 return vars(facade)[name] 206 if name in __all__: 207 ### A submodule the façade did not import as a side effect (`mrsm.plugins`, ...). 208 return _importlib.import_module(f'meerschaum.{name}') 209 raise AttributeError(f"module 'meerschaum' has no attribute '{name}'") 210 211 212 def __dir__(): 213 _bind_facade(_facade_module()) 214 return sorted(set(globals()) | set(__all__))
29def get_pipes( 30 connector_keys: Union[str, List[str], None] = None, 31 metric_keys: Union[str, List[str], None] = None, 32 location_keys: Union[str, List[str], None] = None, 33 tags: Optional[List[str]] = None, 34 targets: Optional[List[str]] = None, 35 datetime_dtypes: Optional[List[str]] = None, 36 params: Optional[Dict[str, Any]] = None, 37 mrsm_instance: Union[str, InstanceConnector, None] = None, 38 instance: Union[str, InstanceConnector, None] = None, 39 as_list: bool = False, 40 as_tags_dict: bool = False, 41 as_targets_dict: bool = False, 42 method: str = 'registered', 43 workers: Optional[int] = None, 44 debug: bool = False, 45 _cache_parameters: bool = True, 46 **kw: Any 47) -> Union[PipesDict, List[mrsm.Pipe], Dict[str, mrsm.Pipe]]: 48 """ 49 Return a dictionary or list of `meerschaum.Pipe` objects. 50 51 Parameters 52 ---------- 53 connector_keys: Union[str, List[str], None], default None 54 String or list of connector keys. 55 If omitted or is `'*'`, fetch all possible keys. 56 If a string begins with `'_'`, select keys that do NOT match the string. 57 58 metric_keys: Union[str, List[str], None], default None 59 String or list of metric keys. See `connector_keys` for formatting. 60 61 location_keys: Union[str, List[str], None], default None 62 String or list of location keys. See `connector_keys` for formatting. 63 64 tags: Optional[List[str]], default None 65 If provided, only include pipes with these tags. 66 67 datetime_dtypes: Optional[List[str]], default None 68 If provided, only include pipes with the corresponding `datetime` axis dtypes. 69 Accepted values are `datetime`, `int`, `None` (or `null`, etc.). 70 May be negated by `_`. 71 72 params: Optional[Dict[str, Any]], default None 73 Dictionary of additional parameters to search by. 74 Params are parsed into a SQL WHERE clause. 75 E.g. `{'a': 1, 'b': 2}` equates to `'WHERE a = 1 AND b = 2'` 76 77 mrsm_instance: Union[str, InstanceConnector, None], default None 78 Connector keys for the Meerschaum instance of the pipes. 79 Must be a `meerschaum.connectors.sql.SQLConnector.SQLConnector` or 80 `meerschaum.connectors.api.APIConnector.APIConnector`. 81 82 as_list: bool, default False 83 If `True`, return pipes in a list instead of a hierarchical dictionary. 84 `False` : `{connector_keys: {metric_key: {location_key: Pipe}}}` 85 `True` : `[Pipe]` 86 87 as_tags_dict: bool, default False 88 If `True`, return a dictionary mapping tags to pipes. 89 Pipes with multiple tags will be repeated. 90 91 as_targets_dict: bool, default False 92 If `True`, return a dictionary mapping `(schema, target)` tuples to pipes. 93 Pipes sharing the same target across different schemata are grouped separately. 94 95 method: str, default 'registered' 96 Available options: `['registered', 'explicit', 'all']` 97 If `'registered'` (default), create pipes based on registered keys in the connector's pipes table 98 (API or SQL connector, depends on mrsm_instance). 99 If `'explicit'`, create pipes from provided connector_keys, metric_keys, and location_keys 100 instead of consulting the pipes table. Useful for creating non-existent pipes. 101 If `'all'`, create pipes from predefined metrics and locations. Required `connector_keys`. 102 **NOTE:** Method `'all'` is not implemented! 103 104 workers: Optional[int], default None 105 If provided (and `as_tags_dict` or `as_targets_dict` is `True`), set the number of workers 106 for the pool to fetch tags or targets. 107 Only takes effect if the instance connector supports multi-threading. 108 109 **kw: Any: 110 Keyword arguments to pass to the `meerschaum.Pipe` constructor. 111 112 Returns 113 ------- 114 A dictionary of dictionaries and `meerschaum.Pipe` objects 115 in the connector, metric, location hierarchy. 116 If `as_list` is `True`, return a list of `meerschaum.Pipe` objects. 117 If `as_tags_dict` is `True`, return a dictionary mapping tags to pipes. 118 If `as_targets_dict` is `True`, return a dictionary mapping targets to pipes. 119 120 Examples 121 -------- 122 ``` 123 >>> ### Manual definition: 124 >>> pipes = { 125 ... <connector_keys>: { 126 ... <metric_key>: { 127 ... <location_key>: Pipe( 128 ... <connector_keys>, 129 ... <metric_key>, 130 ... <location_key>, 131 ... ), 132 ... }, 133 ... }, 134 ... }, 135 >>> ### Accessing a single pipe: 136 >>> pipes['sql:main']['weather'][None] 137 >>> ### Return a list instead: 138 >>> get_pipes(as_list=True) 139 [Pipe('sql:main', 'weather')] 140 >>> get_pipes(as_tags_dict=True) 141 {'gvl': Pipe('sql:main', 'weather')} 142 ``` 143 """ 144 import json 145 from collections import defaultdict 146 from meerschaum.config import get_config 147 from meerschaum.config.static import STATIC_CONFIG 148 from meerschaum.utils.warnings import error 149 from meerschaum.utils.misc import filter_keywords, separate_negation_values 150 from meerschaum.utils.pool import get_pool 151 from meerschaum.utils.pipes import replace_pipes_syntax 152 from meerschaum.utils.debug import dprint 153 from meerschaum.utils.dtypes import value_is_null, get_current_timestamp 154 from meerschaum import Pipe 155 156 negation_prefix = STATIC_CONFIG['system']['fetch_pipes_keys']['negation_prefix'] 157 if datetime_dtypes: 158 if isinstance(datetime_dtypes, str): 159 datetime_dtypes = [datetime_dtypes] 160 for _dt in datetime_dtypes: 161 _clean = str(_dt).lstrip(negation_prefix).lower() 162 if _clean not in ('datetime', 'int') and not value_is_null(_clean): 163 error(f"Invalid datetime dtype '{_dt}'.") 164 165 if connector_keys is None: 166 connector_keys = [] 167 if metric_keys is None: 168 metric_keys = [] 169 if location_keys is None: 170 location_keys = [] 171 if params is None: 172 params = {} 173 if tags is None: 174 tags = [] 175 176 if isinstance(connector_keys, str): 177 connector_keys = [connector_keys] 178 if isinstance(metric_keys, str): 179 metric_keys = [metric_keys] 180 if isinstance(location_keys, str): 181 location_keys = [location_keys] 182 183 ### Get SQL or API connector (keys come from `connector.fetch_pipes_keys()`). 184 if mrsm_instance is None: 185 mrsm_instance = instance 186 if mrsm_instance is None: 187 mrsm_instance = get_config('meerschaum', 'instance', patch=True) 188 if isinstance(mrsm_instance, str): 189 from meerschaum.connectors.parse import parse_instance_keys 190 connector = parse_instance_keys(keys=mrsm_instance, debug=debug) 191 else: 192 from meerschaum.connectors import instance_types 193 valid_connector = False 194 if hasattr(mrsm_instance, 'type'): 195 if mrsm_instance.type in instance_types: 196 valid_connector = True 197 if not valid_connector: 198 error(f"Invalid instance connector: {mrsm_instance}") 199 connector = mrsm_instance 200 if debug: 201 dprint(f"Using instance connector: {connector}") 202 if not connector: 203 error(f"Could not create connector from keys: '{mrsm_instance}'") 204 205 ### Get a list of tuples for the keys needed to build pipes. 206 result = fetch_pipes_keys( 207 method, 208 connector, 209 connector_keys = connector_keys, 210 metric_keys = metric_keys, 211 location_keys = location_keys, 212 tags = tags, 213 params = params, 214 workers = workers, 215 debug = debug 216 ) 217 if result is None: 218 error("Unable to build pipes!") 219 result_items: List[Tuple] = ( 220 list(result.items()) 221 if isinstance(result, dict) 222 else [(None, keys_tuple) for keys_tuple in result] 223 ) 224 225 ### Populate the `pipes` dictionary with Pipes based on the keys 226 ### obtained from the chosen `method`. 227 in_dtypes, ex_dtypes = separate_negation_values(datetime_dtypes or []) 228 in_targets, ex_targets = separate_negation_values(targets or []) 229 pipes: PipesDict = {} 230 targets_pipes: Dict[Tuple[Optional[str], str], List[mrsm.Pipe]] = defaultdict(lambda: []) 231 connector_schema = getattr(connector, 'schema', None) 232 connector_is_sql = getattr(connector, 'type', None) == 'sql' 233 connector_flavor = getattr(connector, 'flavor', None) 234 for pipe_id, keys_tuple in result_items: 235 ck, mk, lk = keys_tuple[0], keys_tuple[1], keys_tuple[2] 236 pipe_tags_or_parameters = keys_tuple[3] if len(keys_tuple) == 4 else None 237 pipe_parameters = ( 238 pipe_tags_or_parameters 239 if isinstance(pipe_tags_or_parameters, (dict, str)) 240 else None 241 ) 242 if isinstance(pipe_parameters, str): 243 pipe_parameters = json.loads(pipe_parameters) 244 pipe_tags = ( 245 pipe_tags_or_parameters 246 if isinstance(pipe_tags_or_parameters, list) 247 else ( 248 pipe_tags_or_parameters.get('tags', []) 249 if isinstance(pipe_tags_or_parameters, dict) 250 else None 251 ) 252 ) 253 254 pipe = Pipe( 255 ck, mk, lk, 256 mrsm_instance = connector, 257 parameters = pipe_parameters, 258 tags = pipe_tags, 259 debug = debug, 260 **filter_keywords(Pipe, **kw) 261 ) 262 pipe.__dict__['_tags'] = pipe_tags 263 if pipe_id is not None: 264 pipe._cache_value('_id', pipe_id, memory_only=True, debug=debug) 265 if pipe_parameters is not None: 266 now = get_current_timestamp('ms', as_int=True) / 1000 267 full_attributes = { 268 'connector_keys': ck, 269 'metric_key': mk, 270 'location_key': lk, 271 'parameters': pipe_parameters, 272 } 273 if pipe_id is not None: 274 full_attributes['pipe_id'] = pipe_id 275 pipe._cache_value('attributes', full_attributes, memory_only=True, debug=debug) 276 pipe._cache_value('_attributes_sync_time', now, memory_only=True, debug=debug) 277 278 if datetime_dtypes or targets: 279 parameters_str = str(pipe_parameters) 280 if pipe_parameters is None or 'MRSM{' in parameters_str or 'Pipe(' in parameters_str: 281 pipe_parameters = pipe.get_parameters(debug=debug) 282 283 keep_pipe = True 284 285 if datetime_dtypes: 286 columns_val = (pipe_parameters or {}).get('columns', {}) or {} 287 dt_col = columns_val.get('datetime', None) 288 pipe_dtypes = ( 289 ((pipe_parameters or {}).get('dtypes', None) or {}) 290 if dt_col 291 else None 292 ) 293 dt_typ = pipe_dtypes.get(dt_col, None) if dt_col else None 294 295 def _dtype_matches(clean_d): 296 if not dt_col: 297 return value_is_null(clean_d) 298 return ( 299 (clean_d == 'int' and 'int' in str(dt_typ).lower()) 300 or 301 (clean_d == 'datetime' and 'int' not in str(dt_typ).lower()) 302 ) 303 304 in_match = not in_dtypes or any(_dtype_matches(d) for d in in_dtypes) 305 ex_match = bool(ex_dtypes and any(_dtype_matches(d) for d in ex_dtypes)) 306 keep_pipe = keep_pipe and in_match and not ex_match 307 if not keep_pipe: 308 continue 309 310 if targets: 311 pipe_target = pipe.target 312 in_target_match = not in_targets or any(t == pipe_target for t in in_targets) 313 ex_target_match = bool(ex_targets and any(t == pipe_target for t in ex_targets)) 314 keep_pipe = keep_pipe and in_target_match and not ex_target_match 315 if not keep_pipe: 316 continue 317 318 if ck not in pipes: 319 pipes[ck] = {} 320 321 if mk not in pipes[ck]: 322 pipes[ck][mk] = {} 323 324 325 pipes[ck][mk][lk] = pipe 326 327 if as_targets_dict: 328 raw_params = pipe_parameters if isinstance(pipe_parameters, dict) else {} 329 schema = raw_params.get('schema') or connector_schema 330 explicit_target = ( 331 raw_params.get('target') 332 or raw_params.get('target_name') 333 or raw_params.get('target_table') 334 or raw_params.get('target_table_name') 335 ) 336 if explicit_target: 337 target_name = ( 338 replace_pipes_syntax(explicit_target, _pipe=pipe) 339 if isinstance(explicit_target, str) and '{{' in explicit_target 340 else explicit_target 341 ) 342 else: 343 target_name = pipe._target_legacy() 344 if connector_is_sql and connector_flavor: 345 from meerschaum.utils.sql import truncate_item_name 346 target_name = truncate_item_name(target_name, connector_flavor) 347 targets_pipes[(schema, target_name)].append(pipe) 348 349 if not as_list and not as_tags_dict and not as_targets_dict: 350 return pipes 351 352 from meerschaum.utils.pipes import flatten_pipes_dict 353 pipes_list = flatten_pipes_dict(pipes) 354 if as_list: 355 return pipes_list 356 357 pool = get_pool(workers=(workers if connector.IS_THREAD_SAFE else 1)) 358 359 def gather_pipe_tags(pipe: mrsm.Pipe) -> Tuple[mrsm.Pipe, List[str]]: 360 _tags = pipe.__dict__.get('_tags', None) 361 gathered_tags = _tags if _tags is not None else pipe.tags 362 return pipe, (gathered_tags or []) 363 364 if as_tags_dict: 365 tags_pipes = defaultdict(lambda: []) 366 pipes_tags = dict(pool.map(gather_pipe_tags, pipes_list)) 367 for pipe, tags in pipes_tags.items(): 368 for tag in (tags or []): 369 tags_pipes[tag].append(pipe) 370 371 return dict(tags_pipes) 372 373 if as_targets_dict: 374 return dict(targets_pipes) 375 376 raise NotImplementedError("No futher options for returning pipes.")
Return a dictionary or list of meerschaum.Pipe objects.
Parameters
- connector_keys (Union[str, List[str], None], default None):
String or list of connector keys.
If omitted or is
'*', fetch all possible keys. If a string begins with'_', select keys that do NOT match the string. - metric_keys (Union[str, List[str], None], default None):
String or list of metric keys. See
connector_keysfor formatting. - location_keys (Union[str, List[str], None], default None):
String or list of location keys. See
connector_keysfor formatting. - tags (Optional[List[str]], default None): If provided, only include pipes with these tags.
- datetime_dtypes (Optional[List[str]], default None):
If provided, only include pipes with the corresponding
datetimeaxis dtypes. Accepted values aredatetime,int,None(ornull, etc.). May be negated by_. - params (Optional[Dict[str, Any]], default None):
Dictionary of additional parameters to search by.
Params are parsed into a SQL WHERE clause.
E.g.
{'a': 1, 'b': 2}equates to'WHERE a = 1 AND b = 2' - mrsm_instance (Union[str, InstanceConnector, None], default None):
Connector keys for the Meerschaum instance of the pipes.
Must be a
meerschaum.connectors.sql.SQLConnector.SQLConnectorormeerschaum.connectors.api.APIConnector.APIConnector. - as_list (bool, default False):
If
True, return pipes in a list instead of a hierarchical dictionary.False:{connector_keys: {metric_key: {location_key: Pipe}}}True:[Pipe] - as_tags_dict (bool, default False):
If
True, return a dictionary mapping tags to pipes. Pipes with multiple tags will be repeated. - as_targets_dict (bool, default False):
If
True, return a dictionary mapping(schema, target)tuples to pipes. Pipes sharing the same target across different schemata are grouped separately. - method (str, default 'registered'):
Available options:
['registered', 'explicit', 'all']If'registered'(default), create pipes based on registered keys in the connector's pipes table (API or SQL connector, depends on mrsm_instance). If'explicit', create pipes from provided connector_keys, metric_keys, and location_keys instead of consulting the pipes table. Useful for creating non-existent pipes. If'all', create pipes from predefined metrics and locations. Requiredconnector_keys. NOTE: Method'all'is not implemented! - workers (Optional[int], default None):
If provided (and
as_tags_dictoras_targets_dictisTrue), set the number of workers for the pool to fetch tags or targets. Only takes effect if the instance connector supports multi-threading. - **kw (Any:):
Keyword arguments to pass to the
meerschaum.Pipeconstructor.
Returns
- A dictionary of dictionaries and
meerschaum.Pipeobjects - in the connector, metric, location hierarchy.
- If
as_listisTrue, return a list ofmeerschaum.Pipeobjects. - If
as_tags_dictisTrue, return a dictionary mapping tags to pipes. - If
as_targets_dictisTrue, return a dictionary mapping targets to pipes.
Examples
>>> ### Manual definition:
>>> pipes = {
... <connector_keys>: {
... <metric_key>: {
... <location_key>: Pipe(
... <connector_keys>,
... <metric_key>,
... <location_key>,
... ),
... },
... },
... },
>>> ### Accessing a single pipe:
>>> pipes['sql:main']['weather'][None]
>>> ### Return a list instead:
>>> get_pipes(as_list=True)
[Pipe('sql:main', 'weather')]
>>> get_pipes(as_tags_dict=True)
{'gvl': Pipe('sql:main', 'weather')}
1549def get_pipelines(instance: Union[str, Any, None] = None) -> List[Pipeline]: 1550 """ 1551 Return the Pipelines saved on `instance` (default: the configured instance), sorted by name. 1552 1553 Parameters 1554 ---------- 1555 instance: Union[str, InstanceConnector, None], default None 1556 The instance whose saved Pipelines to list. 1557 1558 Returns 1559 ------- 1560 A list of `Pipeline` objects, each with its `name` and `definition`. 1561 1562 Examples 1563 -------- 1564 >>> for pipeline in mrsm.get_pipelines(instance='sql:main'): 1565 ... print(pipeline.name, len(pipeline.members)) 1566 """ 1567 from meerschaum.core.Pipeline._saved import list_pipelines 1568 registry = _registry_of(instance) 1569 return [ 1570 Pipeline._from_definitions([definition], registry) 1571 for _, definition in sorted(list_pipelines(registry).items()) 1572 ]
Return the Pipelines saved on instance (default: the configured instance), sorted by name.
Parameters
- instance (Union[str, InstanceConnector, None], default None): The instance whose saved Pipelines to list.
Returns
- A list of
Pipelineobjects, each with itsnameanddefinition.
Examples
>>> for pipeline in mrsm.get_pipelines(instance='sql:main'):
... print(pipeline.name, len(pipeline.members))
1575def get_runs( 1576 instance: Union[str, Any, None] = None, 1577 limit: int = 20, 1578 status: Optional[str] = None, 1579 pipeline: Union[str, Pipeline, None] = None, 1580 pipe: Union[mrsm.Pipe, str, None] = None, 1581 since: Union[datetime, timedelta, str, None] = None, 1582) -> List[Dict[str, Any]]: 1583 """ 1584 Recent engine runs on `instance` (default: the configured instance), newest first. 1585 1586 Parameters 1587 ---------- 1588 instance: Union[str, InstanceConnector, None], default None 1589 The instance that keeps the run history. 1590 1591 limit: int, default 20 1592 The most runs to return. 1593 1594 status: Optional[str], default None 1595 Only `'running'`, `'succeeded'` or `'failed'` runs. 1596 1597 pipeline: Union[str, Pipeline, None], default None 1598 Only the runs of this saved Pipeline (a name or a `Pipeline`). 1599 1600 pipe: Union[mrsm.Pipe, str, None], default None 1601 Only the runs which targeted or contained this pipe (a `Pipe` or a `pipe_ref`). 1602 1603 since: Union[datetime, timedelta, str, None], default None 1604 Only runs started at or after this time: a `datetime` (naive means UTC), a 1605 `timedelta` (that long ago, e.g. `timedelta(hours=1)`) or an ISO 8601 string. 1606 1607 Returns 1608 ------- 1609 Run summaries: `run` (the stored row), `status`, `duration_seconds` and `counts` 1610 (nodes by status). See `Pipeline.get_runs()`. 1611 1612 Examples 1613 -------- 1614 >>> from datetime import timedelta 1615 >>> failed = mrsm.get_runs(status='failed', since=timedelta(days=1)) 1616 """ 1617 connector = ( 1618 parse_instance_keys(instance) 1619 if isinstance(instance, (str, type(None))) 1620 else instance 1621 ) 1622 request = build_request(connector) 1623 request['limit'] = limit 1624 if status is not None: 1625 request['status'] = status 1626 if pipeline is not None: 1627 request['run_name'] = pipeline.name if isinstance(pipeline, Pipeline) else str(pipeline) 1628 if pipe is not None: 1629 request['pipe'] = pipe if isinstance(pipe, str) else _pipe_ref(pipe) 1630 since = since_text(since) 1631 if since is not None: 1632 request['since'] = since 1633 return call('engine_runs', request)
Recent engine runs on instance (default: the configured instance), newest first.
Parameters
- instance (Union[str, InstanceConnector, None], default None): The instance that keeps the run history.
- limit (int, default 20): The most runs to return.
- status (Optional[str], default None):
Only
'running','succeeded'or'failed'runs. - pipeline (Union[str, Pipeline, None], default None):
Only the runs of this saved Pipeline (a name or a
Pipeline). - pipe (Union[mrsm.Pipe, str, None], default None):
Only the runs which targeted or contained this pipe (a
Pipeor apipe_ref). - since (Union[datetime, timedelta, str, None], default None):
Only runs started at or after this time: a
datetime(naive means UTC), atimedelta(that long ago, e.g.timedelta(hours=1)) or an ISO 8601 string.
Returns
Run summaries (
run(the stored row),status,duration_secondsandcounts):(nodes by status). See
Pipeline.get_runs().
Examples
>>> from datetime import timedelta
>>> failed = mrsm.get_runs(status='failed', since=timedelta(days=1))
34def get_engine_metrics(instance=None, begin=None, end=None, pipe=None, pipe_ref=None, 35 pipeline=None, limit=1000, buckets=48, visible_refs=None, 36 visible_pipelines=None, summary_only=False): 37 """Read numeric sync and Pipeline history on an instance. 38 39 The default window is the last 24 hours; retained history spans 30 days. 40 SQL totals and chart buckets cover the authorized window independently of the 41 detailed history limit (at most 5,000 records). ``summary_complete`` identifies 42 whole-window aggregates; ``history_truncated`` identifies capped recent history. 43 Other backends may return explicitly incomplete sample aggregates. Durations 44 are seconds, timestamps are UTC, and ``success_rate`` is a fraction from 0 to 1. 45 ``rows`` counts changed rows (inserted + updated + upserted + deleted), so 46 replacement syncs count both deletions and new rows; unchanged syncs count zero. 47 Local callers are trusted. Hosts must supply fresh ``visible_refs`` and 48 ``visible_pipelines`` before aggregation; empty lists see nothing. API instances 49 additionally enforce their current principal's scopes, teams and object access. 50 """ 51 from meerschaum import _core 52 instance = _instance(instance) 53 carrier, context = _context(instance) 54 if pipe is not None and pipe_ref is not None: 55 raise ValueError('Supply pipe or pipe_ref, not both.') 56 reference = pipe_ref if pipe_ref is not None else pipe 57 if reference is not None and not isinstance(reference, str): 58 reference = '|'.join((str(reference.instance_keys), str(reference.connector_keys), 59 str(reference.metric_key), str(reference.location_key))) 60 request = { 61 'instance': str(instance), 'begin': _time(begin), 'end': _time(end), 62 'pipe_ref': reference, 'pipeline': getattr(pipeline, 'name', pipeline), 63 'limit': limit, 'buckets': buckets, 64 'summary_only': bool(summary_only), 65 'visible_refs': list(visible_refs) if visible_refs is not None else None, 66 'visible_pipelines': list(visible_pipelines) if visible_pipelines is not None else None, 67 } 68 return json.loads(_core.engine_metrics(json.dumps(request), carrier, context))
Read numeric sync and Pipeline history on an instance.
The default window is the last 24 hours; retained history spans 30 days.
SQL totals and chart buckets cover the authorized window independently of the
detailed history limit (at most 5,000 records). summary_complete identifies
whole-window aggregates; history_truncated identifies capped recent history.
Other backends may return explicitly incomplete sample aggregates. Durations
are seconds, timestamps are UTC, and success_rate is a fraction from 0 to 1.
rows counts changed rows (inserted + updated + upserted + deleted), so
replacement syncs count both deletions and new rows; unchanged syncs count zero.
Local callers are trusted. Hosts must supply fresh visible_refs and
visible_pipelines before aggregation; empty lists see nothing. API instances
additionally enforce their current principal's scopes, teams and object access.
71def initialize_engine_metrics(instance=None): 72 """Create empty native metrics/run/node storage and indexes for dashboards. 73 74 API servers initialize their served instance during startup. A remote initialization 75 requires a full administrator; local access uses the instance's own credentials. 76 """ 77 from meerschaum import _core 78 instance = _instance(instance) 79 carrier, context = _context(instance) 80 _core.initialize_engine_metrics(json.dumps({'instance': str(instance)}), carrier, context)
Create empty native metrics/run/node storage and indexes for dashboards.
API servers initialize their served instance during startup. A remote initialization requires a full administrator; local access uses the instance's own credentials.
71def get_sync_statuses( 72 instance: Union[str, InstanceConnector, None] = None, 73 debug: bool = False, 74) -> Dict[str, Dict[str, Any]]: 75 """ 76 Return the latest recorded sync of every pipe on an instance. 77 78 Parameters 79 ---------- 80 instance: Union[str, InstanceConnector, None], default None 81 The instance connector or its keys. Defaults to the configured instance 82 (`meerschaum:instance`). 83 84 debug: bool, default False 85 Verbosity toggle. 86 87 Returns 88 ------- 89 A dictionary of `pipe_ref` (`'instance|connector|metric|location'`) to the pipe's status 90 dictionary (see `meerschaum.core.Pipe._status`). Pipes without a recorded sync are absent. 91 Reading from an unreachable instance raises an exception. 92 93 Examples 94 -------- 95 >>> statuses = mrsm.get_sync_statuses('sql:local') 96 >>> failing = [s for s in statuses.values() if s['consecutive_failures'] > 0] 97 """ 98 from meerschaum import _core 99 from meerschaum.config import get_config 100 if instance is None: 101 instance = get_config('meerschaum', 'instance') 102 ### A pipe only carries the instance to the binding: it is never registered or read. 103 carrier = mrsm.Pipe('mrsm', 'engine_sync_status', instance=instance, temporary=True) 104 statuses = _core.get_sync_statuses( 105 carrier, 106 _status_context(carrier, debug=debug), 107 debug=debug, 108 ) 109 return {status['pipe_ref']: status for status in statuses}
Return the latest recorded sync of every pipe on an instance.
Parameters
- instance (Union[str, InstanceConnector, None], default None):
The instance connector or its keys. Defaults to the configured instance
(
meerschaum:instance). - debug (bool, default False): Verbosity toggle.
Returns
- A dictionary of
pipe_ref('instance|connector|metric|location') to the pipe's status - dictionary (see
meerschaum.core.Pipe._status). Pipes without a recorded sync are absent. - Reading from an unreachable instance raises an exception.
Examples
>>> statuses = mrsm.get_sync_statuses('sql:local')
>>> failing = [s for s in statuses.values() if s['consecutive_failures'] > 0]
367def get_teams( 368 instance: Union[str, 'mrsm.connectors.InstanceConnector', None] = None, 369 debug: bool = False, 370) -> List[Team]: 371 """ 372 Return the teams of an instance, `Everyone` first. 373 374 Parameters 375 ---------- 376 instance: Union[str, InstanceConnector, None], default None 377 The instance (the configured instance by default). 378 379 Returns 380 ------- 381 A list of `Team` objects. 382 383 Examples 384 -------- 385 >>> [team.name for team in mrsm.get_teams('sql:local')] 386 ['Everyone', 'ops'] 387 """ 388 if instance is None: 389 from meerschaum.config import get_config 390 instance = get_config('meerschaum', 'instance', patch=True) 391 if isinstance(instance, str): 392 from meerschaum.connectors.parse import parse_instance_keys 393 instance = parse_instance_keys(instance) 394 teams = [] 395 for record in (instance.get_teams(debug=debug) or []): 396 if isinstance(record, Team): 397 teams.append(record) 398 continue 399 if not isinstance(record, dict): 400 record = {'name': str(getattr(record, 'name', record))} 401 team = Team(str(record.get('name', '')), instance=instance) 402 team._apply_record(record) 403 team._loaded = True 404 teams.append(team) 405 return teams
Return the teams of an instance, Everyone first.
Parameters
- instance (Union[str, InstanceConnector, None], default None): The instance (the configured instance by default).
Returns
- A list of
Teamobjects.
Examples
>>> [team.name for team in mrsm.get_teams('sql:local')]
['Everyone', 'ops']
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'
... )
>>>
116def get_config( 117 *keys: str, 118 patch: bool = True, 119 substitute: bool = True, 120 sync_files: bool = True, 121 write_missing: bool = True, 122 as_tuple: bool = False, 123 warn: bool = True, 124 debug: bool = False 125) -> Any: 126 """ 127 Return the Meerschaum configuration dictionary. 128 If positional arguments are provided, index by the keys. 129 Raises a warning if invalid keys are provided. 130 131 Parameters 132 ---------- 133 keys: str: 134 List of strings to index. 135 136 patch: bool, default True 137 If `True`, patch missing default keys into the config directory. 138 Defaults to `True`. 139 140 sync_files: bool, default True 141 If `True`, sync files if needed. 142 Defaults to `True`. 143 144 write_missing: bool, default True 145 If `True`, write default values when the main config files are missing. 146 Defaults to `True`. 147 148 substitute: bool, default True 149 If `True`, subsitute 'MRSM{}' values. 150 Defaults to `True`. 151 152 as_tuple: bool, default False 153 If `True`, return a tuple of type (success, value). 154 Defaults to `False`. 155 156 Returns 157 ------- 158 The value in the configuration directory, indexed by the provided keys. 159 160 Examples 161 -------- 162 >>> get_config('meerschaum', 'instance') 163 'sql:main' 164 >>> get_config('does', 'not', 'exist') 165 UserWarning: Invalid keys in config: ('does', 'not', 'exist') 166 """ 167 return _core.get_config( 168 *keys, patch=patch, substitute=substitute, sync_files=sync_files, 169 write_missing=write_missing, as_tuple=as_tuple, warn=warn, debug=debug, 170 )
Return the Meerschaum configuration dictionary. If positional arguments are provided, index by the keys. Raises a warning if invalid keys are provided.
Parameters
- keys (str:): List of strings to index.
- patch (bool, default True):
If
True, patch missing default keys into the config directory. Defaults toTrue. - sync_files (bool, default True):
If
True, sync files if needed. Defaults toTrue. - write_missing (bool, default True):
If
True, write default values when the main config files are missing. Defaults toTrue. - substitute (bool, default True):
If
True, subsitute 'MRSM{}' values. Defaults toTrue. - as_tuple (bool, default False):
If
True, return a tuple of type (success, value). Defaults toFalse.
Returns
- The value in the configuration directory, indexed by the provided keys.
Examples
>>> get_config('meerschaum', 'instance')
'sql:main'
>>> get_config('does', 'not', 'exist')
UserWarning: Invalid keys in config: ('does', 'not', 'exist')
65class Pipe: 66 """ 67 Access Meerschaum pipes via Pipe objects. 68 69 Pipes are identified by the following: 70 71 1. Connector keys (e.g. `'sql:main'`) 72 2. Metric key (e.g. `'weather'`) 73 3. Location (optional; e.g. `None`) 74 75 A pipe's connector keys correspond to a data source, and when the pipe is synced, 76 its `fetch` definition is evaluated and executed to produce new data. 77 78 Alternatively, new data may be directly synced via `pipe.sync()`: 79 80 ``` 81 >>> from meerschaum import Pipe 82 >>> pipe = Pipe('csv', 'weather') 83 >>> 84 >>> import pandas as pd 85 >>> df = pd.read_csv('weather.csv') 86 >>> pipe.sync(df) 87 ``` 88 """ 89 90 from ._fetch import ( 91 fetch, 92 get_backtrack_interval, 93 ) 94 from ._data import ( 95 get_data, 96 get_backtrack_data, 97 get_rowcount, 98 get_size, 99 get_doc, 100 get_docs, 101 get_value, 102 get_chunk_interval, 103 get_chunk_bounds, 104 get_chunk_bounds_batches, 105 parse_date_bounds, 106 ) 107 from ._register import register 108 from ._attributes import ( 109 attributes, 110 parameters, 111 columns, 112 indices, 113 indexes, 114 dtypes, 115 autoincrement, 116 autotime, 117 upsert, 118 static, 119 tzinfo, 120 enforce, 121 null_indices, 122 mixed_numerics, 123 get_columns, 124 get_columns_types, 125 get_columns_indices, 126 get_indices, 127 get_parameters, 128 get_dtypes, 129 update_parameters, 130 validate_indices, 131 tags, 132 get_id, 133 id, 134 get_val_column, 135 parents, 136 parent, 137 children, 138 child, 139 reference, 140 references, 141 target, 142 _target_legacy, 143 guess_datetime, 144 precision, 145 get_precision, 146 ) 147 from ._cache import ( 148 _get_cache_connector, 149 _cache_value, 150 _get_cached_value, 151 _invalidate_cache, 152 _get_cache_dir_path, 153 _write_cache_key, 154 _write_cache_file, 155 _write_cache_conn_key, 156 _read_cache_key, 157 _read_cache_file, 158 _read_cache_conn_key, 159 _load_cache_keys, 160 _load_cache_files, 161 _load_cache_conn_keys, 162 _get_cache_keys, 163 _get_cache_file_keys, 164 _get_cache_conn_keys, 165 _clear_cache_key, 166 _clear_cache_file, 167 _clear_cache_conn_key, 168 ) 169 from ._show import show 170 from ._edit import edit, edit_definition, update 171 from ._sync import ( 172 sync, 173 get_sync_time, 174 exists, 175 filter_existing, 176 _get_chunk_label, 177 get_num_workers, 178 ) 179 from ._verify import ( 180 verify, 181 verify_schema, 182 get_bound_interval, 183 get_bound_time, 184 ) 185 from ._delete import delete 186 from ._drop import drop, drop_indices 187 from ._compress import compress, decompress 188 from ._maintenance import vacuum, analyze, repartition 189 from ._index import create_indices 190 from ._clear import clear 191 from ._deduplicate import deduplicate 192 from ._bootstrap import bootstrap 193 from ._dtypes import enforce_dtypes, infer_dtypes 194 from ._copy import copy_to 195 from ._status import get_sync_status 196 from ._access import ( 197 access, 198 owner, 199 team, 200 visibility, 201 edit_mode, 202 locks, 203 lock, 204 unlock, 205 set_access, 206 _lock_refusal, 207 _edit_lock_refusal, 208 ) 209 210 def __init__( 211 self, 212 connector: str = '', 213 metric: str = '', 214 location: Optional[str] = None, 215 parameters: Optional[Dict[str, Any]] = None, 216 columns: Union[Dict[str, str], List[str], None] = None, 217 indices: Optional[Dict[str, Union[str, List[str]]]] = None, 218 tags: Optional[List[str]] = None, 219 target: Optional[str] = None, 220 dtypes: Optional[Dict[str, str]] = None, 221 instance: Optional[Union[str, InstanceConnector]] = None, 222 upsert: Optional[bool] = None, 223 autoincrement: Optional[bool] = None, 224 autotime: Optional[bool] = None, 225 precision: Union[str, Dict[str, Union[str, int]], None] = None, 226 static: Optional[bool] = None, 227 enforce: Optional[bool] = None, 228 null_indices: Optional[bool] = None, 229 mixed_numerics: Optional[bool] = None, 230 compress: Union[bool, Dict[str, Any], None] = None, 231 temporary: bool = False, 232 cache: Optional[bool] = None, 233 cache_connector_keys: Optional[str] = None, 234 references: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]] = None, 235 reference: Union[str, Dict[str, Any], mrsm.Pipe, None] = None, 236 parents: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]] = None, 237 parent: Union[str, Dict[str, Any], mrsm.Pipe, None] = None, 238 children: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]] = None, 239 child: Union[str, Dict[str, Any], mrsm.Pipe, None] = None, 240 mrsm_instance: Optional[Union[str, InstanceConnector]] = None, 241 connector_keys: Optional[str] = None, 242 metric_key: Optional[str] = None, 243 location_key: Optional[str] = None, 244 instance_keys: Optional[str] = None, 245 indexes: Union[Dict[str, str], List[str], None] = None, 246 hypercore: Optional[bool] = None, 247 debug: bool = False, 248 ): 249 """ 250 Parameters 251 ---------- 252 connector: str 253 Keys for the pipe's source connector, e.g. `'sql:main'`. 254 255 metric: str 256 Label for the pipe's contents, e.g. `'weather'`. 257 258 location: str, default None 259 Label for the pipe's location. Defaults to `None`. 260 261 parameters: Optional[Dict[str, Any]], default None 262 Optionally set a pipe's parameters from the constructor, 263 e.g. columns and other attributes. 264 You can edit these parameters with `edit pipes`. 265 266 columns: Union[Dict[str, str], List[str], None], default None 267 Set the `columns` dictionary of `parameters`. 268 If `parameters` is also provided, this dictionary is added under the `'columns'` key. 269 270 indices: Optional[Dict[str, Union[str, List[str]]]], default None 271 Set the `indices` dictionary of `parameters`. 272 If `parameters` is also provided, this dictionary is added under the `'indices'` key. 273 274 tags: Optional[List[str]], default None 275 A list of strings to be added under the `'tags'` key of `parameters`. 276 You can select pipes with certain tags using `--tags`. 277 278 dtypes: Optional[Dict[str, str]], default None 279 Set the `dtypes` dictionary of `parameters`. 280 If `parameters` is also provided, this dictionary is added under the `'dtypes'` key. 281 282 mrsm_instance: Optional[Union[str, InstanceConnector]], default None 283 Connector for the Meerschaum instance where the pipe resides. 284 Defaults to the preconfigured default instance (`'sql:main'`). 285 286 instance: Optional[Union[str, InstanceConnector]], default None 287 Alias for `mrsm_instance`. If `mrsm_instance` is supplied, this value is ignored. 288 289 upsert: Optional[bool], default None 290 If `True`, set `upsert` to `True` in the parameters. 291 292 autoincrement: Optional[bool], default None 293 If `True`, set `autoincrement` in the parameters. 294 295 autotime: Optional[bool], default None 296 If `True`, set `autotime` in the parameters. 297 298 precision: Union[str, Dict[str, Union[str, int]], None], default None 299 If provided, set `precision` in the parameters. 300 This may be either a string (the precision unit) or a dictionary of in the form 301 `{'unit': <unit>, 'interval': <interval>}`. 302 Default is determined by the `datetime` column dtype 303 (e.g. `datetime64[us]` is `microsecond` precision). 304 305 static: Optional[bool], default None 306 If `True`, set `static` in the parameters. 307 308 enforce: Optional[bool], default None 309 If `False`, skip data type enforcement. 310 Default behavior is `True`. 311 312 null_indices: Optional[bool], default None 313 Set to `False` if there will be no null values in the index columns. 314 Defaults to `True`. 315 316 mixed_numerics: bool, default None 317 If `True`, integer columns will be converted to `numeric` when floats are synced. 318 Set to `False` to disable this behavior. 319 Defaults to `True`. 320 321 compress: Union[bool, Dict[str, Any], None], default None 322 If `True` (or a dictionary of compression settings), mark the pipe for compression. 323 For TimescaleDB hypertables, a columnstore (compression) policy is installed 324 automatically on sync. A dictionary may override `segmentby`, `orderby`, and `after`. 325 Defaults to `False`. 326 327 hypercore: Optional[bool], default None 328 Set `hypercore` in the parameters. For TimescaleDB hypertables, the Hypercore 329 columnstore is enabled at table creation by default (declaring 330 `segmentby`/`orderby` in `CREATE TABLE`), which causes TimescaleDB to auto-create a 331 columnstore policy. Set to `False` for a plain row-store hypertable. 332 Has no effect unless the pipe is a hypertable (`hypertable`, default `True`). 333 334 temporary: bool, default False 335 If `True`, prevent instance tables (pipes, users, plugins) from being created. 336 337 cache: Optional[bool], default None 338 If `True`, cache the pipe's metadata to disk (in addition to in-memory caching). 339 If `cache` is not explicitly `True`, it is set to `False` if `temporary` is `True`. 340 Defaults to `True` (from `None`). 341 342 cache_connector_keys: Optional[str], default None 343 If provided, use the keys to a Valkey connector (e.g. `valkey:main`). 344 345 references: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None 346 If provided, inherit the parameters of the reference Pipe(s). 347 May be equal to a string of the Pipe constructor, a dictionary of constructor keys, 348 a Pipe itself, or a list of any of these values. 349 350 parents: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None 351 Set references for parent pipes. See `references` for values. 352 353 children: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None 354 Set references for child pipes. See `references` for values. 355 356 """ 357 from meerschaum.utils.warnings import error, warn 358 if (not connector and not connector_keys) or (not metric and not metric_key): 359 error( 360 "Please provide strings for the connector and metric\n " 361 + "(first two positional arguments)." 362 ) 363 364 ### Fall back to legacy `location_key` just in case. 365 if not location: 366 location = location_key 367 368 if not connector: 369 connector = connector_keys 370 371 if not metric: 372 metric = metric_key 373 374 if location in ('[None]', 'None'): 375 location = None 376 377 from meerschaum._internal.static import STATIC_CONFIG 378 negation_prefix = STATIC_CONFIG['system']['fetch_pipes_keys']['negation_prefix'] 379 for k in (connector, metric, location, *(tags or [])): 380 if str(k).startswith(negation_prefix): 381 error(f"A pipe's keys and tags cannot start with the prefix '{negation_prefix}'.") 382 383 self._connector_keys = str(connector) 384 self._connector_key = self._connector_keys ### Alias 385 self._metric_key = metric 386 self._location_key = location 387 self.temporary = temporary 388 self.cache = ( 389 cache 390 if cache is not None 391 else ((not temporary) and get_config('pipes', 'cache', 'enabled', warn=False)) 392 ) 393 self.cache_connector_keys = ( 394 str(cache_connector_keys) 395 if cache_connector_keys is not None 396 else None 397 ) 398 self.debug = debug 399 400 _mrsm_instance = mrsm_instance if mrsm_instance is not None else (instance or instance_keys) 401 if _mrsm_instance is None: 402 _mrsm_instance = get_config('meerschaum', 'instance', patch=True) 403 404 if not isinstance(_mrsm_instance, str): 405 self._instance_connector = _mrsm_instance 406 self._instance_keys = str(_mrsm_instance) 407 else: 408 self._instance_keys = _mrsm_instance 409 410 if self._instance_keys == 'sql:memory': 411 self.cache = False 412 413 from meerschaum import _core 414 from ._live_parameters import Attrs, new_raw_root 415 ### Keys and raw parameters live in a `_core.Pipe`; `_raw_root` is a read-through 416 ### view of its raw map. Constructing never touches the instance. 417 self._core_pipe = _core.Pipe( 418 self._connector_keys, 419 self._metric_key, 420 self._location_key, 421 self._instance_keys, 422 None, 423 ) 424 self._raw_root = new_raw_root(self._core_pipe) 425 self._attributes = Attrs( 426 self, 427 { 428 'connector_keys': self._connector_keys, 429 'metric_key': self._metric_key, 430 'location_key': self._location_key, 431 'parameters': {}, 432 }, 433 self._raw_root, 434 ) 435 436 ### only set parameters if values are provided 437 if isinstance(parameters, dict): 438 self._attributes['parameters'] = parameters 439 else: 440 if parameters is not None: 441 warn(f"The provided parameters are of invalid type '{type(parameters)}'.") 442 self._attributes['parameters'] = {} 443 444 columns = columns or self._attributes.get('parameters', {}).get('columns', None) 445 if isinstance(columns, (list, tuple)): 446 columns = {str(col): str(col) for col in columns} 447 if isinstance(columns, dict): 448 self._attributes['parameters']['columns'] = columns 449 elif isinstance(columns, str) and 'Pipe(' in columns: 450 pass 451 elif columns is not None: 452 warn(f"The provided columns are of invalid type '{type(columns)}'.") 453 454 indices = ( 455 indices 456 or indexes 457 or self._attributes.get('parameters', {}).get('indices', None) 458 or self._attributes.get('parameters', {}).get('indexes', None) 459 ) 460 if isinstance(indices, dict): 461 indices_key = ( 462 'indexes' 463 if 'indexes' in self._attributes['parameters'] 464 else 'indices' 465 ) 466 self._attributes['parameters'][indices_key] = indices 467 468 if isinstance(tags, (list, tuple)): 469 self._attributes['parameters']['tags'] = tags 470 elif tags is not None: 471 warn(f"The provided tags are of invalid type '{type(tags)}'.") 472 473 if isinstance(target, str): 474 self._attributes['parameters']['target'] = target 475 elif target is not None: 476 warn(f"The provided target is of invalid type '{type(target)}'.") 477 478 if isinstance(dtypes, dict): 479 self._attributes['parameters']['dtypes'] = dtypes 480 elif dtypes is not None: 481 warn(f"The provided dtypes are of invalid type '{type(dtypes)}'.") 482 483 if isinstance(upsert, bool): 484 self._attributes['parameters']['upsert'] = upsert 485 486 if isinstance(autoincrement, bool): 487 self._attributes['parameters']['autoincrement'] = autoincrement 488 489 if isinstance(autotime, bool): 490 self._attributes['parameters']['autotime'] = autotime 491 492 if isinstance(precision, dict): 493 self._attributes['parameters']['precision'] = precision 494 elif isinstance(precision, str): 495 self._attributes['parameters']['precision'] = {'unit': precision} 496 497 if isinstance(static, bool): 498 self._attributes['parameters']['static'] = static 499 self._static = static 500 501 if isinstance(enforce, bool): 502 self._attributes['parameters']['enforce'] = enforce 503 504 if isinstance(null_indices, bool): 505 self._attributes['parameters']['null_indices'] = null_indices 506 507 if isinstance(mixed_numerics, bool): 508 self._attributes['parameters']['mixed_numerics'] = mixed_numerics 509 510 if isinstance(compress, (bool, dict)): 511 self._attributes['parameters']['compress'] = compress 512 513 if isinstance(hypercore, bool): 514 self._attributes['parameters']['hypercore'] = hypercore 515 516 self._cache_locks = collections.defaultdict(lambda: threading.RLock()) 517 self._attributes_lock = threading.RLock() 518 519 if references is not None or reference is not None: 520 reference_vals = references if references is not None else reference 521 self.references = reference_vals 522 523 if parents is not None or parent is not None: 524 parent_vals = parents if parents is not None else parent 525 self.parents = parent_vals 526 527 if children is not None or child is not None: 528 children_vals = children if children is not None else child 529 self.children = children_vals 530 531 @property 532 def metric_key(self) -> str: 533 """ 534 Return the pipe's metric key. 535 """ 536 return self._core_pipe.metric_key 537 538 @property 539 def metric(self) -> str: 540 """ 541 Return the pipe's metric key. 542 """ 543 return self.metric_key 544 545 @property 546 def location_key(self) -> Union[str, None]: 547 """ 548 Return the pipe's location key. 549 """ 550 return self._core_pipe.location_key 551 552 @property 553 def location(self) -> Union[str, None]: 554 """ 555 Return the pipe's location key. 556 """ 557 return self.location_key 558 559 @property 560 def meta(self): 561 """ 562 Return the four keys needed to reconstruct this pipe. 563 """ 564 return { 565 'connector_keys': self.connector_keys, 566 'metric_key': self.metric_key, 567 'location_key': self.location_key, 568 'instance_keys': self.instance_keys, 569 } 570 571 def keys(self) -> List[str]: 572 """ 573 Return the ordered keys for this pipe. 574 """ 575 return { 576 key: val 577 for key, val in self.meta.items() 578 if key != 'instance' 579 } 580 581 @property 582 def instance_keys(self) -> str: 583 """ 584 Return the pipe's instance keys. 585 """ 586 return self._core_pipe.instance_keys 587 588 @property 589 def instance(self) -> Union[InstanceConnector, str]: 590 """ 591 Return the pipe's instance connector or keys. 592 """ 593 conn = self.instance_connector 594 if conn is None: 595 return self.instance_keys 596 return conn 597 598 @property 599 def instance_connector(self) -> Union[InstanceConnector, None]: 600 """ 601 The instance connector on which this pipe resides. 602 """ 603 if '_instance_connector' not in self.__dict__: 604 from meerschaum.connectors.parse import parse_instance_keys 605 conn = parse_instance_keys(self.instance_keys) 606 if conn: 607 self._instance_connector = conn 608 else: 609 return None 610 return self._instance_connector 611 612 @property 613 def connector_keys(self) -> str: 614 """ 615 Return the pipe's connector keys. 616 """ 617 return self._core_pipe.connector_keys 618 619 @property 620 def connector_key(self) -> str: 621 """ 622 Legacy: use `Pipe.connector_keys` instead. 623 """ 624 return self.connector_keys 625 626 @property 627 def connector(self) -> Union['Connector', str]: 628 """ 629 The connector to the data source. 630 """ 631 if '_connector' not in self.__dict__: 632 from meerschaum.connectors.parse import parse_instance_keys 633 import warnings 634 with warnings.catch_warnings(): 635 warnings.simplefilter('ignore') 636 try: 637 conn = parse_instance_keys(self.connector_keys) 638 except Exception: 639 conn = None 640 if conn: 641 self._connector = conn 642 else: 643 return self._connector_keys 644 return self._connector 645 646 def __str__(self, ansi: bool=False): 647 if not ansi: 648 return self._core_pipe.__str__() 649 return pipe_repr(self, ansi=ansi) 650 651 def __eq__(self, other): 652 ### Duck typed: only another façade pipe backed by `_core.Pipe` compares in Rust. 653 other_core = getattr(other, '__dict__', {}).get('_core_pipe', None) 654 if other_core is not None: 655 return isinstance(self, type(other)) and self._core_pipe == other_core 656 try: 657 return ( 658 isinstance(self, type(other)) 659 and self.connector_keys == other.connector_keys 660 and self.metric_key == other.metric_key 661 and self.location_key == other.location_key 662 and self.instance_keys == other.instance_keys 663 ) 664 except Exception: 665 return False 666 667 def __hash__(self): 668 return hash(self._core_pipe) 669 670 def __repr__(self, ansi: bool=True, **kw) -> str: 671 if not hasattr(sys, 'ps1'): 672 ansi = False 673 674 if not ansi and not kw: 675 return self._core_pipe.__str__() 676 return pipe_repr(self, ansi=ansi, **kw) 677 678 def __pt_repr__(self): 679 from meerschaum.utils.packages import attempt_import 680 prompt_toolkit_formatted_text = attempt_import('prompt_toolkit.formatted_text', lazy=False) 681 return prompt_toolkit_formatted_text.ANSI(pipe_repr(self, ansi=True)) 682 683 def __getstate__(self) -> Dict[str, Any]: 684 """ 685 Define the state dictionary (pickling). 686 """ 687 return { 688 'connector_keys': self.connector_keys, 689 'metric_key': self.metric_key, 690 'location_key': self.location_key, 691 'parameters': self._attributes.get('parameters', None), 692 'instance_keys': self.instance_keys, 693 } 694 695 def __setstate__(self, _state: Dict[str, Any]): 696 """ 697 Read the state (unpickling). 698 """ 699 self.__init__(**_state) 700 701 def __getitem__(self, key: str) -> Any: 702 """ 703 Index the pipe's attributes. 704 If the `key` cannot be found`, return `None`. 705 """ 706 if key in self.attributes: 707 return self.attributes.get(key, None) 708 709 aliases = { 710 'connector': 'connector_keys', 711 'connector_key': 'connector_keys', 712 'metric': 'metric_key', 713 'location': 'location_key', 714 } 715 aliased_key = aliases.get(key, None) 716 if aliased_key is not None: 717 return self.attributes.get(aliased_key, None) 718 719 property_aliases = { 720 'instance': 'instance_keys', 721 'instance_key': 'instance_keys', 722 } 723 aliased_key = property_aliases.get(key, None) 724 if aliased_key is not None: 725 key = aliased_key 726 return getattr(self, key, None) 727 728 def __copy__(self): 729 """ 730 Return a shallow copy of the current pipe. 731 """ 732 return mrsm.Pipe( 733 self.connector_keys, self.metric_key, self.location_key, 734 instance=self.instance_keys, 735 parameters=self._attributes.get('parameters', None), 736 ) 737 738 def __deepcopy__(self, memo): 739 """ 740 Return a deep copy of the current pipe. 741 """ 742 return self.__copy__()
Access Meerschaum pipes via Pipe objects.
Pipes are identified by the following:
- Connector keys (e.g.
'sql:main') - Metric key (e.g.
'weather') - Location (optional; e.g.
None)
A pipe's connector keys correspond to a data source, and when the pipe is synced,
its fetch definition is evaluated and executed to produce new data.
Alternatively, new data may be directly synced via pipe.sync():
>>> from meerschaum import Pipe
>>> pipe = Pipe('csv', 'weather')
>>>
>>> import pandas as pd
>>> df = pd.read_csv('weather.csv')
>>> pipe.sync(df)
210 def __init__( 211 self, 212 connector: str = '', 213 metric: str = '', 214 location: Optional[str] = None, 215 parameters: Optional[Dict[str, Any]] = None, 216 columns: Union[Dict[str, str], List[str], None] = None, 217 indices: Optional[Dict[str, Union[str, List[str]]]] = None, 218 tags: Optional[List[str]] = None, 219 target: Optional[str] = None, 220 dtypes: Optional[Dict[str, str]] = None, 221 instance: Optional[Union[str, InstanceConnector]] = None, 222 upsert: Optional[bool] = None, 223 autoincrement: Optional[bool] = None, 224 autotime: Optional[bool] = None, 225 precision: Union[str, Dict[str, Union[str, int]], None] = None, 226 static: Optional[bool] = None, 227 enforce: Optional[bool] = None, 228 null_indices: Optional[bool] = None, 229 mixed_numerics: Optional[bool] = None, 230 compress: Union[bool, Dict[str, Any], None] = None, 231 temporary: bool = False, 232 cache: Optional[bool] = None, 233 cache_connector_keys: Optional[str] = None, 234 references: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]] = None, 235 reference: Union[str, Dict[str, Any], mrsm.Pipe, None] = None, 236 parents: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]] = None, 237 parent: Union[str, Dict[str, Any], mrsm.Pipe, None] = None, 238 children: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]] = None, 239 child: Union[str, Dict[str, Any], mrsm.Pipe, None] = None, 240 mrsm_instance: Optional[Union[str, InstanceConnector]] = None, 241 connector_keys: Optional[str] = None, 242 metric_key: Optional[str] = None, 243 location_key: Optional[str] = None, 244 instance_keys: Optional[str] = None, 245 indexes: Union[Dict[str, str], List[str], None] = None, 246 hypercore: Optional[bool] = None, 247 debug: bool = False, 248 ): 249 """ 250 Parameters 251 ---------- 252 connector: str 253 Keys for the pipe's source connector, e.g. `'sql:main'`. 254 255 metric: str 256 Label for the pipe's contents, e.g. `'weather'`. 257 258 location: str, default None 259 Label for the pipe's location. Defaults to `None`. 260 261 parameters: Optional[Dict[str, Any]], default None 262 Optionally set a pipe's parameters from the constructor, 263 e.g. columns and other attributes. 264 You can edit these parameters with `edit pipes`. 265 266 columns: Union[Dict[str, str], List[str], None], default None 267 Set the `columns` dictionary of `parameters`. 268 If `parameters` is also provided, this dictionary is added under the `'columns'` key. 269 270 indices: Optional[Dict[str, Union[str, List[str]]]], default None 271 Set the `indices` dictionary of `parameters`. 272 If `parameters` is also provided, this dictionary is added under the `'indices'` key. 273 274 tags: Optional[List[str]], default None 275 A list of strings to be added under the `'tags'` key of `parameters`. 276 You can select pipes with certain tags using `--tags`. 277 278 dtypes: Optional[Dict[str, str]], default None 279 Set the `dtypes` dictionary of `parameters`. 280 If `parameters` is also provided, this dictionary is added under the `'dtypes'` key. 281 282 mrsm_instance: Optional[Union[str, InstanceConnector]], default None 283 Connector for the Meerschaum instance where the pipe resides. 284 Defaults to the preconfigured default instance (`'sql:main'`). 285 286 instance: Optional[Union[str, InstanceConnector]], default None 287 Alias for `mrsm_instance`. If `mrsm_instance` is supplied, this value is ignored. 288 289 upsert: Optional[bool], default None 290 If `True`, set `upsert` to `True` in the parameters. 291 292 autoincrement: Optional[bool], default None 293 If `True`, set `autoincrement` in the parameters. 294 295 autotime: Optional[bool], default None 296 If `True`, set `autotime` in the parameters. 297 298 precision: Union[str, Dict[str, Union[str, int]], None], default None 299 If provided, set `precision` in the parameters. 300 This may be either a string (the precision unit) or a dictionary of in the form 301 `{'unit': <unit>, 'interval': <interval>}`. 302 Default is determined by the `datetime` column dtype 303 (e.g. `datetime64[us]` is `microsecond` precision). 304 305 static: Optional[bool], default None 306 If `True`, set `static` in the parameters. 307 308 enforce: Optional[bool], default None 309 If `False`, skip data type enforcement. 310 Default behavior is `True`. 311 312 null_indices: Optional[bool], default None 313 Set to `False` if there will be no null values in the index columns. 314 Defaults to `True`. 315 316 mixed_numerics: bool, default None 317 If `True`, integer columns will be converted to `numeric` when floats are synced. 318 Set to `False` to disable this behavior. 319 Defaults to `True`. 320 321 compress: Union[bool, Dict[str, Any], None], default None 322 If `True` (or a dictionary of compression settings), mark the pipe for compression. 323 For TimescaleDB hypertables, a columnstore (compression) policy is installed 324 automatically on sync. A dictionary may override `segmentby`, `orderby`, and `after`. 325 Defaults to `False`. 326 327 hypercore: Optional[bool], default None 328 Set `hypercore` in the parameters. For TimescaleDB hypertables, the Hypercore 329 columnstore is enabled at table creation by default (declaring 330 `segmentby`/`orderby` in `CREATE TABLE`), which causes TimescaleDB to auto-create a 331 columnstore policy. Set to `False` for a plain row-store hypertable. 332 Has no effect unless the pipe is a hypertable (`hypertable`, default `True`). 333 334 temporary: bool, default False 335 If `True`, prevent instance tables (pipes, users, plugins) from being created. 336 337 cache: Optional[bool], default None 338 If `True`, cache the pipe's metadata to disk (in addition to in-memory caching). 339 If `cache` is not explicitly `True`, it is set to `False` if `temporary` is `True`. 340 Defaults to `True` (from `None`). 341 342 cache_connector_keys: Optional[str], default None 343 If provided, use the keys to a Valkey connector (e.g. `valkey:main`). 344 345 references: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None 346 If provided, inherit the parameters of the reference Pipe(s). 347 May be equal to a string of the Pipe constructor, a dictionary of constructor keys, 348 a Pipe itself, or a list of any of these values. 349 350 parents: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None 351 Set references for parent pipes. See `references` for values. 352 353 children: Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None 354 Set references for child pipes. See `references` for values. 355 356 """ 357 from meerschaum.utils.warnings import error, warn 358 if (not connector and not connector_keys) or (not metric and not metric_key): 359 error( 360 "Please provide strings for the connector and metric\n " 361 + "(first two positional arguments)." 362 ) 363 364 ### Fall back to legacy `location_key` just in case. 365 if not location: 366 location = location_key 367 368 if not connector: 369 connector = connector_keys 370 371 if not metric: 372 metric = metric_key 373 374 if location in ('[None]', 'None'): 375 location = None 376 377 from meerschaum._internal.static import STATIC_CONFIG 378 negation_prefix = STATIC_CONFIG['system']['fetch_pipes_keys']['negation_prefix'] 379 for k in (connector, metric, location, *(tags or [])): 380 if str(k).startswith(negation_prefix): 381 error(f"A pipe's keys and tags cannot start with the prefix '{negation_prefix}'.") 382 383 self._connector_keys = str(connector) 384 self._connector_key = self._connector_keys ### Alias 385 self._metric_key = metric 386 self._location_key = location 387 self.temporary = temporary 388 self.cache = ( 389 cache 390 if cache is not None 391 else ((not temporary) and get_config('pipes', 'cache', 'enabled', warn=False)) 392 ) 393 self.cache_connector_keys = ( 394 str(cache_connector_keys) 395 if cache_connector_keys is not None 396 else None 397 ) 398 self.debug = debug 399 400 _mrsm_instance = mrsm_instance if mrsm_instance is not None else (instance or instance_keys) 401 if _mrsm_instance is None: 402 _mrsm_instance = get_config('meerschaum', 'instance', patch=True) 403 404 if not isinstance(_mrsm_instance, str): 405 self._instance_connector = _mrsm_instance 406 self._instance_keys = str(_mrsm_instance) 407 else: 408 self._instance_keys = _mrsm_instance 409 410 if self._instance_keys == 'sql:memory': 411 self.cache = False 412 413 from meerschaum import _core 414 from ._live_parameters import Attrs, new_raw_root 415 ### Keys and raw parameters live in a `_core.Pipe`; `_raw_root` is a read-through 416 ### view of its raw map. Constructing never touches the instance. 417 self._core_pipe = _core.Pipe( 418 self._connector_keys, 419 self._metric_key, 420 self._location_key, 421 self._instance_keys, 422 None, 423 ) 424 self._raw_root = new_raw_root(self._core_pipe) 425 self._attributes = Attrs( 426 self, 427 { 428 'connector_keys': self._connector_keys, 429 'metric_key': self._metric_key, 430 'location_key': self._location_key, 431 'parameters': {}, 432 }, 433 self._raw_root, 434 ) 435 436 ### only set parameters if values are provided 437 if isinstance(parameters, dict): 438 self._attributes['parameters'] = parameters 439 else: 440 if parameters is not None: 441 warn(f"The provided parameters are of invalid type '{type(parameters)}'.") 442 self._attributes['parameters'] = {} 443 444 columns = columns or self._attributes.get('parameters', {}).get('columns', None) 445 if isinstance(columns, (list, tuple)): 446 columns = {str(col): str(col) for col in columns} 447 if isinstance(columns, dict): 448 self._attributes['parameters']['columns'] = columns 449 elif isinstance(columns, str) and 'Pipe(' in columns: 450 pass 451 elif columns is not None: 452 warn(f"The provided columns are of invalid type '{type(columns)}'.") 453 454 indices = ( 455 indices 456 or indexes 457 or self._attributes.get('parameters', {}).get('indices', None) 458 or self._attributes.get('parameters', {}).get('indexes', None) 459 ) 460 if isinstance(indices, dict): 461 indices_key = ( 462 'indexes' 463 if 'indexes' in self._attributes['parameters'] 464 else 'indices' 465 ) 466 self._attributes['parameters'][indices_key] = indices 467 468 if isinstance(tags, (list, tuple)): 469 self._attributes['parameters']['tags'] = tags 470 elif tags is not None: 471 warn(f"The provided tags are of invalid type '{type(tags)}'.") 472 473 if isinstance(target, str): 474 self._attributes['parameters']['target'] = target 475 elif target is not None: 476 warn(f"The provided target is of invalid type '{type(target)}'.") 477 478 if isinstance(dtypes, dict): 479 self._attributes['parameters']['dtypes'] = dtypes 480 elif dtypes is not None: 481 warn(f"The provided dtypes are of invalid type '{type(dtypes)}'.") 482 483 if isinstance(upsert, bool): 484 self._attributes['parameters']['upsert'] = upsert 485 486 if isinstance(autoincrement, bool): 487 self._attributes['parameters']['autoincrement'] = autoincrement 488 489 if isinstance(autotime, bool): 490 self._attributes['parameters']['autotime'] = autotime 491 492 if isinstance(precision, dict): 493 self._attributes['parameters']['precision'] = precision 494 elif isinstance(precision, str): 495 self._attributes['parameters']['precision'] = {'unit': precision} 496 497 if isinstance(static, bool): 498 self._attributes['parameters']['static'] = static 499 self._static = static 500 501 if isinstance(enforce, bool): 502 self._attributes['parameters']['enforce'] = enforce 503 504 if isinstance(null_indices, bool): 505 self._attributes['parameters']['null_indices'] = null_indices 506 507 if isinstance(mixed_numerics, bool): 508 self._attributes['parameters']['mixed_numerics'] = mixed_numerics 509 510 if isinstance(compress, (bool, dict)): 511 self._attributes['parameters']['compress'] = compress 512 513 if isinstance(hypercore, bool): 514 self._attributes['parameters']['hypercore'] = hypercore 515 516 self._cache_locks = collections.defaultdict(lambda: threading.RLock()) 517 self._attributes_lock = threading.RLock() 518 519 if references is not None or reference is not None: 520 reference_vals = references if references is not None else reference 521 self.references = reference_vals 522 523 if parents is not None or parent is not None: 524 parent_vals = parents if parents is not None else parent 525 self.parents = parent_vals 526 527 if children is not None or child is not None: 528 children_vals = children if children is not None else child 529 self.children = children_vals
Parameters
- connector (str):
Keys for the pipe's source connector, e.g.
'sql:main'. - metric (str):
Label for the pipe's contents, e.g.
'weather'. - location (str, default None):
Label for the pipe's location. Defaults to
None. - parameters (Optional[Dict[str, Any]], default None):
Optionally set a pipe's parameters from the constructor,
e.g. columns and other attributes.
You can edit these parameters with
edit pipes. - columns (Union[Dict[str, str], List[str], None], default None):
Set the
columnsdictionary ofparameters. Ifparametersis also provided, this dictionary is added under the'columns'key. - indices (Optional[Dict[str, Union[str, List[str]]]], default None):
Set the
indicesdictionary ofparameters. Ifparametersis also provided, this dictionary is added under the'indices'key. - tags (Optional[List[str]], default None):
A list of strings to be added under the
'tags'key ofparameters. You can select pipes with certain tags using--tags. - dtypes (Optional[Dict[str, str]], default None):
Set the
dtypesdictionary ofparameters. Ifparametersis also provided, this dictionary is added under the'dtypes'key. - mrsm_instance (Optional[Union[str, InstanceConnector]], default None):
Connector for the Meerschaum instance where the pipe resides.
Defaults to the preconfigured default instance (
'sql:main'). - instance (Optional[Union[str, InstanceConnector]], default None):
Alias for
mrsm_instance. Ifmrsm_instanceis supplied, this value is ignored. - upsert (Optional[bool], default None):
If
True, setupserttoTruein the parameters. - autoincrement (Optional[bool], default None):
If
True, setautoincrementin the parameters. - autotime (Optional[bool], default None):
If
True, setautotimein the parameters. - precision (Union[str, Dict[str, Union[str, int]], None], default None):
If provided, set
precisionin the parameters. This may be either a string (the precision unit) or a dictionary of in the form{'unit': <unit>, 'interval': <interval>}. Default is determined by thedatetimecolumn dtype (e.g.datetime64[us]ismicrosecondprecision). - static (Optional[bool], default None):
If
True, setstaticin the parameters. - enforce (Optional[bool], default None):
If
False, skip data type enforcement. Default behavior isTrue. - null_indices (Optional[bool], default None):
Set to
Falseif there will be no null values in the index columns. Defaults toTrue. - mixed_numerics (bool, default None):
If
True, integer columns will be converted tonumericwhen floats are synced. Set toFalseto disable this behavior. Defaults toTrue. - compress (Union[bool, Dict[str, Any], None], default None):
If
True(or a dictionary of compression settings), mark the pipe for compression. For TimescaleDB hypertables, a columnstore (compression) policy is installed automatically on sync. A dictionary may overridesegmentby,orderby, andafter. Defaults toFalse. - hypercore (Optional[bool], default None):
Set
hypercorein the parameters. For TimescaleDB hypertables, the Hypercore columnstore is enabled at table creation by default (declaringsegmentby/orderbyinCREATE TABLE), which causes TimescaleDB to auto-create a columnstore policy. Set toFalsefor a plain row-store hypertable. Has no effect unless the pipe is a hypertable (hypertable, defaultTrue). - temporary (bool, default False):
If
True, prevent instance tables (pipes, users, plugins) from being created. - cache (Optional[bool], default None):
If
True, cache the pipe's metadata to disk (in addition to in-memory caching). Ifcacheis not explicitlyTrue, it is set toFalseiftemporaryisTrue. Defaults toTrue(fromNone). - cache_connector_keys (Optional[str], default None):
If provided, use the keys to a Valkey connector (e.g.
valkey:main). - references (Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None): If provided, inherit the parameters of the reference Pipe(s). May be equal to a string of the Pipe constructor, a dictionary of constructor keys, a Pipe itself, or a list of any of these values.
- parents (Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None):
Set references for parent pipes. See
referencesfor values. - children (Optional[List[Union[str, Dict[str, Any], mrsm.Pipe, None]]], default None):
Set references for child pipes. See
referencesfor values.
531 @property 532 def metric_key(self) -> str: 533 """ 534 Return the pipe's metric key. 535 """ 536 return self._core_pipe.metric_key
Return the pipe's metric key.
538 @property 539 def metric(self) -> str: 540 """ 541 Return the pipe's metric key. 542 """ 543 return self.metric_key
Return the pipe's metric key.
545 @property 546 def location_key(self) -> Union[str, None]: 547 """ 548 Return the pipe's location key. 549 """ 550 return self._core_pipe.location_key
Return the pipe's location key.
552 @property 553 def location(self) -> Union[str, None]: 554 """ 555 Return the pipe's location key. 556 """ 557 return self.location_key
Return the pipe's location key.
559 @property 560 def meta(self): 561 """ 562 Return the four keys needed to reconstruct this pipe. 563 """ 564 return { 565 'connector_keys': self.connector_keys, 566 'metric_key': self.metric_key, 567 'location_key': self.location_key, 568 'instance_keys': self.instance_keys, 569 }
Return the four keys needed to reconstruct this pipe.
571 def keys(self) -> List[str]: 572 """ 573 Return the ordered keys for this pipe. 574 """ 575 return { 576 key: val 577 for key, val in self.meta.items() 578 if key != 'instance' 579 }
Return the ordered keys for this pipe.
581 @property 582 def instance_keys(self) -> str: 583 """ 584 Return the pipe's instance keys. 585 """ 586 return self._core_pipe.instance_keys
Return the pipe's instance keys.
588 @property 589 def instance(self) -> Union[InstanceConnector, str]: 590 """ 591 Return the pipe's instance connector or keys. 592 """ 593 conn = self.instance_connector 594 if conn is None: 595 return self.instance_keys 596 return conn
Return the pipe's instance connector or keys.
598 @property 599 def instance_connector(self) -> Union[InstanceConnector, None]: 600 """ 601 The instance connector on which this pipe resides. 602 """ 603 if '_instance_connector' not in self.__dict__: 604 from meerschaum.connectors.parse import parse_instance_keys 605 conn = parse_instance_keys(self.instance_keys) 606 if conn: 607 self._instance_connector = conn 608 else: 609 return None 610 return self._instance_connector
The instance connector on which this pipe resides.
612 @property 613 def connector_keys(self) -> str: 614 """ 615 Return the pipe's connector keys. 616 """ 617 return self._core_pipe.connector_keys
Return the pipe's connector keys.
619 @property 620 def connector_key(self) -> str: 621 """ 622 Legacy: use `Pipe.connector_keys` instead. 623 """ 624 return self.connector_keys
Legacy: use Pipe.connector_keys instead.
626 @property 627 def connector(self) -> Union['Connector', str]: 628 """ 629 The connector to the data source. 630 """ 631 if '_connector' not in self.__dict__: 632 from meerschaum.connectors.parse import parse_instance_keys 633 import warnings 634 with warnings.catch_warnings(): 635 warnings.simplefilter('ignore') 636 try: 637 conn = parse_instance_keys(self.connector_keys) 638 except Exception: 639 conn = None 640 if conn: 641 self._connector = conn 642 else: 643 return self._connector_keys 644 return self._connector
The connector to the data source.
21def fetch( 22 self, 23 begin: Union[datetime, int, str, None] = '', 24 end: Union[datetime, int, None] = None, 25 check_existing: bool = True, 26 sync_chunks: bool = False, 27 debug: bool = False, 28 as_polars: Optional[bool] = None, 29 as_pandas: Optional[bool] = None, 30 **kw: Any 31) -> Union['pl.DataFrame', 'pd.DataFrame', Iterator[Any], None]: 32 """ 33 Fetch a Pipe's latest data from its connector. 34 35 Parameters 36 ---------- 37 begin: Union[datetime, str, None], default '': 38 If provided, only fetch data newer than or equal to `begin`. 39 40 end: Optional[datetime], default None: 41 If provided, only fetch data older than or equal to `end`. 42 43 check_existing: bool, default True 44 If `False`, do not apply the backtrack interval. 45 46 sync_chunks: bool, default False 47 If `True` and the pipe's connector is of type `'sql'`, begin syncing chunks while fetching 48 loads chunks into memory. 49 50 debug: bool, default False 51 Verbosity toggle. 52 53 as_polars: Optional[bool], default None 54 `True` returns Polars frames and `False` pandas ones, whatever `meerschaum:dataframe` 55 says (see `as_pandas`). 56 57 as_pandas: Optional[bool], default None 58 `True` returns pandas frames and `False` Polars ones. By default (`None`) the frames are 59 the library of the `meerschaum:dataframe` setting, whatever the source returned. 60 A `chunk_hook` is given chunks of the same library. 61 62 Returns 63 ------- 64 A DataFrame of the newest unseen data (Polars or pandas, per `meerschaum:dataframe`), or a 65 generator of chunks for a SQL source. 66 67 """ 68 from meerschaum import _core 69 from meerschaum.core.Pipe._data import _rust_context, _wants_polars 70 polars = _wants_polars(False, as_polars, as_pandas) 71 try: 72 context = _rust_context(self, debug=debug) 73 except Exception as e: 74 ### v4 checked the source before it touched the instance: a source without `fetch()` 75 ### answers `None` with a warning even when the instance cannot be parsed. 76 if type(e) is Exception and 'fetch' not in dir(self.connector): 77 from meerschaum.utils.warnings import warn 78 warn(f"No `fetch()` function defined for connector '{self.connector}'") 79 return None 80 raise 81 begin, end = self.parse_date_bounds(begin, end) 82 fetched = _core.pipe_fetch( 83 self, 84 context, 85 begin=begin, 86 end=end, 87 check_existing=check_existing, 88 params=kw.get('params', None), 89 kwargs={ 90 key: val 91 for key, val in kw.items() 92 if key not in ('params', 'chunk_hook') 93 }, 94 as_polars=polars, 95 sync_chunks=sync_chunks, 96 chunk_hook=kw.get('chunk_hook', None), 97 debug=debug, 98 ) 99 ### A SQL source's rows come in chunks, as a generator. 100 return (chunk for chunk in fetched) if hasattr(fetched, '__next__') else fetched
Fetch a Pipe's latest data from its connector.
Parameters
- begin (Union[datetime, str, None], default '':):
If provided, only fetch data newer than or equal to
begin. - end (Optional[datetime], default None:):
If provided, only fetch data older than or equal to
end. - check_existing (bool, default True):
If
False, do not apply the backtrack interval. - sync_chunks (bool, default False):
If
Trueand the pipe's connector is of type'sql', begin syncing chunks while fetching loads chunks into memory. - debug (bool, default False): Verbosity toggle.
- as_polars (Optional[bool], default None):
Truereturns Polars frames andFalsepandas ones, whatevermeerschaum:dataframesays (seeas_pandas). - as_pandas (Optional[bool], default None):
Truereturns pandas frames andFalsePolars ones. By default (None) the frames are the library of themeerschaum:dataframesetting, whatever the source returned. Achunk_hookis given chunks of the same library.
Returns
**A DataFrame of the newest unseen data (Polars or pandas, per
meerschaum** (dataframe), or a):generator of chunks for a SQL source.
103def get_backtrack_interval( 104 self, 105 check_existing: bool = True, 106 debug: bool = False, 107) -> Union[timedelta, int]: 108 """ 109 Get the chunk interval to use for this pipe. 110 111 Parameters 112 ---------- 113 check_existing: bool, default True 114 If `False`, return a backtrack_interval of 0 minutes. 115 116 Returns 117 ------- 118 The backtrack interval (`timedelta` or `int`) to use with this pipe's `datetime` axis. 119 """ 120 from meerschaum import _core 121 return _core.pipe_get_backtrack_interval(self, check_existing, self.dtypes)
Get the chunk interval to use for this pipe.
Parameters
- check_existing (bool, default True):
If
False, return a backtrack_interval of 0 minutes.
Returns
- The backtrack interval (
timedeltaorint) to use with this pipe'sdatetimeaxis.
85def get_data( 86 self, 87 select_columns: Optional[List[str]] = None, 88 omit_columns: Optional[List[str]] = None, 89 begin: Union[datetime, int, str, None] = None, 90 end: Union[datetime, int, str, None] = None, 91 params: Optional[Dict[str, Any]] = None, 92 as_docs: bool = False, 93 as_iterator: bool = False, 94 as_chunks: bool = False, 95 as_dask: bool = False, 96 as_polars: Optional[bool] = None, 97 add_missing_columns: bool = False, 98 chunk_interval: Union[timedelta, int, None] = None, 99 order: Optional[str] = 'asc', 100 limit: Optional[int] = None, 101 fresh: bool = False, 102 silent: bool = False, 103 debug: bool = False, 104 as_pandas: Optional[bool] = None, 105 **kw: Any 106) -> Union['pl.DataFrame', 'pd.DataFrame', Iterator[Any], List[Dict[str, Any]], None]: 107 """ 108 Get a pipe's data from the instance connector. 109 110 Parameters 111 ---------- 112 select_columns: Optional[List[str]], default None 113 If provided, only select these given columns. 114 Otherwise select all available columns (i.e. `SELECT *`). 115 116 omit_columns: Optional[List[str]], default None 117 If provided, remove these columns from the selection. 118 119 begin: Union[datetime, int, str, None], default None 120 Lower bound datetime to begin searching for data (inclusive). 121 Translates to a `WHERE` clause like `WHERE datetime >= begin`. 122 Defaults to `None`. 123 124 end: Union[datetime, int, str, None], default None 125 Upper bound datetime to stop searching for data (exclusive). 126 Translates to a `WHERE` clause like `WHERE datetime < end`. 127 Defaults to `None`. 128 129 params: Optional[Dict[str, Any]], default None 130 Filter the rows by column values (a `WHERE` clause), e.g. 131 `params={'station': 'KATL'}`: 132 133 - a value is `=`: `{'station': 'KATL'}`; 134 - a list is `IN`: `{'station': ['KATL', 'KGMU']}`; 135 - a leading `_` negates: `{'station': '_KATL'}` is `!=`, `{'station': ['_KATL']}` 136 is `NOT IN`; 137 - `None` (or `'None'`) is `IS NULL` and `'_None'` is `IS NOT NULL`; 138 - a dictionary value is compared as JSON text. 139 140 See `meerschaum.utils.sql.build_where` for more details. 141 142 as_docs: bool, default False 143 If `True`, return a list of dictionaries rather than a DataFrame. 144 Relies on `get_pipe_docs` from the instance connector if implemented. 145 May be combined with `as_chunks` to return an `Iterator[List[Dict]]` 146 chunked by time bounds (useful for large result sets without pandas overhead). 147 148 as_iterator: bool, default False 149 If `True`, return a generator of chunks of pipe data. 150 When combined with `as_docs=True`, yields `List[Dict]` per chunk instead of DataFrames. 151 152 as_chunks: bool, default False 153 Alias for `as_iterator`. 154 When combined with `as_docs=True`, yields `List[Dict]` per chunk instead of DataFrames. 155 156 as_dask: bool, default False 157 Not supported in Meerschaum 5: `True` raises a `TypeError`. 158 Read the data in chunks with `as_iterator=True` (or as one Polars frame) instead. 159 160 as_polars: Optional[bool], default None 161 `True` returns a `polars.DataFrame` and `False` a pandas one, whatever the 162 `meerschaum:dataframe` setting says (see `as_pandas`). Chunks (`as_iterator=True`) 163 follow it too. This may not be combined with `as_docs`. 164 165 add_missing_columns: bool, default False 166 If `True`, add any missing columns from `Pipe.dtypes` to the dataframe. 167 168 chunk_interval: Union[timedelta, int, None], default None 169 If `as_iterator`, then return chunks with `begin` and `end` separated by this interval. 170 This may be set under `pipe.parameters['chunk_minutes']`. 171 By default, use a timedelta of 43200 minutes (30 days). 172 If `chunk_interval` is an integer and the `datetime` axis a timestamp, 173 then use a timedelta with the number of minutes configured to this value. 174 If the `datetime` axis is an integer, an `int` is used verbatim as the number of 175 epoch units, and a `timedelta` is scaled into epoch units by the pipe's `precision`. 176 177 order: Optional[str], default 'asc' 178 If `order` is not `None`, sort the resulting dataframe by indices. 179 180 limit: Optional[int], default None 181 If provided, cap the dataframe to this many rows. 182 183 fresh: bool, default False 184 If `True`, skip local cache and directly query the instance connector. 185 186 silent: bool, default False 187 If `True`, return `None` instead of raising when the instance cannot be read 188 (Meerschaum 4's behavior). By default, database errors raise; a pipe whose table 189 does not exist yet returns `None` either way. 190 191 debug: bool, default False 192 Verbosity toggle. 193 Defaults to `False`. 194 195 as_pandas: Optional[bool], default None 196 `True` returns a pandas DataFrame (a `geopandas.GeoDataFrame` when the pipe has 197 geometry columns) and `False` a Polars one. By default (`None`) the frame is the 198 library of the `meerschaum:dataframe` setting (`MRSM_DATAFRAME` overrides it): 199 Polars for a new root, pandas for a root created before Meerschaum 5.0.0rc15. 200 Polars frames carry geometry as WKB (binary) with the CRS in the field metadata. 201 202 kw: Any 203 Other keyword arguments are passed on to the instance connector (a SQL instance 204 reads `begin_add_minutes`, `end_add_minutes`, `replace_nulls` and 205 `skip_existing_cols_check`; a custom instance connector may read its own). A keyword 206 that nothing reads is ignored with a warning that suggests the parameter you probably 207 meant (`start` → `begin`, `where` or `filters` → `params`). 208 209 Returns 210 ------- 211 A DataFrame of the pipe's data: Polars or pandas, per `meerschaum:dataframe` 212 (or `as_polars` / `as_pandas`). 213 A `List[Dict]` if `as_docs=True`. 214 An iterator of DataFrames if `as_chunks=True` (or `as_iterator=True`). 215 An `Iterator[List[Dict]]` if both `as_docs=True` and `as_chunks=True`. 216 `None` if the pipe's table does not exist yet. 217 218 """ 219 from meerschaum import _core 220 from meerschaum.core.Pipe._keywords import warn_unknown_get_data_keywords 221 warn_unknown_get_data_keywords(self, kw, as_docs=as_docs) 222 if as_polars and (as_docs or as_dask): 223 raise ValueError("`as_polars` may not be combined with `as_docs` or `as_dask`.") 224 if as_pandas and (as_docs or as_dask): 225 raise ValueError("`as_pandas` may not be combined with `as_docs` or `as_dask`.") 226 if as_dask: 227 raise TypeError( 228 "Dask is not supported in Meerschaum 5. Use `as_iterator=True` for chunks " 229 "or `as_polars=True`. See the migration guide: reference/migrating-to-v5." 230 ) 231 polars = _wants_polars(as_docs, as_polars, as_pandas) 232 try: 233 context = _rust_context(self, debug=debug) 234 begin, end = self.parse_date_bounds(begin, end, debug=debug) 235 if as_iterator or as_chunks: 236 chunks = _iterate_chunks(_core.pipe_get_data_chunks( 237 self, 238 context, 239 select_columns=select_columns, 240 omit_columns=omit_columns, 241 begin=begin, 242 end=end, 243 params=params, 244 as_docs=as_docs, 245 as_polars=polars, 246 chunk_interval=chunk_interval, 247 order=order, 248 limit=limit, 249 kwargs=kw, 250 debug=debug, 251 )) 252 return _silent_chunks(chunks) if silent else chunks 253 return _core.pipe_get_data( 254 self, 255 context, 256 select_columns=select_columns, 257 omit_columns=omit_columns, 258 begin=begin, 259 end=end, 260 params=params, 261 as_docs=as_docs, 262 as_polars=polars, 263 add_missing_columns=add_missing_columns, 264 order=order, 265 limit=limit, 266 kwargs=kw, 267 debug=debug, 268 ) 269 except Exception: 270 ### Database errors raise; `silent=True` keeps Meerschaum 4's `None`. 271 if silent: 272 return None 273 raise
Get a pipe's data from the instance connector.
Parameters
- 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):
Lower bound datetime to begin searching for data (inclusive).
Translates to a
WHEREclause likeWHERE datetime >= begin. Defaults toNone. - end (Union[datetime, int, str, None], default None):
Upper bound datetime to stop searching for data (exclusive).
Translates to a
WHEREclause likeWHERE datetime < end. Defaults toNone. params (Optional[Dict[str, Any]], default None): Filter the rows by column values (a
WHEREclause), e.g.params={'station': 'KATL'}:- a value is
=:{'station': 'KATL'}; - a list is
IN:{'station': ['KATL', 'KGMU']}; - a leading
_negates:{'station': '_KATL'}is!=,{'station': ['_KATL']}isNOT IN; None(or'None') isIS NULLand'_None'isIS NOT NULL;- a dictionary value is compared as JSON text.
See
meerschaum.utils.sql.build_wherefor more details.- a value is
- as_docs (bool, default False):
If
True, return a list of dictionaries rather than a DataFrame. Relies onget_pipe_docsfrom the instance connector if implemented. May be combined withas_chunksto return anIterator[List[Dict]]chunked by time bounds (useful for large result sets without pandas overhead). - as_iterator (bool, default False):
If
True, return a generator of chunks of pipe data. When combined withas_docs=True, yieldsList[Dict]per chunk instead of DataFrames. - as_chunks (bool, default False):
Alias for
as_iterator. When combined withas_docs=True, yieldsList[Dict]per chunk instead of DataFrames. - as_dask (bool, default False):
Not supported in Meerschaum 5:
Trueraises aTypeError. Read the data in chunks withas_iterator=True(or as one Polars frame) instead. - as_polars (Optional[bool], default None):
Truereturns apolars.DataFrameandFalsea pandas one, whatever themeerschaum:dataframesetting says (seeas_pandas). Chunks (as_iterator=True) follow it too. This may not be combined withas_docs. - add_missing_columns (bool, default False):
If
True, add any missing columns fromPipe.dtypesto the dataframe. - chunk_interval (Union[timedelta, int, None], default None):
If
as_iterator, then return chunks withbeginandendseparated by this interval. This may be set underpipe.parameters['chunk_minutes']. By default, use a timedelta of 43200 minutes (30 days). Ifchunk_intervalis an integer and thedatetimeaxis a timestamp, then use a timedelta with the number of minutes configured to this value. If thedatetimeaxis is an integer, anintis used verbatim as the number of epoch units, and atimedeltais scaled into epoch units by the pipe'sprecision. - order (Optional[str], default 'asc'):
If
orderis notNone, sort the resulting dataframe by indices. - limit (Optional[int], default None): If provided, cap the dataframe to this many rows.
- fresh (bool, default False):
If
True, skip local cache and directly query the instance connector. - silent (bool, default False):
If
True, returnNoneinstead of raising when the instance cannot be read (Meerschaum 4's behavior). By default, database errors raise; a pipe whose table does not exist yet returnsNoneeither way. - debug (bool, default False):
Verbosity toggle.
Defaults to
False. - as_pandas (Optional[bool], default None):
Truereturns a pandas DataFrame (ageopandas.GeoDataFramewhen the pipe has geometry columns) andFalsea Polars one. By default (None) the frame is the library of themeerschaum:dataframesetting (MRSM_DATAFRAMEoverrides it): Polars for a new root, pandas for a root created before Meerschaum 5.0.0rc15. Polars frames carry geometry as WKB (binary) with the CRS in the field metadata. - kw (Any):
Other keyword arguments are passed on to the instance connector (a SQL instance
reads
begin_add_minutes,end_add_minutes,replace_nullsandskip_existing_cols_check; a custom instance connector may read its own). A keyword that nothing reads is ignored with a warning that suggests the parameter you probably meant (start→begin,whereorfilters→params).
Returns
**A DataFrame of the pipe's data: Polars or pandas, per
meerschaum** (dataframe):(or
as_polars/as_pandas).- A
List[Dict]ifas_docs=True. - An iterator of DataFrames if
as_chunks=True(oras_iterator=True). - An
Iterator[List[Dict]]if bothas_docs=Trueandas_chunks=True. Noneif the pipe's table does not exist yet.
276def get_backtrack_data( 277 self, 278 backtrack_minutes: Optional[int] = None, 279 begin: Union[datetime, int, None] = None, 280 params: Optional[Dict[str, Any]] = None, 281 limit: Optional[int] = None, 282 fresh: bool = False, 283 debug: bool = False, 284 **kw: Any 285) -> Optional['pd.DataFrame']: 286 """ 287 Get the most recent data from the instance connector as a Pandas DataFrame. 288 289 Parameters 290 ---------- 291 backtrack_minutes: Optional[int], default None 292 How many minutes from `begin` to select from. 293 If `None`, use `pipe.parameters['fetch']['backtrack_minutes']`. 294 295 begin: Optional[datetime], default None 296 The starting point to search for data. 297 If begin is `None` (default), use the most recent observed datetime 298 (AKA sync_time). 299 300 ``` 301 E.g. begin = 02:00 302 303 Search this region. Ignore this, even if there's data. 304 / / / / / / / / / | 305 -----|----------|----------|----------|----------|----------| 306 00:00 01:00 02:00 03:00 04:00 05:00 307 308 ``` 309 310 params: Optional[Dict[str, Any]], default None 311 The standard Meerschaum `params` query dictionary. 312 313 limit: Optional[int], default None 314 If provided, cap the number of rows to be returned. 315 316 fresh: bool, default False 317 If `True`, Ignore local cache and pull directly from the instance connector. 318 Only comes into effect if a pipe was created with `cache=True`. 319 320 debug: bool default False 321 Verbosity toggle. 322 323 **kw: Any 324 Also `end` (exclusive), `select_columns`, `omit_columns`, `order` (default `'desc'`), 325 `as_docs`, `as_polars`, `as_pandas` and `add_missing_columns`, as in `get_data()`. 326 Other keywords go to the instance connector; a keyword nothing reads is ignored with a 327 warning that suggests the parameter you probably meant. 328 329 Returns 330 ------- 331 A DataFrame (Polars or pandas, per `meerschaum:dataframe`, as `get_data()`) of the pipe's 332 data corresponding to the provided parameters. Backtrack data is a convenient way to get a 333 pipe's data "backtracked" from the most recent datetime. 334 """ 335 from meerschaum import _core 336 from meerschaum.core.Pipe._keywords import BACKTRACK_KEYWORDS, warn_unknown_backtrack_keywords 337 warn_unknown_backtrack_keywords(self, kw) 338 polars = _wants_polars( 339 kw.get('as_docs', False), 340 kw.get('as_polars', None), 341 kw.get('as_pandas', None), 342 ) 343 context = _rust_context(self, debug=debug) 344 named = BACKTRACK_KEYWORDS 345 end = kw.get('end', None) 346 return _core.pipe_get_backtrack_data( 347 self, 348 context, 349 backtrack_minutes=backtrack_minutes, 350 begin=self.parse_date_bounds(begin, debug=debug), 351 params=params, 352 limit=limit, 353 order=kw.get('order', 'desc') or 'desc', 354 select_columns=kw.get('select_columns', None), 355 omit_columns=kw.get('omit_columns', None), 356 end=self.parse_date_bounds(end, debug=debug) if end is not None else None, 357 as_docs=kw.get('as_docs', False), 358 as_polars=polars, 359 add_missing_columns=kw.get('add_missing_columns', False), 360 kwargs={key: val for key, val in kw.items() if key not in named}, 361 debug=debug, 362 )
Get the most recent data from the instance connector as a Pandas DataFrame.
Parameters
- backtrack_minutes (Optional[int], default None):
How many minutes from
beginto select from. IfNone, usepipe.parameters['fetch']['backtrack_minutes']. begin (Optional[datetime], default None): The starting point to search for data. If begin is
None(default), use the most recent observed datetime (AKA sync_time).E.g. begin = 02:00 Search this region. Ignore this, even if there's data. / / / / / / / / / | -----|----------|----------|----------|----------|----------| 00:00 01:00 02:00 03:00 04:00 05:00params (Optional[Dict[str, Any]], default None): The standard Meerschaum
paramsquery dictionary.- limit (Optional[int], default None): If provided, cap the number of rows to be returned.
- fresh (bool, default False):
If
True, Ignore local cache and pull directly from the instance connector. Only comes into effect if a pipe was created withcache=True. - debug (bool default False): Verbosity toggle.
- **kw (Any):
Also
end(exclusive),select_columns,omit_columns,order(default'desc'),as_docs,as_polars,as_pandasandadd_missing_columns, as inget_data(). Other keywords go to the instance connector; a keyword nothing reads is ignored with a warning that suggests the parameter you probably meant.
Returns
**A DataFrame (Polars or pandas, per
meerschaum** (dataframe, asget_data()) of the pipe's):data corresponding to the provided parameters. Backtrack data is a convenient way to get a
- pipe's data "backtracked" from the most recent datetime.
365def get_rowcount( 366 self, 367 begin: Union[datetime, int, None] = None, 368 end: Union[datetime, int, None] = None, 369 params: Optional[Dict[str, Any]] = None, 370 remote: bool = False, 371 estimate: bool = False, 372 silent: bool = False, 373 debug: bool = False 374) -> int: 375 """ 376 Get a Pipe's instance or remote rowcount. 377 378 Parameters 379 ---------- 380 begin: Optional[datetime], default None 381 Count rows where datetime >= begin (inclusive). 382 383 end: Optional[datetime], default None 384 Count rows where datetime < end (exclusive). 385 386 remote: bool, default False 387 Count rows from a pipe's remote source. 388 **NOTE**: This is experimental! 389 390 estimate: bool, default False 391 If `True`, return the database's estimate of the number of rows from its catalog 392 statistics (PostgreSQL, TimescaleDB, MySQL / MariaDB, MSSQL, Oracle) instead of 393 counting them: instant on large tables, and close (the statistics trail recent 394 writes). The count is exact where the database has no estimate (SQLite, DuckDB), for 395 small tables, and with `begin`, `end`, `params` or `remote`. 396 397 silent: bool, default False 398 If `True`, return 0 instead of raising when the instance cannot be read 399 (Meerschaum 4's behavior). 400 401 debug: bool, default False 402 Verbosity toggle. 403 404 Returns 405 ------- 406 An `int` of the number of rows in the pipe corresponding to the provided parameters. 407 Returned 0 if the pipe does not exist. Raises the database error if the instance cannot 408 be read (0 with `silent=True`). 409 """ 410 from meerschaum import _core 411 try: 412 context = _rust_context(self, debug=debug) 413 begin, end = self.parse_date_bounds(begin, end, debug=debug) 414 return _core.pipe_get_rowcount( 415 self, 416 context, 417 begin=begin, 418 end=end, 419 params=params, 420 remote=remote, 421 estimate=estimate, 422 debug=debug, 423 ) 424 except Exception: 425 if silent: 426 return 0 427 raise
Get a Pipe's instance or remote rowcount.
Parameters
- begin (Optional[datetime], default None): Count rows where datetime >= begin (inclusive).
- end (Optional[datetime], default None): Count rows where datetime < end (exclusive).
- remote (bool, default False): Count rows from a pipe's remote source. NOTE: This is experimental!
- estimate (bool, default False):
If
True, return the database's estimate of the number of rows from its catalog statistics (PostgreSQL, TimescaleDB, MySQL / MariaDB, MSSQL, Oracle) instead of counting them: instant on large tables, and close (the statistics trail recent writes). The count is exact where the database has no estimate (SQLite, DuckDB), for small tables, and withbegin,end,paramsorremote. - silent (bool, default False):
If
True, return 0 instead of raising when the instance cannot be read (Meerschaum 4's behavior). - debug (bool, default False): Verbosity toggle.
Returns
- An
intof the number of rows in the pipe corresponding to the provided parameters. - Returned 0 if the pipe does not exist. Raises the database error if the instance cannot
- be read (0 with
silent=True).
430def get_size( 431 self, 432 debug: bool = False, 433 **kw: Any 434) -> Union[int, None]: 435 """ 436 Return the on-disk size of the pipe's target table in bytes. 437 438 Parameters 439 ---------- 440 debug: bool, default False 441 Verbosity toggle. 442 443 Returns 444 ------- 445 An `int` of the number of bytes occupied by the pipe's target table, 446 or `None` if the size could not be determined (e.g. the connector does 447 not implement `get_pipe_size()` or the table does not exist). 448 """ 449 from meerschaum.utils.warnings import warn 450 from meerschaum.utils.venv import Venv 451 from meerschaum.connectors import get_connector_plugin 452 from meerschaum.utils.misc import filter_keywords 453 454 connector = self.instance_connector 455 try: 456 with Venv(get_connector_plugin(connector)): 457 if not hasattr(connector, 'get_pipe_size'): 458 return None 459 kwargs = filter_keywords( 460 connector.get_pipe_size, 461 debug=debug, 462 **kw 463 ) 464 return connector.get_pipe_size(self, **kwargs) 465 except NotImplementedError: 466 return None 467 except Exception as e: 468 warn(f"Failed to get the size of {self}:\n{e}", stack=False) 469 return None
Return the on-disk size of the pipe's target table in bytes.
Parameters
- debug (bool, default False): Verbosity toggle.
Returns
- An
intof the number of bytes occupied by the pipe's target table, - or
Noneif the size could not be determined (e.g. the connector does - not implement
get_pipe_size()or the table does not exist).
695def get_doc(self, **kwargs) -> Union[Dict[str, Any], None]: 696 """ 697 Convenience function to return a single row as a dictionary (or `None`) from `Pipe.get_data(). 698 Keywords arguments are passed to `Pipe.get_data()`. 699 """ 700 from meerschaum.utils.warnings import warn 701 kwargs['limit'] = 1 702 kwargs['as_docs'] = True 703 try: 704 docs = self.get_data(**kwargs) 705 if not docs: 706 return None 707 return docs[0] 708 except Exception as e: 709 warn(f"Failed to read value from {self}:\n{e}", stack=False) 710 return None
Convenience function to return a single row as a dictionary (or None) from Pipe.get_data().
Keywords arguments are passed toPipe.get_data()`.
712def get_docs(self, **kwargs) -> list[dict[str, Any]]: 713 """ 714 Convenience method to return a pipe's data as a list of dictionaries. 715 Relies on `get_pipe_docs` from the instance connector if implemented. 716 """ 717 kwargs['as_docs'] = True 718 return self.get_data(**kwargs)
Convenience method to return a pipe's data as a list of dictionaries.
Relies on get_pipe_docs from the instance connector if implemented.
720def get_value( 721 self, 722 column: str, 723 params: Optional[Dict[str, Any]] = None, 724 **kwargs: Any 725) -> Any: 726 """ 727 Convenience function to return a single value (or `None`) from `Pipe.get_data()`. 728 Keywords arguments are passed to `Pipe.get_data()`. 729 """ 730 from meerschaum.utils.warnings import warn 731 kwargs['select_columns'] = [column] 732 kwargs['limit'] = 1 733 kwargs['as_docs'] = True 734 try: 735 docs = self.get_data(params=params, **kwargs) 736 if not docs: 737 return None 738 if column not in docs[0]: 739 raise ValueError(f"Column '{column}' was not included in the result set.") 740 return docs[0][column] 741 except Exception as e: 742 warn(f"Failed to read value from {self}:\n{e}", stack=False) 743 return None
Convenience function to return a single value (or None) from Pipe.get_data().
Keywords arguments are passed to Pipe.get_data().
472def get_chunk_interval( 473 self, 474 chunk_interval: Union[timedelta, int, None] = None, 475 debug: bool = False, 476) -> Union[timedelta, int]: 477 """ 478 Get the chunk interval to use for this pipe. 479 480 The size is read from the `verify` parameters. Any one of these aliased keys may be used 481 (the first present, in this priority order, wins): 482 483 - `verify.chunk_minutes` (the default; 43200 — 30 days — if none is set) 484 - `verify.chunk_hours` 485 - `verify.chunk_days` 486 - `verify.chunk_weeks` 487 - `verify.chunk_years` 488 - `verify.chunk_seconds` 489 490 For an integer datetime axis, `verify.chunk_range` (if set) is used verbatim as the chunk size 491 in epoch units. Otherwise the time-based size above is converted to epoch units via the pipe's 492 `precision`, or — preserving legacy behavior when no `precision` is set — its minutes are used 493 verbatim. 494 495 Parameters 496 ---------- 497 chunk_interval: Union[timedelta, int, None], default None 498 If provided, use this value instead of the `verify` keys. 499 An `int` is already in the axis's own units — epoch units for an integer axis, 500 minutes otherwise — and is used verbatim. A `timedelta` is a duration, so on an 501 integer axis it is scaled into epoch units by the pipe's `precision`. 502 503 Returns 504 ------- 505 The chunk interval (`timedelta` or `int`) to use with this pipe's `datetime` axis. 506 """ 507 from meerschaum import _core 508 return _core.get_chunk_interval(self, chunk_interval, self.dtypes)
Get the chunk interval to use for this pipe.
The size is read from the verify parameters. Any one of these aliased keys may be used
(the first present, in this priority order, wins):
- `verify.chunk_minutes` (the default; 43200 — 30 days — if none is set)
- `verify.chunk_hours`
- `verify.chunk_days`
- `verify.chunk_weeks`
- `verify.chunk_years`
- `verify.chunk_seconds`
For an integer datetime axis, verify.chunk_range (if set) is used verbatim as the chunk size
in epoch units. Otherwise the time-based size above is converted to epoch units via the pipe's
precision, or, preserving legacy behavior when no precision is set, its minutes are used
verbatim.
Parameters
- chunk_interval (Union[timedelta, int, None], default None):
If provided, use this value instead of the
verifykeys. Anintis already in the axis's own units, epoch units for an integer axis, minutes otherwise, and is used verbatim. Atimedeltais a duration, so on an integer axis it is scaled into epoch units by the pipe'sprecision.
Returns
- The chunk interval (
timedeltaorint) to use with this pipe'sdatetimeaxis.
511def get_chunk_bounds( 512 self, 513 begin: Union[datetime, int, None] = None, 514 end: Union[datetime, int, None] = None, 515 bounded: bool = False, 516 chunk_interval: Union[timedelta, int, None] = None, 517 align: bool = False, 518 debug: bool = False, 519) -> List[ 520 Tuple[ 521 Union[datetime, int, None], 522 Union[datetime, int, None], 523 ] 524]: 525 """ 526 Return a list of datetime bounds for iterating over the pipe's `datetime` axis. 527 528 Parameters 529 ---------- 530 begin: Union[datetime, int, None], default None 531 If provided, do not select less than this value. 532 Otherwise the first chunk will be unbounded. 533 534 end: Union[datetime, int, None], default None 535 If provided, do not select greater than or equal to this value. 536 Otherwise the last chunk will be unbounded. 537 538 bounded: bool, default False 539 If `True`, do not include `None` in the first chunk. 540 541 chunk_interval: Union[timedelta, int, None], default None 542 If provided, use this interval for the size of chunk boundaries. 543 The default value for this pipe may be set 544 under `pipe.parameters['verify']['chunk_minutes']`. 545 546 align: bool, default False 547 If `True`, anchor the interior chunk boundaries to a fixed Unix-epoch grid (the same 548 grid used for native range partitioning) rather than to `begin`. This makes the 549 boundaries deterministic across re-syncs and aligned with the pipe's partitions 550 (used by `Pipe.verify()`). The first chunk's lower bound and the last chunk's upper 551 bound are still clamped to `begin` / `end`. 552 553 debug: bool, default False 554 Verbosity toggle. 555 556 Returns 557 ------- 558 A list of chunk bounds (datetimes or integers). 559 If unbounded, the first and last chunks will include `None`. 560 """ 561 from meerschaum import _core 562 ### The sync times stand in for an unset `begin` / `end` (v4 queries them the same way). 563 oldest = self.get_sync_time(newest=False, debug=debug) if begin is None else None 564 newest = self.get_sync_time(newest=True, debug=debug) if end is None else None 565 return _core.get_chunk_bounds( 566 self.parse_date_bounds(begin, debug=debug), 567 self.parse_date_bounds(end, debug=debug), 568 bounded, 569 self.get_chunk_interval(chunk_interval, debug=debug), 570 align, 571 self.parse_date_bounds(oldest, debug=debug), 572 self.parse_date_bounds(newest, debug=debug), 573 )
Return a list of datetime bounds for iterating over the pipe's datetime axis.
Parameters
- begin (Union[datetime, int, None], default None): If provided, do not select less than this value. Otherwise the first chunk will be unbounded.
- end (Union[datetime, int, None], default None): If provided, do not select greater than or equal to this value. Otherwise the last chunk will be unbounded.
- bounded (bool, default False):
If
True, do not includeNonein the first chunk. - chunk_interval (Union[timedelta, int, None], default None):
If provided, use this interval for the size of chunk boundaries.
The default value for this pipe may be set
under
pipe.parameters['verify']['chunk_minutes']. - align (bool, default False):
If
True, anchor the interior chunk boundaries to a fixed Unix-epoch grid (the same grid used for native range partitioning) rather than tobegin. This makes the boundaries deterministic across re-syncs and aligned with the pipe's partitions (used byPipe.verify()). The first chunk's lower bound and the last chunk's upper bound are still clamped tobegin/end. - debug (bool, default False): Verbosity toggle.
Returns
- A list of chunk bounds (datetimes or integers).
- If unbounded, the first and last chunks will include
None.
576def get_chunk_bounds_batches( 577 self, 578 chunk_bounds: List[Tuple[Union[datetime, int, None], Union[datetime, int, None]]], 579 batchsize: Optional[int] = None, 580 workers: Optional[int] = None, 581 debug: bool = False, 582) -> List[ 583 Tuple[ 584 Tuple[ 585 Union[datetime, int, None], 586 Union[datetime, int, None], 587 ], ... 588 ] 589]: 590 """ 591 Return a list of tuples of chunk bounds of size `batchsize`. 592 593 Parameters 594 ---------- 595 chunk_bounds: List[Tuple[Union[datetime, int, None], Union[datetime, int, None]]] 596 A list of chunk_bounds (see `Pipe.get_chunk_bounds()`). 597 598 batchsize: Optional[int], default None 599 How many chunks to include in a batch. Defaults to `Pipe.get_num_workers()`. 600 601 workers: Optional[int], default None 602 If `batchsize` is `None`, use this as the desired number of workers. 603 Passed to `Pipe.get_num_workers()`. 604 605 Returns 606 ------- 607 A list of tuples of chunk bound tuples. 608 """ 609 from meerschaum.utils.misc import iterate_chunks 610 611 if batchsize is None: 612 batchsize = self.get_num_workers(workers=workers) 613 614 return [ 615 tuple( 616 _batch_chunk_bounds 617 for _batch_chunk_bounds in batch 618 if _batch_chunk_bounds is not None 619 ) 620 for batch in iterate_chunks(chunk_bounds, batchsize) 621 if batch 622 ]
Return a list of tuples of chunk bounds of size batchsize.
Parameters
- chunk_bounds (List[Tuple[Union[datetime, int, None], Union[datetime, int, None]]]):
A list of chunk_bounds (see
Pipe.get_chunk_bounds()). - batchsize (Optional[int], default None):
How many chunks to include in a batch. Defaults to
Pipe.get_num_workers(). - workers (Optional[int], default None):
If
batchsizeisNone, use this as the desired number of workers. Passed toPipe.get_num_workers().
Returns
- A list of tuples of chunk bound tuples.
625def parse_date_bounds(self, *dt_vals: Union[datetime, int, None], debug: bool = False) -> Union[ 626 datetime, 627 int, 628 str, 629 None, 630 Tuple[Union[datetime, int, str, None]] 631]: 632 """ 633 Given a date bound (begin, end), coerce a timezone if necessary. 634 """ 635 from meerschaum.utils.misc import is_int 636 from meerschaum.utils.dtypes import coerce_timezone, MRSM_PD_DTYPES, are_dtypes_equal 637 from meerschaum.utils.warnings import warn 638 639 _columns = None 640 _dtypes = None 641 642 def _get_coercion_info(): 643 nonlocal _columns, _dtypes 644 if _columns is None: 645 _columns = self.get_parameters(debug=debug).get('columns', {}) or {} 646 if _dtypes is None: 647 _dtypes = self.get_dtypes(debug=debug) 648 649 def _parse_date_bound(dt_val): 650 if dt_val is None: 651 return None 652 653 if isinstance(dt_val, int): 654 return dt_val 655 656 if dt_val == '': 657 return '' 658 659 if is_int(dt_val): 660 return int(dt_val) 661 662 if isinstance(dt_val, str): 663 ### Only a text bound needs `dateutil` (a missing package is not installed 664 ### for the integer, `None` and datetime bounds that never reach here). 665 dateutil_parser = mrsm.attempt_import('dateutil.parser') 666 try: 667 dt_val = dateutil_parser.parse(dt_val) 668 except Exception as e: 669 warn(f"Could not parse '{dt_val}' as datetime:\n{e}") 670 return None 671 672 _get_coercion_info() 673 dt_col = _columns.get('datetime', None) 674 dt_typ = str(_dtypes.get(dt_col, 'datetime')) 675 if are_dtypes_equal(dt_typ, 'int'): 676 if self.get_parameters(debug=debug).get('precision'): 677 from meerschaum.utils.dtypes import datetime_to_int 678 return datetime_to_int(dt_val, self.precision['unit']) 679 from meerschaum.utils.warnings import error 680 error( 681 f"Cannot use datetime bound '{dt_val}' on the non-epoch integer axis " 682 f"of {self}.\n Pass an integer instead, or set the `precision` parameter.", 683 ValueError, 684 ) 685 if dt_typ == 'datetime': 686 dt_typ = MRSM_PD_DTYPES['datetime'] 687 return coerce_timezone(dt_val, strip_utc=('utc' not in dt_typ.lower())) 688 689 bounds = tuple(_parse_date_bound(dt_val) for dt_val in dt_vals) 690 if len(bounds) == 1: 691 return bounds[0] 692 return bounds
Given a date bound (begin, end), coerce a timezone if necessary.
12def register( 13 self, 14 debug: bool = False, 15 **kw: Any 16) -> SuccessTuple: 17 """ 18 Register a new Pipe along with its attributes. 19 20 Parameters 21 ---------- 22 debug: bool, default False 23 Verbosity toggle. 24 25 kw: Any 26 Keyword arguments to pass to `instance_connector.register_pipe()`. 27 28 Returns 29 ------- 30 A `SuccessTuple` of success, message. 31 """ 32 if self.temporary: 33 return False, "Cannot register pipes created with `temporary=True` (read-only)." 34 35 from meerschaum.utils.formatting import print_exception 36 from meerschaum.utils.venv import Venv 37 from meerschaum.connectors import get_connector_plugin, custom_types 38 from meerschaum.config._patch import apply_patch_to_config 39 40 import warnings 41 with warnings.catch_warnings(): 42 warnings.simplefilter('ignore') 43 try: 44 _conn = self.connector 45 except Exception: 46 _conn = None 47 48 if isinstance(_conn, str): 49 _conn = None 50 51 if ( 52 _conn is not None 53 and 54 (_conn.type == 'plugin' or _conn.type in custom_types) 55 and 56 getattr(_conn, 'register', None) is not None 57 ): 58 try: 59 with Venv(get_connector_plugin(_conn), debug=debug): 60 params = self.connector.register(self) 61 except Exception: 62 print_exception() 63 params = None 64 params = {} if params is None else params 65 if not isinstance(params, dict): 66 from meerschaum.utils.warnings import warn 67 warn( 68 f"Invalid parameters returned from `register()` in connector {self.connector}:\n" 69 + f"{params}" 70 ) 71 else: 72 self.parameters = apply_patch_to_config(params, self.parameters) 73 74 if not self.parameters: 75 cols = self.columns if self.columns else {'datetime': None, 'id': None} 76 self.parameters = { 77 'columns': cols, 78 } 79 80 ### Refuse an index column which cannot be sorted or deduplicated, rather than 81 ### registering a pipe whose second sync is guaranteed to fail. 82 indices_success, indices_msg = self.validate_indices(debug=debug) 83 if not indices_success: 84 return indices_success, indices_msg 85 86 ### Registering a pipe that exists again must not get past its `parameters` lock (a custom 87 ### instance connector may overwrite the registration; the core checks its own instances). 88 refusal = self._edit_lock_refusal(patch=False, debug=debug) 89 if refusal: 90 return False, refusal 91 92 with Venv(get_connector_plugin(self.instance_connector)): 93 result = self.instance_connector.register_pipe(self, debug=debug, **kw) 94 from meerschaum.core.Pipe._cache import _bump_invalidation_epoch 95 _bump_invalidation_epoch() 96 return result
Register a new Pipe along with its attributes.
Parameters
- debug (bool, default False): Verbosity toggle.
- kw (Any):
Keyword arguments to pass to
instance_connector.register_pipe().
Returns
- A
SuccessTupleof success, message.
23@property 24def attributes(self) -> Dict[str, Any]: 25 """ 26 Return a dictionary of a pipe's keys and parameters. 27 These values are reflected directly from the pipes table of the instance. 28 """ 29 from meerschaum.config import get_config 30 from meerschaum.config._patch import apply_patch_to_config 31 from meerschaum.utils.venv import Venv 32 from meerschaum.connectors import get_connector_plugin 33 from meerschaum.utils.dtypes import get_current_timestamp 34 35 if self.temporary: 36 return self._attributes 37 38 with self._attributes_lock: 39 # Instance connectors may read local parameters while fetching metadata. 40 if self.__dict__.get('_attributes_loading', False): 41 return self._attributes 42 timeout_seconds = get_config('pipes', 'attributes', 'local_cache_timeout_seconds') 43 now = get_current_timestamp('ms', as_int=True) / 1000 44 sync_time = self._get_cached_value('_attributes_sync_time', debug=self.debug) 45 timed_out = ( 46 sync_time is None 47 or (timeout_seconds is not None and (now - sync_time) >= timeout_seconds) 48 ) 49 if timed_out: 50 local_attributes = self._get_cached_value('attributes', debug=self.debug) or {} 51 self._attributes_loading = True 52 try: 53 with Venv(get_connector_plugin(self.instance_connector)): 54 instance_attributes = self.instance_connector.get_pipe_attributes(self) 55 if not isinstance(instance_attributes, dict) or ( 56 not instance_attributes and self._get_cached_value('_id') is not None 57 ): 58 raise RuntimeError(f"Missing metadata for registered pipe {self}.") 59 self._cache_value( 60 'attributes', 61 apply_patch_to_config(instance_attributes, local_attributes), 62 memory_only=True, 63 debug=self.debug, 64 ) 65 except Exception as e: 66 self._attributes_load_failures = self.__dict__.get('_attributes_load_failures', 0) + 1 67 self._clear_cache_key('_attributes_sync_time', debug=self.debug) 68 warn(f"Failed to load attributes for {self}: {e}") 69 return self._attributes 70 finally: 71 self._attributes_loading = False 72 self._cache_value( 73 '_attributes_sync_time', get_current_timestamp('ms', as_int=True) / 1000, 74 memory_only=True, debug=self.debug, 75 ) 76 77 return self._attributes
Return a dictionary of a pipe's keys and parameters. These values are reflected directly from the pipes table of the instance.
214@property 215def parameters(self) -> Optional[Dict[str, Any]]: 216 """ 217 Return the parameters dictionary of the pipe. 218 219 The dictionary is a live view (design §4.4): reads are resolved, and mutations (including 220 nested ones) write through to the raw parameters, so 221 `pipe.parameters['fetch']['backtrack_minutes'] = 5` followed by `pipe.edit()` persists. 222 """ 223 resolved = self.get_parameters(debug=self.debug) 224 from ._live_parameters import live_view 225 return live_view(self, resolved)
Return the parameters dictionary of the pipe.
The dictionary is a live view (design §4.4): reads are resolved, and mutations (including
nested ones) write through to the raw parameters, so
pipe.parameters['fetch']['backtrack_minutes'] = 5 followed by pipe.edit() persists.
238@property 239def columns(self) -> Union[Dict[str, str], None]: 240 """ 241 Return the `columns` dictionary defined in `meerschaum.Pipe.parameters`. 242 """ 243 cols = self.get_parameters(debug=self.debug).get('columns', {}) 244 if not isinstance(cols, dict): 245 return {} 246 return {col_ix: col for col_ix, col in cols.items() if col and col_ix}
Return the columns dictionary defined in meerschaum.Pipe.parameters.
263@property 264def indices(self) -> Union[Dict[str, Union[str, List[str]]], None]: 265 """ 266 Return the `indices` dictionary defined in `meerschaum.Pipe.parameters`. 267 """ 268 _parameters = self.get_parameters(debug=self.debug) 269 indices_key = ( 270 'indexes' 271 if 'indexes' in _parameters 272 else 'indices' 273 ) 274 275 _indices = _parameters.get(indices_key, {}) 276 _columns = self.columns 277 dt_col = _columns.get('datetime', None) 278 if not isinstance(_indices, dict): 279 _indices = {} 280 unique_cols = list(set(( 281 [dt_col] 282 if dt_col 283 else [] 284 ) + [ 285 col 286 for col_ix, col in _columns.items() 287 if col and col_ix != 'datetime' 288 ])) 289 return { 290 **({'unique': unique_cols} if len(unique_cols) > 1 else {}), 291 **{col_ix: col for col_ix, col in _columns.items() if col}, 292 **_indices 293 }
Return the indices dictionary defined in meerschaum.Pipe.parameters.
296@property 297def indexes(self) -> Union[Dict[str, Union[str, List[str]]], None]: 298 """ 299 Alias for `meerschaum.Pipe.indices`. 300 """ 301 return self.indices
Alias for meerschaum.Pipe.indices.
352@property 353def dtypes(self) -> Dict[str, Any]: 354 """ 355 If defined, return the `dtypes` dictionary defined in `meerschaum.Pipe.parameters`. 356 """ 357 return self.get_dtypes(refresh=False, debug=self.debug)
If defined, return the dtypes dictionary defined in meerschaum.Pipe.parameters.
510@property 511def autoincrement(self) -> bool: 512 """ 513 Return the `autoincrement` parameter for the pipe. 514 """ 515 return self.get_parameters(debug=self.debug).get('autoincrement', False)
Return the autoincrement parameter for the pipe.
526@property 527def autotime(self) -> bool: 528 """ 529 Return the `autotime` parameter for the pipe. 530 """ 531 return self.get_parameters(debug=self.debug).get('autotime', False)
Return the autotime parameter for the pipe.
477@property 478def upsert(self) -> bool: 479 """ 480 Return whether `upsert` is set for the pipe. 481 """ 482 return self.get_parameters(debug=self.debug).get('upsert', False)
Return whether upsert is set for the pipe.
493@property 494def static(self) -> bool: 495 """ 496 Return whether `static` is set for the pipe. 497 """ 498 return self.get_parameters(debug=self.debug).get('static', False)
Return whether static is set for the pipe.
542@property 543def tzinfo(self) -> Union[None, timezone]: 544 """ 545 Return `timezone.utc` if the pipe is timezone-aware. 546 """ 547 _tzinfo = self._get_cached_value('tzinfo', debug=self.debug) 548 if _tzinfo is not None: 549 return _tzinfo if _tzinfo != 'None' else None 550 551 _tzinfo = None 552 dt_col = self.columns.get('datetime', None) 553 dt_typ = str(self.dtypes.get(dt_col, 'datetime')) if dt_col else None 554 if self.autotime: 555 ts_col = mrsm.get_config('pipes', 'autotime', 'column_name_if_datetime_missing') 556 ts_typ = self.dtypes.get(ts_col, 'datetime') 557 dt_typ = ts_typ 558 559 if dt_typ and 'utc' in dt_typ.lower() or dt_typ == 'datetime': 560 _tzinfo = timezone.utc 561 562 self._cache_value('tzinfo', (_tzinfo if _tzinfo is not None else 'None'), debug=self.debug) 563 return _tzinfo
Return timezone.utc if the pipe is timezone-aware.
566@property 567def enforce(self) -> bool: 568 """ 569 Return the `enforce` parameter for the pipe. 570 """ 571 return self.get_parameters(debug=self.debug).get('enforce', True)
Return the enforce parameter for the pipe.
582@property 583def null_indices(self) -> bool: 584 """ 585 Return the `null_indices` parameter for the pipe. 586 """ 587 return self.get_parameters(debug=self.debug).get('null_indices', True)
Return the null_indices parameter for the pipe.
598@property 599def mixed_numerics(self) -> bool: 600 """ 601 Return the `mixed_numerics` parameter for the pipe. 602 """ 603 return self.get_parameters(debug=self.debug).get('mixed_numerics', True)
Return the mixed_numerics parameter for the pipe.
614def get_columns(self, *args: str, error: bool = False) -> Union[str, Tuple[str]]: 615 """ 616 Check if the requested columns are defined. 617 618 Parameters 619 ---------- 620 *args: str 621 The column names to be retrieved. 622 623 error: bool, default False 624 If `True`, raise an `Exception` if the specified column is not defined. 625 626 Returns 627 ------- 628 A tuple of the same size of `args` or a `str` if `args` is a single argument. 629 630 Examples 631 -------- 632 >>> pipe = mrsm.Pipe('test', 'test') 633 >>> pipe.columns = {'datetime': 'dt', 'id': 'id'} 634 >>> pipe.get_columns('datetime', 'id') 635 ('dt', 'id') 636 >>> pipe.get_columns('value', error=True) 637 Exception: 🛑 Missing 'value' column for Pipe('test', 'test'). 638 """ 639 from meerschaum.utils.warnings import error as _error 640 if not args: 641 args = tuple(self.columns.keys()) 642 col_names = [] 643 for col in args: 644 col_name = None 645 try: 646 col_name = self.columns[col] 647 if col_name is None and error: 648 _error(f"Please define the name of the '{col}' column for {self}.") 649 except Exception: 650 col_name = None 651 if col_name is None and error: 652 _error(f"Missing '{col}'" + f" column for {self}.") 653 col_names.append(col_name) 654 if len(col_names) == 1: 655 return col_names[0] 656 return tuple(col_names)
Check if the requested columns are defined.
Parameters
- *args (str): The column names to be retrieved.
- error (bool, default False):
If
True, raise anExceptionif the specified column is not defined.
Returns
- A tuple of the same size of
argsor astrifargsis a single argument.
Examples
>>> pipe = mrsm.Pipe('test', 'test')
>>> pipe.columns = {'datetime': 'dt', 'id': 'id'}
>>> pipe.get_columns('datetime', 'id')
('dt', 'id')
>>> pipe.get_columns('value', error=True)
Exception: 🛑 Missing 'value' column for Pipe('test', 'test').
659def get_columns_types( 660 self, 661 refresh: bool = False, 662 debug: bool = False, 663) -> Union[Dict[str, str], None]: 664 """ 665 Get a dictionary of a pipe's column names and their types. 666 667 Parameters 668 ---------- 669 refresh: bool, default False 670 If `True`, invalidate the cache and fetch directly from the instance connector. 671 672 debug: bool, default False: 673 Verbosity toggle. 674 675 Returns 676 ------- 677 A dictionary of column names (`str`) to column types (`str`). 678 679 Examples 680 -------- 681 >>> pipe.get_columns_types() 682 { 683 'dt': 'TIMESTAMP WITH TIMEZONE', 684 'id': 'BIGINT', 685 'val': 'DOUBLE PRECISION', 686 } 687 >>> 688 """ 689 from meerschaum.connectors import get_connector_plugin 690 from meerschaum.utils.dtypes import get_current_timestamp 691 692 now = get_current_timestamp('ms', as_int=True) / 1000 693 cache_seconds = ( 694 mrsm.get_config('pipes', 'static', 'static_schema_cache_seconds') 695 if self.static 696 else mrsm.get_config('pipes', 'dtypes', 'columns_types_cache_seconds') 697 ) 698 if refresh: 699 self._clear_cache_key('_columns_types_timestamp', debug=debug) 700 self._clear_cache_key('_columns_types', debug=debug) 701 702 _columns_types = self._get_cached_value('_columns_types', debug=debug) 703 if _columns_types: 704 columns_types_timestamp = self._get_cached_value('_columns_types_timestamp', debug=debug) 705 if columns_types_timestamp is not None: 706 delta = now - columns_types_timestamp 707 if delta < cache_seconds: 708 if debug: 709 dprint( 710 f"Returning cached `columns_types` for {self} " 711 f"({round(delta, 2)} seconds old)." 712 ) 713 return _columns_types 714 715 with mrsm.Venv(get_connector_plugin(self.instance_connector)): 716 _columns_types = ( 717 self.instance_connector.get_pipe_columns_types(self, debug=debug) 718 if hasattr(self.instance_connector, 'get_pipe_columns_types') 719 else None 720 ) 721 722 self._cache_value('_columns_types', _columns_types, debug=debug) 723 self._cache_value('_columns_types_timestamp', now, debug=debug) 724 return _columns_types or {}
Get a dictionary of a pipe's column names and their types.
Parameters
- refresh (bool, default False):
If
True, invalidate the cache and fetch directly from the instance connector. - debug (bool, default False:): Verbosity toggle.
Returns
- A dictionary of column names (
str) to column types (str).
Examples
>>> pipe.get_columns_types()
{
'dt': 'TIMESTAMP WITH TIMEZONE',
'id': 'BIGINT',
'val': 'DOUBLE PRECISION',
}
>>>
727def get_columns_indices( 728 self, 729 debug: bool = False, 730 refresh: bool = False, 731) -> Dict[str, List[Dict[str, str]]]: 732 """ 733 Return a dictionary mapping columns to index information. 734 """ 735 from meerschaum.connectors import get_connector_plugin 736 from meerschaum.utils.dtypes import get_current_timestamp 737 738 now = get_current_timestamp('ms', as_int=True) / 1000 739 cache_seconds = ( 740 mrsm.get_config('pipes', 'static', 'static_schema_cache_seconds') 741 if self.static 742 else mrsm.get_config('pipes', 'dtypes', 'columns_types_cache_seconds') 743 ) 744 if refresh: 745 self._clear_cache_key('_columns_indices_timestamp', debug=debug) 746 self._clear_cache_key('_columns_indices', debug=debug) 747 748 _columns_indices = self._get_cached_value('_columns_indices', debug=debug) 749 750 if _columns_indices: 751 columns_indices_timestamp = self._get_cached_value('_columns_indices_timestamp', debug=debug) 752 if columns_indices_timestamp is not None: 753 delta = now - columns_indices_timestamp 754 if delta < cache_seconds: 755 if debug: 756 dprint( 757 f"Returning cached `columns_indices` for {self} " 758 f"({round(delta, 2)} seconds old)." 759 ) 760 return _columns_indices 761 762 with mrsm.Venv(get_connector_plugin(self.instance_connector)): 763 _columns_indices = ( 764 self.instance_connector.get_pipe_columns_indices(self, debug=debug) 765 if hasattr(self.instance_connector, 'get_pipe_columns_indices') 766 else None 767 ) 768 769 self._cache_value('_columns_indices', _columns_indices, debug=debug) 770 self._cache_value('_columns_indices_timestamp', now, debug=debug) 771 return {k: v for k, v in _columns_indices.items() if k and v} or {}
Return a dictionary mapping columns to index information.
1175def get_indices(self) -> Dict[str, str]: 1176 """ 1177 Return a dictionary mapping index keys to their names in the database. 1178 1179 Returns 1180 ------- 1181 A dictionary of index keys to index names. 1182 """ 1183 from meerschaum.connectors import get_connector_plugin 1184 with mrsm.Venv(get_connector_plugin(self.instance_connector)): 1185 if hasattr(self.instance_connector, 'get_pipe_index_names'): 1186 result = self.instance_connector.get_pipe_index_names(self) 1187 else: 1188 result = {} 1189 1190 return result
Return a dictionary mapping index keys to their names in the database.
Returns
- A dictionary of index keys to index names.
109def get_parameters( 110 self, 111 apply_symlinks: bool = True, 112 refresh: bool = False, 113 debug: bool = False, 114) -> Dict[str, Any]: 115 """ 116 Return the `parameters` dictionary of the pipe. 117 118 Parameters 119 ---------- 120 apply_symlinks: bool, default True 121 If `True`, resolve references to parameters from other pipes. 122 123 refresh: bool, default False 124 If `True`, pull the latest attributes for the pipe. 125 126 Returns 127 ------- 128 The pipe's parameters dictionary. 129 """ 130 from copy import deepcopy 131 from meerschaum import _core 132 133 if refresh: 134 _ = self._invalidate_cache(hard=True) 135 with self._attributes_lock: 136 self._clear_cache_key('_attributes_sync_time', debug=debug) 137 self.__dict__.pop('_target', None) 138 ### Drop any memoized resolution so a later non-refresh call recomputes from fresh state. 139 _ = self.__dict__.pop('_resolved_parameters_raw', None) 140 _ = self.__dict__.pop('_resolved_parameters', None) 141 _ = self.__dict__.pop('_resolved_parameters_symlinks', None) 142 143 raw_parameters = self.attributes.get('parameters', {}) 144 if not apply_symlinks: 145 return raw_parameters 146 147 # Resolving a self-table template asks the Python connector for this Pipe's target 148 # and schema. Those access parameters again; use the current raw snapshot for that 149 # nested metadata read rather than recursively re-entering the native resolver. 150 # Context-local keys also cover fresh wrappers for the same Pipe without affecting 151 # concurrent readers or caching an incompletely resolved result. 152 resolution_key = (self.instance_keys, self.connector_keys, self.metric_key, self.location_key) 153 resolution_stack = _PARAMETER_RESOLUTION_STACK.get() 154 if resolution_key in resolution_stack: 155 return deepcopy(raw_parameters) 156 157 ### Resolving references + `{{ Pipe() }}` / `MRSM{}` symlinks (in Rust) is expensive (it walks 158 ### reference pipes and may build connectors), and `get_parameters` is a hot path hit by 159 ### `.dtypes`, `.columns`, `.precision`, etc. Memoize the resolved result, keyed on the 160 ### mutation counter of the pipe's `_core.Pipe` (the Python view objects keep their identity 161 ### across mutations). Schema is *not* part of this: dynamic-schema freshness is handled 162 ### separately by `get_columns_types`' TTL cache, so this is safe for dynamic pipes. 163 can_memoize = not refresh 164 core = self._core_pipe 165 ### Cross-pipe `{{ Pipe() }}` templates read other pipes' state. v4 keeps them memoized 166 ### forever; here the memo is also keyed on this process's metadata writes (a hard 167 ### `_invalidate_cache()`, `register()`, or any `_core.SqlConnector` register / edit / delete, 168 ### including a sync's dtype edits) and expires after 169 ### `pipes:attributes:local_cache_timeout_seconds`, v4's own staleness window for attributes 170 ### (another process's edit of a referenced pipe shows up within it). Re-resolving on every 171 ### access cost one instance round trip per reference per access (wave 9 U). 172 cross_pipe = _has_cross_pipe_template(self, core, raw_parameters) 173 if cross_pipe: 174 from meerschaum.core.Pipe._cache import _INVALIDATION_EPOCH 175 memo_key = ( 176 core.generation, 177 _INVALIDATION_EPOCH[0], 178 _core.pipe_metadata_epoch(), 179 ) 180 else: 181 memo_key = core.generation 182 memo_hit = ( 183 '_resolved_parameters' in self.__dict__ 184 and self.__dict__.get('_resolved_parameters_raw', None) == memo_key 185 and (not cross_pipe or _cross_pipe_memo_is_fresh(self)) 186 ) 187 if can_memoize and memo_hit: 188 self._symlinks = self.__dict__.get('_resolved_parameters_symlinks', {}) 189 ### Return a copy so callers that mutate the result (e.g. `infer_dtypes(persist=True)`) 190 ### don't corrupt the memo. 191 return deepcopy(self.__dict__['_resolved_parameters']) 192 193 ### References, `{{ Pipe() }}` and `MRSM{}` (against the live config registry) resolve in 194 ### `meerschaum_core`; the Rust call also returns the `_symlinks` v4 records on the pipe. 195 resolution_token = _PARAMETER_RESOLUTION_STACK.set((*resolution_stack, resolution_key)) 196 try: 197 resolved_parameters, self._symlinks = _core.get_parameters(self) 198 finally: 199 _PARAMETER_RESOLUTION_STACK.reset(resolution_token) 200 201 if can_memoize: 202 ### Stash the symlinks captured above alongside the resolved result. 203 if cross_pipe: 204 import time 205 self.__dict__['_resolved_parameters_ts'] = time.monotonic() 206 self.__dict__['_resolved_parameters_raw'] = memo_key 207 self.__dict__['_resolved_parameters'] = resolved_parameters 208 self.__dict__['_resolved_parameters_symlinks'] = self._symlinks 209 return deepcopy(resolved_parameters) 210 211 return resolved_parameters
Return the parameters dictionary of the pipe.
Parameters
- apply_symlinks (bool, default True):
If
True, resolve references to parameters from other pipes. - refresh (bool, default False):
If
True, pull the latest attributes for the pipe.
Returns
- The pipe's parameters dictionary.
371def get_dtypes( 372 self, 373 infer: bool = True, 374 refresh: bool = False, 375 debug: bool = False, 376) -> Dict[str, Any]: 377 """ 378 If defined, return the `dtypes` dictionary defined in `meerschaum.Pipe.parameters`. 379 380 Parameters 381 ---------- 382 infer: bool, default True 383 If `True`, include the implicit existing dtypes. 384 Else only return the explicitly configured dtypes (e.g. `Pipe.parameters['dtypes']`). 385 386 refresh: bool, default False 387 If `True`, invalidate any cache and return the latest known dtypes. 388 389 Returns 390 ------- 391 A dictionary mapping column names to dtypes. 392 """ 393 from meerschaum.config._patch import apply_patch_to_config 394 from meerschaum.utils.dtypes import MRSM_ALIAS_DTYPES 395 parameters = self.get_parameters(refresh=refresh, debug=debug) 396 configured_dtypes = parameters.get('dtypes', {}) 397 if debug: 398 dprint(f"Configured dtypes for {self}:") 399 mrsm.pprint(configured_dtypes) 400 401 remote_dtypes = ( 402 self.infer_dtypes(persist=False, refresh=refresh, debug=debug) 403 if infer 404 else {} 405 ) 406 if debug and infer: 407 dprint(f"Remote dtypes for {self}:") 408 mrsm.pprint(remote_dtypes) 409 410 patched_dtypes = apply_patch_to_config((remote_dtypes or {}), (configured_dtypes or {})) 411 412 dt_col = parameters.get('columns', {}).get('datetime', None) 413 primary_col = parameters.get('columns', {}).get('primary', None) 414 _dtypes = { 415 col: MRSM_ALIAS_DTYPES.get(typ, typ) 416 for col, typ in patched_dtypes.items() 417 if col and typ 418 } 419 if dt_col and dt_col not in configured_dtypes: 420 _dtypes[dt_col] = 'datetime' 421 if primary_col and parameters.get('autoincrement', False) and primary_col not in _dtypes: 422 _dtypes[primary_col] = 'int' 423 424 return _dtypes
If defined, return the dtypes dictionary defined in meerschaum.Pipe.parameters.
Parameters
- infer (bool, default True):
If
True, include the implicit existing dtypes. Else only return the explicitly configured dtypes (e.g.Pipe.parameters['dtypes']). - refresh (bool, default False):
If
True, invalidate any cache and return the latest known dtypes.
Returns
- A dictionary mapping column names to dtypes.
1193def update_parameters( 1194 self, 1195 parameters_patch: Dict[str, Any], 1196 persist: bool = True, 1197 debug: bool = False, 1198) -> mrsm.SuccessTuple: 1199 """ 1200 Apply a patch to a pipe's `parameters` dictionary. 1201 1202 Parameters 1203 ---------- 1204 parameters_patch: Dict[str, Any] 1205 The patch to be applied to `Pipe.parameters`. 1206 1207 persist: bool, default True 1208 If `True`, call `Pipe.edit()` to persist the new parameters. 1209 """ 1210 from meerschaum.config import apply_patch_to_config 1211 if 'parameters' not in self._attributes: 1212 self._attributes['parameters'] = {} 1213 1214 self._attributes['parameters'] = apply_patch_to_config( 1215 self._attributes['parameters'], 1216 parameters_patch, 1217 ) 1218 1219 if self.temporary: 1220 persist = False 1221 1222 if not persist: 1223 return True, "Success" 1224 1225 return self.edit(debug=debug)
Apply a patch to a pipe's parameters dictionary.
Parameters
- parameters_patch (Dict[str, Any]):
The patch to be applied to
Pipe.parameters. - persist (bool, default True):
If
True, callPipe.edit()to persist the new parameters.
431def validate_indices(self, debug: bool = False) -> mrsm.SuccessTuple: 432 """ 433 Return whether this pipe's index columns can actually be used as indices. 434 435 A `json` column holds `dict` or `list` values. Syncing sorts and deduplicates 436 on the index columns in Pandas (`Series.unique()`, `DataFrame.sort_values()`), 437 and both raise `TypeError: unhashable type: 'dict'` on container values. The 438 failure surfaces on the *second* sync — the first one has nothing to diff 439 against — so it is worth reporting up front instead of at that point. 440 441 Note this is a client-side limitation, not a database one: on PostgreSQL the 442 `json` dtype is stored as `JSONB`, which does support B-tree, hash, and GIN 443 indices. Non-unique performance indices declared under `indices` are pure 444 DDL and are not affected; only the index *roles* in `columns` are. 445 446 Returns 447 ------- 448 A `SuccessTuple` of whether the index columns are valid. 449 """ 450 columns = self.columns or {} 451 if not columns: 452 return True, "Success" 453 454 dtypes = self.get_dtypes(infer=False, debug=debug) 455 offenders = { 456 col_ix: col 457 for col_ix, col in columns.items() 458 if col and str(dtypes.get(col, '')).lower() in UNHASHABLE_DTYPES 459 } 460 if not offenders: 461 return True, "Success" 462 463 listing = ', '.join( 464 f"'{col}' (as the '{col_ix}' index)" 465 for col_ix, col in sorted(offenders.items()) 466 ) 467 return False, ( 468 f"Cannot use a 'json' column as an index column: {listing}.\n" 469 " JSON values are dictionaries or lists, which cannot be sorted or " 470 "deduplicated when syncing.\n" 471 " Either give the column a scalar dtype (e.g. 'str'), or move it out of " 472 "`columns` and\n" 473 " declare it under `indices` if you only need a database index on it." 474 )
Return whether this pipe's index columns can actually be used as indices.
A json column holds dict or list values. Syncing sorts and deduplicates
on the index columns in Pandas (Series.unique(), DataFrame.sort_values()),
and both raise TypeError: unhashable type: 'dict' on container values. The
failure surfaces on the second sync, the first one has nothing to diff
against, so it is worth reporting up front instead of at that point.
Note this is a client-side limitation, not a database one: on PostgreSQL the
json dtype is stored as JSONB, which does support B-tree, hash, and GIN
indices. Non-unique performance indices declared under indices are pure
DDL and are not affected; only the index roles in columns are.
Returns
- A
SuccessTupleof whether the index columns are valid.
774def get_id(self, **kw: Any) -> Union[int, str, None]: 775 """ 776 Fetch a pipe's ID from its instance connector. 777 If the pipe is not registered, return `None`. 778 """ 779 if self.temporary: 780 return None 781 782 from meerschaum.utils.venv import Venv 783 from meerschaum.connectors import get_connector_plugin 784 785 with Venv(get_connector_plugin(self.instance_connector)): 786 if hasattr(self.instance_connector, 'get_pipe_id'): 787 return self.instance_connector.get_pipe_id(self, **kw) 788 789 return None
Fetch a pipe's ID from its instance connector.
If the pipe is not registered, return None.
792@property 793def id(self) -> Union[int, str, uuid.UUID, None]: 794 """ 795 Fetch and cache a pipe's ID. 796 """ 797 _id = self._get_cached_value('_id', debug=self.debug) 798 if _id is None: 799 _id = self.get_id(debug=self.debug) 800 if _id is not None: 801 self._cache_value('_id', _id, debug=self.debug) 802 return _id
Fetch and cache a pipe's ID.
805def get_val_column(self, debug: bool = False) -> Union[str, None]: 806 """ 807 Return the name of the value column if it's defined, otherwise make an educated guess. 808 If not set in the `columns` dictionary, return the first numeric column that is not 809 an ID or datetime column. 810 If none may be found, return `None`. 811 812 Parameters 813 ---------- 814 debug: bool, default False: 815 Verbosity toggle. 816 817 Returns 818 ------- 819 Either a string or `None`. 820 """ 821 if debug: 822 dprint('Attempting to determine the value column...') 823 try: 824 val_name = self.get_columns('value') 825 except Exception: 826 val_name = None 827 if val_name is not None: 828 if debug: 829 dprint(f"Value column: {val_name}") 830 return val_name 831 832 cols = self.columns 833 if cols is None: 834 if debug: 835 dprint('No columns could be determined. Returning...') 836 return None 837 try: 838 dt_name = self.get_columns('datetime', error=False) 839 except Exception: 840 dt_name = None 841 try: 842 id_name = self.get_columns('id', errors=False) 843 except Exception: 844 id_name = None 845 846 if debug: 847 dprint(f"dt_name: {dt_name}") 848 dprint(f"id_name: {id_name}") 849 850 cols_types = self.get_columns_types(debug=debug) 851 if cols_types is None: 852 return None 853 if debug: 854 dprint(f"cols_types: {cols_types}") 855 if dt_name is not None: 856 cols_types.pop(dt_name, None) 857 if id_name is not None: 858 cols_types.pop(id_name, None) 859 860 candidates = [] 861 candidate_keywords = {'float', 'double', 'precision', 'int', 'numeric',} 862 for search_term in candidate_keywords: 863 for col, typ in cols_types.items(): 864 if search_term in typ.lower(): 865 candidates.append(col) 866 break 867 if not candidates: 868 if debug: 869 dprint("No value column could be determined.") 870 return None 871 872 return candidates[0]
Return the name of the value column if it's defined, otherwise make an educated guess.
If not set in the columns dictionary, return the first numeric column that is not
an ID or datetime column.
If none may be found, return None.
Parameters
- debug (bool, default False:): Verbosity toggle.
Returns
- Either a string or
None.
875@property 876def parents(self) -> List[mrsm.Pipe]: 877 """ 878 Return a list of `meerschaum.Pipe` objects to be designated as parents. 879 """ 880 _cached_parents = self.__dict__.get('_parents', None) 881 if _cached_parents is not None: 882 return _cached_parents 883 884 from meerschaum.utils.pipes import get_pipe_from_value 885 base_params = self.get_parameters() 886 key = 'parents' if 'parents' in base_params else 'parent' 887 parents_refs = base_params.get(key, None) or [] 888 if isinstance(parents_refs, str) or isinstance(parents_refs, dict): 889 parents_refs = [parents_refs] 890 891 if not parents_refs: 892 return [] 893 894 self._parents = [get_pipe_from_value(val, _pipe=self) for val in parents_refs] 895 return self._parents
Return a list of meerschaum.Pipe objects to be designated as parents.
898@property 899def parent(self) -> Union[mrsm.Pipe, None]: 900 """ 901 Return the first pipe in `self.parents` or `None`. 902 """ 903 _parents = self.parents 904 if not _parents: 905 return None 906 907 return _parents[0]
Return the first pipe in self.parents or None.
943@property 944def children(self) -> List[mrsm.Pipe]: 945 """ 946 Return a list of `meerschaum.Pipe` objects to be designated as children. 947 """ 948 _cached_children = self.__dict__.get('_children', None) 949 if _cached_children is not None: 950 return _cached_children 951 952 from meerschaum.utils.pipes import get_pipe_from_value 953 base_params = self.get_parameters() 954 key = 'children' if 'children' in base_params else 'child' 955 children_refs = base_params.get(key, None) or [] 956 if isinstance(children_refs, str) or isinstance(children_refs, dict): 957 children_refs = [children_refs] 958 959 if not children_refs: 960 return [] 961 962 self._children = [get_pipe_from_value(val, _pipe=self) for val in children_refs] 963 return self._children
Return a list of meerschaum.Pipe objects to be designated as children.
966@property 967def child(self) -> mrsm.Pipe | None: 968 """ 969 Return the first pipe in `self.children` or None. 970 """ 971 _children = self.children 972 if not _children: 973 return None 974 975 return _children[0]
Return the first pipe in self.children or None.
1035@property 1036def reference(self) -> mrsm.Pipe | None: 1037 """ 1038 Return the first pipe in `self.references` or None. 1039 """ 1040 _references = self.references 1041 if not _references: 1042 return None 1043 1044 return _references[0]
Return the first pipe in self.references or None.
1012@property 1013def references(self) -> List[mrsm.Pipe]: 1014 """ 1015 Return a list of `meerschaum.Pipe` objects to be designated as references. 1016 """ 1017 _cached_references = self.__dict__.get('_references', None) 1018 if _cached_references is not None: 1019 return _cached_references 1020 1021 from meerschaum.utils.pipes import get_pipe_from_value 1022 base_params = self.get_parameters(apply_symlinks=False) 1023 key = 'references' if 'references' in base_params else 'reference' 1024 refs = base_params.get(key, None) or [] 1025 if isinstance(refs, str) or isinstance(refs, dict): 1026 refs = [refs] 1027 1028 if not refs: 1029 return [] 1030 1031 self._references = [get_pipe_from_value(val, _pipe=self) for val in refs] 1032 return self._references
Return a list of meerschaum.Pipe objects to be designated as references.
1082@property 1083def target(self) -> str: 1084 """ 1085 The target table name. 1086 Registered pipes do not memoize guessed defaults while metadata failures are outstanding. 1087 You can set the target name under on of the following keys 1088 (checked in this order): 1089 - `target` 1090 - `target_name` 1091 - `target_table` 1092 - `target_table_name` 1093 """ 1094 cached_target = self.__dict__.get('_target', None) 1095 if cached_target: 1096 return cached_target 1097 1098 load_failures = self.__dict__.get('_attributes_load_failures', 0) 1099 params = self.parameters 1100 target_val = params.get('target', None) 1101 if target_val: 1102 self.__dict__['_target'] = target_val 1103 return target_val 1104 1105 core = self._core_pipe 1106 default_target = self._target_legacy() 1107 default_targets = {default_target} 1108 ### The Rust name rules (`target_name` / `target_table` / ... keys, the legacy default), 1109 ### over the parameters resolved here (so cross-pipe templates are honored). 1110 _target = core.target(None, dict(params)) 1111 1112 if self.instance_connector.type == 'sql': 1113 truncated_target = core.target(self.instance_connector.flavor, dict(params)) 1114 default_targets.add(truncated_target) 1115 warned_target = self.__dict__.get('_warned_target', False) 1116 if truncated_target != _target and not warned_target: 1117 if self.instance_connector.flavor not in ('oracle', 'mysql', 'mariadb'): 1118 warn( 1119 f"The target '{_target}' is too long for '{self.instance_connector.flavor}', " 1120 + f"will use {truncated_target} instead." 1121 ) 1122 self.__dict__['_warned_target'] = True 1123 _target = truncated_target 1124 1125 if _target not in default_targets: 1126 self.target = _target 1127 1128 # Parameter resolution may retry a failed load; its first snapshot is still stale. 1129 resolved_cleanly = load_failures == self.__dict__.get('_attributes_load_failures', 0) 1130 if (params and resolved_cleanly) or self.temporary or self.id is None: 1131 self.__dict__['_target'] = _target 1132 return _target
The target table name. Registered pipes do not memoize guessed defaults while metadata failures are outstanding. You can set the target name under on of the following keys (checked in this order):
targettarget_nametarget_tabletarget_table_name
1153def guess_datetime(self) -> Union[str, None]: 1154 """ 1155 Try to determine a pipe's datetime column. 1156 """ 1157 _dtypes = self.dtypes 1158 1159 ### Abort if the user explictly disallows a datetime index. 1160 if 'datetime' in _dtypes: 1161 if _dtypes['datetime'] is None: 1162 return None 1163 1164 from meerschaum.utils.dtypes import are_dtypes_equal 1165 dt_cols = [ 1166 col 1167 for col, typ in _dtypes.items() 1168 if are_dtypes_equal(typ, 'datetime') 1169 ] 1170 if not dt_cols: 1171 return None 1172 return dt_cols[0]
Try to determine a pipe's datetime column.
1317@property 1318def precision(self) -> Dict[str, Union[str, int]]: 1319 """ 1320 Return the configured or detected precision. 1321 """ 1322 return self.get_precision(debug=self.debug)
Return the configured or detected precision.
1228def get_precision(self, debug: bool = False) -> Dict[str, Union[str, int]]: 1229 """ 1230 Return the timestamp precision unit and interval for the `datetime` axis. 1231 """ 1232 from meerschaum.utils.dtypes import ( 1233 MRSM_PRECISION_UNITS_SCALARS, 1234 MRSM_PRECISION_UNITS_ALIASES, 1235 MRSM_PD_DTYPES, 1236 are_dtypes_equal, 1237 ) 1238 from meerschaum._internal.static import STATIC_CONFIG 1239 1240 _precision = self._get_cached_value('precision', debug=debug) 1241 if _precision: 1242 if debug: 1243 dprint(f"Returning cached precision: {_precision}") 1244 return _precision 1245 1246 parameters = self.parameters 1247 _precision = parameters.get('precision', {}) 1248 if isinstance(_precision, str): 1249 _precision = {'unit': _precision} 1250 default_precision_unit = STATIC_CONFIG['dtypes']['datetime']['default_precision_unit'] 1251 1252 if not _precision: 1253 1254 dt_col = parameters.get('columns', {}).get('datetime', None) 1255 if not dt_col and self.autotime: 1256 dt_col = mrsm.get_config('pipes', 'autotime', 'column_name_if_datetime_missing') 1257 if not dt_col: 1258 if debug: 1259 dprint(f"No datetime axis, returning default precision '{default_precision_unit}'.") 1260 return {'unit': default_precision_unit} 1261 1262 dt_typ = self.dtypes.get(dt_col, 'datetime') 1263 if are_dtypes_equal(dt_typ, 'datetime'): 1264 if dt_typ == 'datetime': 1265 dt_typ = MRSM_PD_DTYPES['datetime'] 1266 if debug: 1267 dprint(f"Datetime type is `datetime`, assuming {dt_typ} precision.") 1268 1269 _precision = { 1270 'unit': ( 1271 dt_typ 1272 .split('[', maxsplit=1)[-1] 1273 .split(',', maxsplit=1)[0] 1274 .split(' ', maxsplit=1)[0] 1275 ).rstrip(']') 1276 } 1277 1278 if debug: 1279 dprint(f"Extracted precision '{_precision['unit']}' from type '{dt_typ}'.") 1280 1281 elif are_dtypes_equal(dt_typ, 'int'): 1282 _precision = { 1283 'unit': ( 1284 'second' 1285 if '32' in dt_typ 1286 else default_precision_unit 1287 ) 1288 } 1289 elif are_dtypes_equal(dt_typ, 'date'): 1290 if debug: 1291 dprint("Datetime axis is 'date', falling back to 'day' precision.") 1292 _precision = {'unit': 'day'} 1293 1294 precision_unit = _precision.get('unit', default_precision_unit) 1295 precision_interval = _precision.get('interval', None) 1296 true_precision_unit = MRSM_PRECISION_UNITS_ALIASES.get(precision_unit, precision_unit) 1297 if true_precision_unit is None: 1298 if debug: 1299 dprint(f"No precision could be determined, falling back to '{default_precision_unit}'.") 1300 true_precision_unit = default_precision_unit 1301 1302 if true_precision_unit not in MRSM_PRECISION_UNITS_SCALARS: 1303 from meerschaum.utils.misc import items_str 1304 raise ValueError( 1305 f"Invalid precision unit '{true_precision_unit}'.\n" 1306 "Accepted values are " 1307 f"{items_str(list(MRSM_PRECISION_UNITS_SCALARS) + list(MRSM_PRECISION_UNITS_ALIASES))}." 1308 ) 1309 1310 _precision = {'unit': true_precision_unit} 1311 if precision_interval: 1312 _precision['interval'] = precision_interval 1313 self._cache_value('precision', _precision, debug=debug) 1314 return self._precision
Return the timestamp precision unit and interval for the datetime axis.
12def show( 13 self, 14 nopretty: bool = False, 15 debug: bool = False, 16 **kw 17) -> SuccessTuple: 18 """ 19 Show attributes of a Pipe. 20 21 Parameters 22 ---------- 23 nopretty: bool, default False 24 If `True`, simply print the JSON of the pipe's attributes. 25 26 debug: bool, default False 27 Verbosity toggle. 28 29 Returns 30 ------- 31 A `SuccessTuple` of success, message. 32 33 """ 34 import json 35 from meerschaum.utils.formatting import ( 36 pprint, make_header, ANSI, highlight_pipes, fill_ansi, get_console, 37 ) 38 from meerschaum.utils.packages import import_rich, attempt_import 39 from meerschaum.utils.warnings import info 40 attributes_json = json.dumps(self.attributes) 41 if not nopretty: 42 _to_print = f"Attributes for {self}:" 43 if ANSI: 44 _to_print = fill_ansi(highlight_pipes(make_header(_to_print)), 'magenta') 45 print(_to_print) 46 rich = import_rich() 47 rich_json = attempt_import('rich.json') 48 get_console().print(rich_json.JSON(attributes_json)) 49 else: 50 print(_to_print) 51 else: 52 print(attributes_json) 53 54 return True, "Success"
Show attributes of a Pipe.
Parameters
- nopretty (bool, default False):
If
True, simply print the JSON of the pipe's attributes. - debug (bool, default False): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
21def edit( 22 self, 23 patch: bool = False, 24 interactive: bool = False, 25 debug: bool = False, 26 **kw: Any 27) -> SuccessTuple: 28 """ 29 Edit a Pipe's configuration. 30 31 Parameters 32 ---------- 33 patch: bool, default False 34 If `patch` is True, update parameters by cascading rather than overwriting. 35 interactive: bool, default False 36 If `True`, open an editor for the user to make changes to the pipe's YAML file. 37 debug: bool, default False 38 Verbosity toggle. 39 40 Returns 41 ------- 42 A `SuccessTuple` of success, message. 43 44 """ 45 from meerschaum.utils.venv import Venv 46 from meerschaum.connectors import get_connector_plugin 47 48 if self.temporary: 49 return False, "Cannot edit pipes created with `temporary=True` (read-only)." 50 51 self._invalidate_cache(hard=True, debug=debug) 52 53 if hasattr(self, '_symlinks'): 54 from meerschaum.utils.misc import get_val_from_dict_path, set_val_in_dict_path 55 for path, vals in self._symlinks.items(): 56 current_val = get_val_from_dict_path(self.parameters, path) 57 if current_val == vals['substituted']: 58 set_val_in_dict_path(self.parameters, path, vals['original']) 59 60 if not interactive: 61 ### Refuse an index column which cannot be sorted or deduplicated. 62 indices_success, indices_msg = self.validate_indices(debug=debug) 63 if not indices_success: 64 return indices_success, indices_msg 65 66 ### A `parameters` lock allows changing `locks` only (the core checks its own instances). 67 refusal = self._edit_lock_refusal(patch=patch, debug=debug) 68 if refusal: 69 return False, refusal 70 71 with Venv(get_connector_plugin(self.instance_connector)): 72 return self.instance_connector.edit_pipe(self, patch=patch, debug=debug, **kw) 73 74 import meerschaum.config.paths as paths 75 from meerschaum.utils.misc import edit_file 76 parameters_filename = str(self) + '.yaml' 77 parameters_path = paths.PIPES_CACHE_RESOURCES_PATH / parameters_filename 78 79 from meerschaum.utils.yaml import yaml 80 81 edit_text = f"Edit the parameters for {self}" 82 edit_top = '#' * (len(edit_text) + 4) 83 edit_header = edit_top + f'\n# {edit_text} #\n' + edit_top + '\n\n' 84 85 from meerschaum.config import get_config 86 parameters = dict(get_config('pipes', 'parameters', patch=True)) 87 from meerschaum.config._patch import apply_patch_to_config 88 raw_parameters = self.attributes.get('parameters', {}) 89 parameters = apply_patch_to_config(parameters, raw_parameters) 90 91 ### write parameters to yaml file 92 with open(parameters_path, 'w+') as f: 93 f.write(edit_header) 94 yaml.dump(parameters, stream=f, sort_keys=False) 95 96 ### only quit editing if yaml is valid 97 editing = True 98 while editing: 99 edit_file(parameters_path) 100 try: 101 with open(parameters_path, 'r') as f: 102 file_parameters = yaml.load(f.read()) 103 except Exception as e: 104 from meerschaum.utils.warnings import warn 105 warn(f"Invalid format defined for '{self}':\n\n{e}") 106 input(f"Press [Enter] to correct the configuration for '{self}': ") 107 else: 108 editing = False 109 110 self.parameters = file_parameters 111 112 if debug: 113 from meerschaum.utils.formatting import pprint 114 pprint(self.parameters) 115 116 refusal = self._edit_lock_refusal(patch=patch, debug=debug) 117 if refusal: 118 return False, refusal 119 120 with Venv(get_connector_plugin(self.instance_connector)): 121 return self.instance_connector.edit_pipe(self, patch=patch, debug=debug, **kw)
Edit a Pipe's configuration.
Parameters
- patch (bool, default False):
If
patchis True, update parameters by cascading rather than overwriting. - interactive (bool, default False):
If
True, open an editor for the user to make changes to the pipe's YAML file. - debug (bool, default False): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
124def edit_definition( 125 self, 126 yes: bool = False, 127 noask: bool = False, 128 force: bool = False, 129 debug : bool = False, 130 **kw : Any 131) -> SuccessTuple: 132 """ 133 Edit a pipe's definition file and update its configuration. 134 **NOTE:** This function is interactive and should not be used in automated scripts! 135 136 Returns 137 ------- 138 A `SuccessTuple` of success, message. 139 140 """ 141 if self.temporary: 142 return False, "Cannot edit pipes created with `temporary=True` (read-only)." 143 144 from meerschaum.connectors import instance_types 145 if (self.connector is None or isinstance(self.connector, str)) or self.connector.type not in instance_types: 146 return self.edit(interactive=True, debug=debug, **kw) 147 148 import json 149 from meerschaum.utils.warnings import info, warn 150 from meerschaum.utils.debug import dprint 151 from meerschaum.config._patch import apply_patch_to_config 152 from meerschaum.utils.misc import edit_file 153 154 _parameters = self.parameters 155 if 'fetch' not in _parameters: 156 _parameters['fetch'] = {} 157 158 def _edit_api(): 159 from meerschaum.utils.prompt import prompt, yes_no 160 info( 161 f"Please enter the keys of the source pipe from '{self.connector}'.\n" + 162 "Type 'None' for None, or empty when there is no default. Press [CTRL+C] to skip." 163 ) 164 165 _keys = { 'connector_keys' : None, 'metric_key' : None, 'location_key' : None } 166 for k in _keys: 167 _keys[k] = _parameters['fetch'].get(k, None) 168 169 for k, v in _keys.items(): 170 try: 171 _keys[k] = prompt(k.capitalize().replace('_', ' ') + ':', icon=True, default=v) 172 except KeyboardInterrupt: 173 continue 174 if _keys[k] in ('', 'None', '\'None\'', '[None]'): 175 _keys[k] = None 176 177 _parameters['fetch'] = apply_patch_to_config(_parameters['fetch'], _keys) 178 179 info("You may optionally specify additional filter parameters as JSON.") 180 print(" Parameters are translated into a 'WHERE x AND y' clause, and lists are IN clauses.") 181 print(" For example, the following JSON would correspond to 'WHERE x = 1 AND y IN (2, 3)':") 182 print(json.dumps({'x': 1, 'y': [2, 3]}, indent=2, separators=(',', ': '))) 183 if force or yes_no( 184 "Would you like to add additional filter parameters?", 185 yes=yes, noask=noask 186 ): 187 import meerschaum.config.paths as paths 188 definition_filename = str(self) + '.json' 189 definition_path = paths.PIPES_CACHE_RESOURCES_PATH / definition_filename 190 try: 191 definition_path.touch() 192 with open(definition_path, 'w+') as f: 193 json.dump(_parameters.get('fetch', {}).get('params', {}), f, indent=2) 194 except Exception as e: 195 return False, f"Failed writing file '{definition_path}':\n" + str(e) 196 197 _params = None 198 while True: 199 edit_file(definition_path) 200 try: 201 with open(definition_path, 'r') as f: 202 _params = json.load(f) 203 except Exception as e: 204 warn(f'Failed to read parameters JSON:\n{e}', stack=False) 205 if force or yes_no( 206 "Would you like to try again?\n " 207 + "If not, the parameters JSON file will be ignored.", 208 noask=noask, yes=yes 209 ): 210 continue 211 _params = None 212 break 213 if _params is not None: 214 if 'fetch' not in _parameters: 215 _parameters['fetch'] = {} 216 _parameters['fetch']['params'] = _params 217 218 self.parameters = _parameters 219 return True, "Success" 220 221 def _edit_sql(): 222 import textwrap 223 import meerschaum.config.paths as paths 224 from meerschaum.utils.misc import edit_file 225 definition_filename = str(self) + '.sql' 226 definition_path = paths.PIPES_CACHE_RESOURCES_PATH / definition_filename 227 228 sql_definition = _parameters['fetch'].get('definition', None) 229 if sql_definition is None: 230 sql_definition = '' 231 sql_definition = textwrap.dedent(sql_definition).lstrip() 232 233 try: 234 definition_path.touch() 235 with open(definition_path, 'w+') as f: 236 f.write(sql_definition) 237 except Exception as e: 238 return False, f"Failed writing file '{definition_path}':\n" + str(e) 239 240 edit_file(definition_path) 241 try: 242 with open(definition_path, 'r', encoding='utf-8') as f: 243 file_definition = f.read() 244 except Exception as e: 245 return False, f"Failed reading file '{definition_path}':\n" + str(e) 246 247 if sql_definition == file_definition: 248 return False, f"No changes made to definition for {self}." 249 250 if ' ' not in file_definition: 251 return False, f"Invalid SQL definition for {self}." 252 253 if debug: 254 dprint("Read SQL definition:\n\n" + file_definition) 255 _parameters['fetch']['definition'] = file_definition 256 self.parameters = _parameters 257 return True, "Success" 258 259 locals()['_edit_' + str(self.connector.type)]() 260 return self.edit(interactive=False, debug=debug, **kw)
Edit a pipe's definition file and update its configuration. NOTE: This function is interactive and should not be used in automated scripts!
Returns
- A
SuccessTupleof success, message.
13def update(self, *args, **kw) -> SuccessTuple: 14 """ 15 Update a pipe's parameters in its instance. 16 """ 17 kw['interactive'] = False 18 return self.edit(*args, **kw)
Update a pipe's parameters in its instance.
736def sync( 737 self, 738 df: Union[ 739 pd.DataFrame, 740 Dict[str, List[Any]], 741 List[Dict[str, Any]], 742 str, 743 InferFetch 744 ] = InferFetch, 745 begin: Union[datetime, int, str, None] = '', 746 end: Union[datetime, int, None] = None, 747 force: bool = False, 748 retries: int = 10, 749 min_seconds: int = 1, 750 check_existing: bool = True, 751 enforce_dtypes: bool = True, 752 blocking: bool = True, 753 workers: Optional[int] = None, 754 callback: Optional[Callable[[Tuple[bool, str]], Any]] = None, 755 error_callback: Optional[Callable[[Exception], Any]] = None, 756 chunksize: Optional[int] = -1, 757 sync_chunks: bool = True, 758 parents: Union[bool, int, None] = None, 759 children: Union[bool, int, None] = None, 760 replace: bool = False, 761 debug: bool = False, 762 _inplace: bool = True, 763 **kw: Any 764) -> SuccessTuple: 765 """ 766 Fetch new data from the source and update the pipe's table with new data. 767 768 Get new remote data via fetch, get existing data in the same time period, 769 and merge the two, only keeping the unseen data. 770 771 Parameters 772 ---------- 773 df: Union[None, pd.DataFrame, Dict[str, List[Any]]], default None 774 An optional DataFrame to sync into the pipe. Defaults to `None`. 775 If `df` is a string, it will be parsed via `meerschaum.utils.dataframe.parse_simple_lines()`. 776 777 begin: Union[datetime, int, str, None], default '' 778 Optionally specify the earliest datetime to search for data. 779 780 end: Union[datetime, int, str, None], default None 781 Optionally specify the latest datetime to search for data. 782 783 force: bool, default False 784 If `True`, keep trying to sync untul `retries` attempts. 785 786 retries: int, default 10 787 If `force`, how many attempts to try syncing before declaring failure. 788 789 min_seconds: Union[int, float], default 1 790 If `force`, how many seconds to sleep between retries. Defaults to `1`. 791 792 check_existing: bool, default True 793 If `True`, pull and diff with existing data from the pipe. 794 795 enforce_dtypes: bool, default True 796 If `True`, enforce dtypes on incoming data. 797 Set this to `False` if the incoming rows are expected to be of the correct dtypes. 798 799 blocking: bool, default True 800 If `True`, wait for sync to finish and return its result, otherwise 801 asyncronously sync (oxymoron?) and return success. Defaults to `True`. 802 Only intended for specific scenarios. 803 804 workers: Optional[int], default None 805 If provided and the instance connector is thread-safe 806 (`pipe.instance_connector.IS_THREAD_SAFE is True`), 807 limit concurrent sync to this many threads. 808 809 callback: Optional[Callable[[Tuple[bool, str]], Any]], default None 810 Callback function which expects a SuccessTuple as input. 811 Only applies when `blocking=False`. 812 813 error_callback: Optional[Callable[[Exception], Any]], default None 814 Callback function which expects an Exception as input. 815 Only applies when `blocking=False`. 816 817 chunksize: int, default -1 818 Specify the number of rows to sync per chunk. 819 If `-1`, resort to system configuration (default is `900`). 820 A `chunksize` of `None` will sync all rows in one transaction. 821 822 sync_chunks: bool, default True 823 If possible, sync chunks while fetching them into memory. 824 825 parents: Union[bool, int, None], default None 826 If `True`, sync every ancestor of this pipe first, in dependency order (the implicit 827 pipe graph, including `{{ Pipe() }}` references). An `int` syncs that many generations 828 (`parents=1` is the direct parents). Runs through the Rust Syncing Engine, which skips 829 parents and children that are already caught up. A bare `sync()` (the default) never 830 uses the engine and behaves exactly like v4. 831 832 children: Union[bool, int, None], default None 833 If `True`, sync this pipe, then propagate to every descendant. An `int` limits the 834 generations. Combine with `parents` for the whole connected slice. 835 See `meerschaum.Pipeline` to plan the run first and read the run history. 836 837 replace: bool, default False 838 If `True`, the rows you sync replace the pipe's rows in `[begin, end)` instead of being 839 merged into them: in one transaction, the rows of that window (matching `params`, if 840 given) are deleted and the new rows inserted, so rows missing from the new data are 841 removed. When you pass data, `begin` defaults to the earliest datetime in the data and 842 `end` to just after the latest one, and rows outside `[begin, end)` are not written 843 (the message says how many were skipped). An empty frame with `begin` and `end` clears 844 the window. Without data (a fetch), the window is the fetch's own `[begin, end)`; set 845 `fetch: {replace: true}` in the parameters to do this on every sync. A pipe without a 846 datetime column has one window: the whole table. Valkey instances clear the window 847 then write the rows (no transaction). 848 849 on_event: Optional[Callable[[Dict[str, Any]], Any]], default None 850 Only with `parents` or `children`: called on this thread with every event of the engine 851 run as it happens (`run_started`, `node_started`, `chunk_finished`, `node_finished`, 852 ..., `run_finished`), each a `dict` with `event`, `run_id`, `seq`, `ts`, `pipe_ref` and 853 the event's own fields. An exception it raises is warned about and the run continues 854 (see `meerschaum.Pipeline.sync`). 855 856 debug: bool, default False 857 Verbosity toggle. Defaults to False. 858 859 Returns 860 ------- 861 A `SyncResult`: a `SuccessTuple` of success (`bool`) and message (`str`) that also carries 862 the structured fields of the sync (`inserted`, `updated`, `upserted`, `deleted`, 863 `changed_intervals`, `duration`, `begin` and `end`; `None` where the sync did not 864 report them). 865 With `parents` or `children`, a `meerschaum.core.Pipeline.RunResult` (a `SuccessTuple` 866 with the run's `run_id`, `plan`, `outcomes` and `events()`). 867 """ 868 if replace: 869 kw['replace'] = True 870 if parents or children: 871 from meerschaum.core.Pipeline import sync_with_graph 872 return sync_with_graph( 873 self, 874 df, 875 parents=parents, 876 children=children, 877 begin=begin, 878 end=end, 879 force=force, 880 retries=retries, 881 min_seconds=min_seconds, 882 check_existing=check_existing, 883 enforce_dtypes=enforce_dtypes, 884 workers=workers, 885 chunksize=chunksize, 886 debug=debug, 887 **kw 888 ) 889 890 from meerschaum import _core 891 return _sync_native( 892 self, 893 _core.sync_pipe, 894 df, 895 begin=begin, 896 end=end, 897 check_existing=check_existing, 898 enforce_dtypes=enforce_dtypes, 899 chunksize=chunksize, 900 sync_chunks=sync_chunks, 901 force=force, 902 retries=retries, 903 min_seconds=min_seconds, 904 blocking=blocking, 905 workers=workers, 906 callback=callback, 907 error_callback=error_callback, 908 debug=debug, 909 inplace=( 910 _inplace 911 and bool(mrsm.get_config('system', 'experimental', 'inplace_sync')) 912 ), 913 kw=kw, 914 replace=bool(replace), 915 )
Fetch new data from the source and update the pipe's table with new data.
Get new remote data via fetch, get existing data in the same time period, and merge the two, only keeping the unseen data.
Parameters
- df (Union[None, pd.DataFrame, Dict[str, List[Any]]], default None):
An optional DataFrame to sync into the pipe. Defaults to
None. Ifdfis a string, it will be parsed viameerschaum.utils.dataframe.parse_simple_lines(). - begin (Union[datetime, int, str, None], default ''): Optionally specify the earliest datetime to search for data.
- end (Union[datetime, int, str, None], default None): Optionally specify the latest datetime to search for data.
- force (bool, default False):
If
True, keep trying to sync untulretriesattempts. - retries (int, default 10):
If
force, how many attempts to try syncing before declaring failure. - min_seconds (Union[int, float], default 1):
If
force, how many seconds to sleep between retries. Defaults to1. - check_existing (bool, default True):
If
True, pull and diff with existing data from the pipe. - enforce_dtypes (bool, default True):
If
True, enforce dtypes on incoming data. Set this toFalseif the incoming rows are expected to be of the correct dtypes. - blocking (bool, default True):
If
True, wait for sync to finish and return its result, otherwise asyncronously sync (oxymoron?) and return success. Defaults toTrue. Only intended for specific scenarios. - workers (Optional[int], default None):
If provided and the instance connector is thread-safe
(
pipe.instance_connector.IS_THREAD_SAFE is True), limit concurrent sync to this many threads. - callback (Optional[Callable[[Tuple[bool, str]], Any]], default None):
Callback function which expects a SuccessTuple as input.
Only applies when
blocking=False. - error_callback (Optional[Callable[[Exception], Any]], default None):
Callback function which expects an Exception as input.
Only applies when
blocking=False. - chunksize (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. - sync_chunks (bool, default True): If possible, sync chunks while fetching them into memory.
- parents (Union[bool, int, None], default None):
If
True, sync every ancestor of this pipe first, in dependency order (the implicit pipe graph, including{{ Pipe() }}references). Anintsyncs that many generations (parents=1is the direct parents). Runs through the Rust Syncing Engine, which skips parents and children that are already caught up. A baresync()(the default) never uses the engine and behaves exactly like v4. - children (Union[bool, int, None], default None):
If
True, sync this pipe, then propagate to every descendant. Anintlimits the generations. Combine withparentsfor the whole connected slice. Seemeerschaum.Pipelineto plan the run first and read the run history. - replace (bool, default False):
If
True, the rows you sync replace the pipe's rows in[begin, end)instead of being merged into them: in one transaction, the rows of that window (matchingparams, if given) are deleted and the new rows inserted, so rows missing from the new data are removed. When you pass data,begindefaults to the earliest datetime in the data andendto just after the latest one, and rows outside[begin, end)are not written (the message says how many were skipped). An empty frame withbeginandendclears the window. Without data (a fetch), the window is the fetch's own[begin, end); setfetch: {replace: true}in the parameters to do this on every sync. A pipe without a datetime column has one window: the whole table. Valkey instances clear the window then write the rows (no transaction). - on_event (Optional[Callable[[Dict[str, Any]], Any]], default None):
Only with
parentsorchildren: called on this thread with every event of the engine run as it happens (run_started,node_started,chunk_finished,node_finished, ...,run_finished), each adictwithevent,run_id,seq,ts,pipe_refand the event's own fields. An exception it raises is warned about and the run continues (seemeerschaum.Pipeline.sync). - debug (bool, default False): Verbosity toggle. Defaults to False.
Returns
A
SyncResult(aSuccessTupleof success (bool) and message (str) that also carries):the structured fields of the sync (
inserted,updated,upserted,deleted,changed_intervals,duration,beginandend;Nonewhere the sync did not- report them).
- With
parentsorchildren, ameerschaum.core.Pipeline.RunResult(aSuccessTuple - with the run's
run_id,plan,outcomesandevents()).
918def get_sync_time( 919 self, 920 params: Optional[Dict[str, Any]] = None, 921 newest: bool = True, 922 apply_backtrack_interval: bool = False, 923 remote: bool = False, 924 round_down: bool = False, 925 silent: bool = False, 926 debug: bool = False 927) -> Union['datetime', int, None]: 928 """ 929 Get the most recent datetime value for a Pipe. 930 931 Parameters 932 ---------- 933 params: Optional[Dict[str, Any]], default None 934 Dictionary to build a WHERE clause for a specific column. 935 See `meerschaum.utils.sql.build_where`. 936 937 newest: bool, default True 938 If `True`, get the most recent datetime (honoring `params`). 939 If `False`, get the oldest datetime (`ASC` instead of `DESC`). 940 941 apply_backtrack_interval: bool, default False 942 If `True`, subtract the backtrack interval from the sync time. 943 944 remote: bool, default False 945 If `True` and the instance connector supports it, return the sync time 946 for the remote table definition. 947 948 round_down: bool, default False 949 If `True`, round down the datetime value to the nearest minute. 950 951 silent: bool, default False 952 If `True`, return `None` instead of raising when the database cannot be read 953 (Meerschaum 4's behavior). 954 955 debug: bool, default False 956 Verbosity toggle. 957 958 Returns 959 ------- 960 A `datetime` or int, if the pipe exists, otherwise `None` (a pipe that was never synced 961 has no sync time). Raises the database error if the instance cannot be read. 962 963 """ 964 from meerschaum import _core 965 from meerschaum.core.Pipe._data import _rust_context 966 try: 967 context = _rust_context(self, debug=debug) 968 return _core.pipe_get_sync_time( 969 self, 970 context, 971 params=params, 972 newest=newest, 973 apply_backtrack_interval=apply_backtrack_interval, 974 remote=remote, 975 round_down=round_down, 976 debug=debug, 977 ) 978 except Exception: 979 if silent: 980 return None 981 raise
Get the most recent datetime value for a Pipe.
Parameters
- params (Optional[Dict[str, Any]], default None):
Dictionary to build a WHERE clause for a specific column.
See
meerschaum.utils.sql.build_where. - newest (bool, default True):
If
True, get the most recent datetime (honoringparams). IfFalse, get the oldest datetime (ASCinstead ofDESC). - apply_backtrack_interval (bool, default False):
If
True, subtract the backtrack interval from the sync time. - remote (bool, default False):
If
Trueand the instance connector supports it, return the sync time for the remote table definition. - round_down (bool, default False):
If
True, round down the datetime value to the nearest minute. - silent (bool, default False):
If
True, returnNoneinstead of raising when the database cannot be read (Meerschaum 4's behavior). - debug (bool, default False): Verbosity toggle.
Returns
- A
datetimeor int, if the pipe exists, otherwiseNone(a pipe that was never synced - has no sync time). Raises the database error if the instance cannot be read.
984def exists( 985 self, 986 debug: bool = False 987) -> bool: 988 """ 989 See if a Pipe's table exists. 990 991 Parameters 992 ---------- 993 debug: bool, default False 994 Verbosity toggle. 995 996 Returns 997 ------- 998 A `bool` corresponding to whether a pipe's underlying table exists. 999 1000 """ 1001 from meerschaum.utils.venv import Venv 1002 from meerschaum.connectors import get_connector_plugin 1003 from meerschaum.utils.debug import dprint 1004 from meerschaum.utils.dtypes import get_current_timestamp 1005 now = get_current_timestamp('ms', as_int=True) / 1000 1006 cache_seconds = mrsm.get_config('pipes', 'sync', 'exists_cache_seconds') 1007 1008 _exists = self._get_cached_value('_exists', debug=debug) 1009 if _exists: 1010 exists_timestamp = self._get_cached_value('_exists_timestamp', debug=debug) 1011 if exists_timestamp is not None: 1012 delta = now - exists_timestamp 1013 if delta < cache_seconds: 1014 if debug: 1015 dprint(f"Returning cached `exists` for {self} ({round(delta, 2)} seconds old).") 1016 return _exists 1017 1018 with Venv(get_connector_plugin(self.instance_connector)): 1019 _exists = ( 1020 self.instance_connector.pipe_exists(pipe=self, debug=debug) 1021 if hasattr(self.instance_connector, 'pipe_exists') 1022 else False 1023 ) 1024 1025 self._cache_value('_exists', _exists, debug=debug) 1026 self._cache_value('_exists_timestamp', now, debug=debug) 1027 return _exists
See if a Pipe's table exists.
Parameters
- debug (bool, default False): Verbosity toggle.
Returns
- A
boolcorresponding to whether a pipe's underlying table exists.
1030def filter_existing( 1031 self, 1032 df: 'pd.DataFrame', 1033 safe_copy: bool = True, 1034 date_bound_only: bool = False, 1035 include_unchanged_columns: bool = False, 1036 enforce_dtypes: bool = False, 1037 chunksize: Optional[int] = -1, 1038 debug: bool = False, 1039 as_polars: Optional[bool] = None, 1040 as_pandas: Optional[bool] = None, 1041 **kw 1042) -> Tuple['pd.DataFrame', 'pd.DataFrame', 'pd.DataFrame']: 1043 """ 1044 Inspect a dataframe and filter out rows which already exist in the pipe. 1045 1046 Parameters 1047 ---------- 1048 df: 'pd.DataFrame' 1049 The dataframe to inspect and filter. 1050 A `polars.DataFrame` or `LazyFrame` is accepted, 1051 in which case the returned dataframes are `polars.DataFrame`. 1052 Documents, dictionaries and Arrow data come back in the library of the 1053 `meerschaum:dataframe` setting. 1054 1055 safe_copy: bool, default True 1056 If `True`, create a copy before comparing and modifying the dataframes. 1057 Setting to `False` may mutate the DataFrames. 1058 See `meerschaum.utils.dataframe.filter_unseen_df`. 1059 1060 date_bound_only: bool, default False 1061 If `True`, only use the datetime index to fetch the sample dataframe. 1062 1063 include_unchanged_columns: bool, default False 1064 If `True`, include the backtrack columns which haven't changed in the update dataframe. 1065 This is useful if you can't update individual keys. 1066 1067 enforce_dtypes: bool, default False 1068 If `True`, ensure the given and intermediate dataframes are enforced to the correct dtypes. 1069 Setting `enforce_dtypes=True` may impact performance. 1070 1071 chunksize: Optional[int], default -1 1072 The `chunksize` used when fetching existing data. 1073 1074 debug: bool, default False 1075 Verbosity toggle. 1076 1077 as_polars: Optional[bool], default None 1078 For input that is not a DataFrame: `True` returns Polars frames, `False` pandas ones. 1079 1080 as_pandas: Optional[bool], default None 1081 For input that is not a DataFrame: `True` returns pandas frames, `False` Polars ones. 1082 1083 Returns 1084 ------- 1085 A tuple of three DataFrames: unseen, update, and delta, of the library `df` came in 1086 (Polars for a Polars DataFrame or LazyFrame, pandas for a pandas DataFrame). 1087 """ 1088 from meerschaum import _core 1089 from meerschaum.core.Pipe._data import _rust_context 1090 from meerschaum.utils.dataframe import ( 1091 output_library_for, ensure_dataframe_library, frame_library_of, 1092 ) 1093 context = _rust_context(self, debug=debug) 1094 if df is not None and not is_accepted_data(df): 1095 raise unsupported_data_error(df, f"filter the existing rows of {self}") 1096 library = ( 1097 frame_library_of(df) 1098 or output_library_for(df, as_polars=as_polars, as_pandas=as_pandas) 1099 ) 1100 ensure_dataframe_library(library) 1101 return _core.pipe_filter_existing( 1102 self, 1103 context, 1104 df, 1105 date_bound_only=date_bound_only, 1106 include_unchanged_columns=include_unchanged_columns, 1107 enforce_dtypes=enforce_dtypes, 1108 as_polars=(library == 'polars'), 1109 debug=debug, 1110 )
Inspect a dataframe and filter out rows which already exist in the pipe.
Parameters
- df ('pd.DataFrame'):
The dataframe to inspect and filter.
A
polars.DataFrameorLazyFrameis accepted, in which case the returned dataframes arepolars.DataFrame. Documents, dictionaries and Arrow data come back in the library of themeerschaum:dataframesetting. - safe_copy (bool, default True):
If
True, create a copy before comparing and modifying the dataframes. Setting toFalsemay mutate the DataFrames. Seemeerschaum.utils.dataframe.filter_unseen_df. - date_bound_only (bool, default False):
If
True, only use the datetime index to fetch the sample dataframe. - include_unchanged_columns (bool, default False):
If
True, include the backtrack columns which haven't changed in the update dataframe. This is useful if you can't update individual keys. - enforce_dtypes (bool, default False):
If
True, ensure the given and intermediate dataframes are enforced to the correct dtypes. Settingenforce_dtypes=Truemay impact performance. - chunksize (Optional[int], default -1):
The
chunksizeused when fetching existing data. - debug (bool, default False): Verbosity toggle.
- as_polars (Optional[bool], default None):
For input that is not a DataFrame:
Truereturns Polars frames,Falsepandas ones. - as_pandas (Optional[bool], default None):
For input that is not a DataFrame:
Truereturns pandas frames,FalsePolars ones.
Returns
A tuple of three DataFrames (unseen, update, and delta, of the library
dfcame in):(Polars for a Polars DataFrame or LazyFrame, pandas for a pandas DataFrame).
1135def get_num_workers(self, workers: Optional[int] = None) -> int: 1136 """ 1137 Get the number of workers to use for concurrent syncs. 1138 1139 Parameters 1140 ---------- 1141 The number of workers passed via `--workers`. 1142 1143 Returns 1144 ------- 1145 The number of workers, capped for safety. 1146 """ 1147 is_thread_safe = getattr(self.instance_connector, 'IS_THREAD_SAFE', False) 1148 if not is_thread_safe: 1149 return 1 1150 1151 engine_pool_size = None 1152 current_num_connections = threading.active_count() 1153 if self.instance_connector.type == 'sql': 1154 ### The Rust driver owns the pool, sized by the flavor's `pool_size`. An SQLAlchemy engine 1155 ### (the `sql` extra) is consulted only when the caller already created one: building it 1156 ### here imported (and auto-installed) SQLAlchemy and the flavor's drivers on the first 1157 ### sync of every plugin pipe on a SQL instance. 1158 engine = self.instance_connector.__dict__.get('_engine', None) 1159 if engine is None: 1160 try: 1161 engine_pool_size = int( 1162 self.instance_connector._sys_config.get('create_engine', {}).get('pool_size') 1163 or (os.cpu_count() or 5) 1164 ) 1165 except (TypeError, ValueError): 1166 engine_pool_size = os.cpu_count() or 5 1167 if engine is not None: 1168 engine_pool_size = engine.pool.size() 1169 current_num_connections = engine.pool.checkedout() 1170 desired_workers = ( 1171 min(workers or engine_pool_size, engine_pool_size) 1172 if engine_pool_size is not None 1173 else workers 1174 ) 1175 if desired_workers is None: 1176 desired_workers = (multiprocessing.cpu_count() if is_thread_safe else 1) 1177 1178 return max( 1179 (desired_workers - current_num_connections), 1180 1, 1181 )
Get the number of workers to use for concurrent syncs.
Parameters
- The number of workers passed via
--workers.
Returns
- The number of workers, capped for safety.
40def verify( 41 self, 42 begin: Union[datetime, int, None] = None, 43 end: Union[datetime, int, None] = None, 44 params: Optional[Dict[str, Any]] = None, 45 chunk_interval: Union[timedelta, int, None] = None, 46 bounded: Optional[bool] = None, 47 deduplicate: bool = False, 48 workers: Optional[int] = None, 49 batchsize: Optional[int] = None, 50 skip_chunks_with_greater_rowcounts: bool = False, 51 check_rowcounts_only: bool = False, 52 debug: bool = False, 53 **kwargs: Any 54) -> SuccessTuple: 55 """ 56 Verify the contents of the pipe by resyncing its interval. 57 58 Parameters 59 ---------- 60 begin: Union[datetime, int, None], default None 61 If specified, only verify rows greater than or equal to this value. 62 63 end: Union[datetime, int, None], default None 64 If specified, only verify rows less than this value. 65 66 chunk_interval: Union[timedelta, int, None], default None 67 If provided, use this as the size of the chunk boundaries. 68 Default to the value set in `pipe.parameters['verify']['chunk_minutes']` (43200 — 30 days). 69 70 bounded: Optional[bool], default None 71 If `True`, do not verify older than the oldest sync time or newer than the newest. 72 If `False`, verify unbounded syncs outside of the new and old sync times. 73 The default behavior (`None`) is to bound only if a bound interval is set 74 (e.g. `pipe.parameters['verify']['bound_days']`). 75 76 deduplicate: bool, default False 77 If `True`, deduplicate the pipe's table after the verification syncs. 78 79 workers: Optional[int], default None 80 If provided, limit the verification to this many threads. 81 Use a value of `1` to sync chunks in series. 82 83 batchsize: Optional[int], default None 84 If provided, sync this many chunks in parallel. 85 Defaults to `Pipe.get_num_workers()`. 86 87 skip_chunks_with_greater_rowcounts: bool, default False 88 If `True`, compare the rowcounts for a chunk and skip syncing if the pipe's 89 chunk rowcount equals or exceeds the remote's rowcount. 90 91 check_rowcounts_only: bool, default False 92 If `True`, only compare rowcounts and print chunks which are out-of-sync. 93 94 debug: bool, default False 95 Verbosity toggle. 96 97 kwargs: Any 98 All keyword arguments are passed to `pipe.sync()`. 99 100 Returns 101 ------- 102 A SuccessTuple indicating whether the pipe was successfully resynced. 103 """ 104 from meerschaum import _core 105 ### Verifying re-syncs its chunks: a `sync` lock refuses it before any is read. 106 if not check_rowcounts_only: 107 refusal = self._lock_refusal('sync', always=True, debug=debug) 108 if refusal: 109 return False, refusal 110 context, reason = _rust_verify_context(self, debug=debug) 111 if context is None: 112 return False, f"Cannot verify {self}: {reason}." 113 result = _core.verify_pipe( 114 self, 115 context['connectors'], 116 begin=begin, 117 end=end, 118 params=params, 119 chunk_interval=chunk_interval, 120 bounded=bounded, 121 deduplicate=deduplicate, 122 workers=workers, 123 batchsize=batchsize, 124 skip_chunks_with_greater_rowcounts=skip_chunks_with_greater_rowcounts, 125 check_rowcounts_only=check_rowcounts_only, 126 chunksize=kwargs.get('chunksize', -1), 127 check_existing=kwargs.get('check_existing', True), 128 kwargs={ 129 key: value 130 for key, value in kwargs.items() 131 if key not in _VERIFY_SYNC_KWARGS 132 }, 133 attributes=context['attributes'], 134 debug=debug, 135 ) 136 ### The chunk syncs ran in Rust: drop what this object cached about the table. 137 self._invalidate_cache(hard=True, debug=debug) 138 self._clear_cache_key('_attributes_sync_time', debug=debug) 139 return result
Verify the contents of the pipe by resyncing its interval.
Parameters
- begin (Union[datetime, int, None], default None): If specified, only verify rows greater than or equal to this value.
- end (Union[datetime, int, None], default None): If specified, only verify rows less than this value.
- chunk_interval (Union[timedelta, int, None], default None):
If provided, use this as the size of the chunk boundaries.
Default to the value set in
pipe.parameters['verify']['chunk_minutes'](43200, 30 days). - bounded (Optional[bool], default None):
If
True, do not verify older than the oldest sync time or newer than the newest. IfFalse, verify unbounded syncs outside of the new and old sync times. The default behavior (None) is to bound only if a bound interval is set (e.g.pipe.parameters['verify']['bound_days']). - deduplicate (bool, default False):
If
True, deduplicate the pipe's table after the verification syncs. - workers (Optional[int], default None):
If provided, limit the verification to this many threads.
Use a value of
1to sync chunks in series. - batchsize (Optional[int], default None):
If provided, sync this many chunks in parallel.
Defaults to
Pipe.get_num_workers(). - skip_chunks_with_greater_rowcounts (bool, default False):
If
True, compare the rowcounts for a chunk and skip syncing if the pipe's chunk rowcount equals or exceeds the remote's rowcount. - check_rowcounts_only (bool, default False):
If
True, only compare rowcounts and print chunks which are out-of-sync. - debug (bool, default False): Verbosity toggle.
- kwargs (Any):
All keyword arguments are passed to
pipe.sync().
Returns
- A SuccessTuple indicating whether the pipe was successfully resynced.
236def verify_schema( 237 self, 238 apply: bool = False, 239 dry_run: bool = False, 240 debug: bool = False, 241) -> SuccessTuple: 242 """ 243 Compare the pipe's table with its declared dtypes (`dtypes`, the `columns` roles, geometry 244 notations) and, with `apply=True`, bring the table in line. 245 246 Added columns and safe widenings (`int` to `bigint`, `int` to `float`, `float` to `numeric`, 247 `varchar(n)` to `text`, `text` to `json`) keep every value; other type changes may fail on 248 existing values or change them. Columns are never dropped, and static pipes are included. 249 The command line equivalent is `mrsm verify schema`. 250 251 Parameters 252 ---------- 253 apply: bool, default False 254 If `True`, apply every change the plan lists. Otherwise nothing is written. 255 256 dry_run: bool, default False 257 If `True`, return the SQL the plan would run (and write nothing). 258 259 debug: bool, default False 260 Verbosity toggle. 261 262 Returns 263 ------- 264 A `SuccessTuple`: the message is the plan (a table of the columns that change), the SQL 265 with `dry_run=True`, or the plan and what changed with `apply=True`. A protected instance 266 refuses `apply=True` unless writes are allowed (`meerschaum.connectors.allow_protected()`). 267 268 Examples 269 -------- 270 >>> pipe = mrsm.Pipe('plugin:noaa', 'weather', dtypes={'temperature': 'float'}) 271 >>> success, plan = pipe.verify_schema() 272 >>> print(plan) 273 >>> success, msg = pipe.verify_schema(apply=True) 274 """ 275 from meerschaum import _core 276 from meerschaum.core.Pipe._data import _rust_context 277 context = _rust_context(self, debug=debug) 278 success, message = _core.pipe_verify_schema( 279 self, 280 context, 281 apply=(apply and not dry_run), 282 dry_run=dry_run, 283 debug=debug, 284 ) 285 if apply and not dry_run: 286 ### The table's columns changed: forget the cached columns types. 287 self._clear_cache_key('_columns_types', debug=debug) 288 self._clear_cache_key('_columns_types_timestamp', debug=debug) 289 return success, message
Compare the pipe's table with its declared dtypes (dtypes, the columns roles, geometry
notations) and, with apply=True, bring the table in line.
Added columns and safe widenings (int to bigint, int to float, float to numeric,
varchar(n) to text, text to json) keep every value; other type changes may fail on
existing values or change them. Columns are never dropped, and static pipes are included.
The command line equivalent is mrsm verify schema.
Parameters
- apply (bool, default False):
If
True, apply every change the plan lists. Otherwise nothing is written. - dry_run (bool, default False):
If
True, return the SQL the plan would run (and write nothing). - debug (bool, default False): Verbosity toggle.
Returns
A
SuccessTuple(the message is the plan (a table of the columns that change), the SQL):with
dry_run=True, or the plan and what changed withapply=True. A protected instance- refuses
apply=Trueunless writes are allowed (meerschaum.connectors.allow_protected()).
Examples
>>> pipe = mrsm.Pipe('plugin:noaa', 'weather', dtypes={'temperature': 'float'})
>>> success, plan = pipe.verify_schema()
>>> print(plan)
>>> success, msg = pipe.verify_schema(apply=True)
143def get_bound_interval(self, debug: bool = False) -> Union[timedelta, int, None]: 144 """ 145 Return the interval used to determine the bound time (limit for verification syncs). 146 If the datetime axis is an integer, just return its value. 147 148 Below are the supported keys for the bound interval: 149 150 - `pipe.parameters['verify']['bound_minutes']` 151 - `pipe.parameters['verify']['bound_hours']` 152 - `pipe.parameters['verify']['bound_days']` 153 - `pipe.parameters['verify']['bound_weeks']` 154 - `pipe.parameters['verify']['bound_years']` 155 - `pipe.parameters['verify']['bound_seconds']` 156 157 If multiple keys are present, the first on this priority list will be used. 158 159 Returns 160 ------- 161 A `timedelta` or `int` value to be used to determine the bound time. 162 """ 163 verify_params = self.parameters.get('verify', {}) 164 prefix = 'bound_' 165 suffixes_to_check = ('minutes', 'hours', 'days', 'weeks', 'years', 'seconds') 166 keys_to_search = { 167 key: val 168 for key, val in verify_params.items() 169 if key.startswith(prefix) 170 } 171 bound_time_key, bound_time_value = None, None 172 for key, value in keys_to_search.items(): 173 for suffix in suffixes_to_check: 174 if key == prefix + suffix: 175 bound_time_key = key 176 bound_time_value = value 177 break 178 if bound_time_key is not None: 179 break 180 181 if bound_time_value is None: 182 return bound_time_value 183 184 dt_col = self.columns.get('datetime', None) 185 if not dt_col: 186 return bound_time_value 187 188 dt_typ = self.dtypes.get(dt_col, 'datetime') 189 if 'int' in dt_typ.lower(): 190 return int(bound_time_value) 191 192 interval_type = bound_time_key.replace(prefix, '') 193 return timedelta(**{interval_type: bound_time_value})
Return the interval used to determine the bound time (limit for verification syncs). If the datetime axis is an integer, just return its value.
Below are the supported keys for the bound interval:
- `pipe.parameters['verify']['bound_minutes']`
- `pipe.parameters['verify']['bound_hours']`
- `pipe.parameters['verify']['bound_days']`
- `pipe.parameters['verify']['bound_weeks']`
- `pipe.parameters['verify']['bound_years']`
- `pipe.parameters['verify']['bound_seconds']`
If multiple keys are present, the first on this priority list will be used.
Returns
- A
timedeltaorintvalue to be used to determine the bound time.
196def get_bound_time(self, debug: bool = False) -> Union[datetime, int, None]: 197 """ 198 The bound time is the limit at which long-running verification syncs should stop. 199 A value of `None` means verification syncs should be unbounded. 200 201 Like deriving a backtrack time from `pipe.get_sync_time()`, 202 the bound time is the sync time minus a large window (e.g. 366 days). 203 204 Unbound verification syncs (i.e. `bound_time is None`) 205 if the oldest sync time is less than the bound interval. 206 207 Returns 208 ------- 209 A `datetime` or `int` corresponding to the 210 `begin` bound for verification and deduplication syncs. 211 """ 212 bound_interval = self.get_bound_interval(debug=debug) 213 if bound_interval is None: 214 return None 215 216 sync_time = self.get_sync_time(debug=debug) 217 if sync_time is None: 218 return None 219 220 bound_time = sync_time - bound_interval 221 oldest_sync_time = self.get_sync_time(newest=False, debug=debug) 222 max_bound_time_days = STATIC_CONFIG['pipes']['max_bound_time_days'] 223 224 extreme_sync_times_delta = ( 225 hasattr(oldest_sync_time, 'tzinfo') 226 and (sync_time - oldest_sync_time) >= timedelta(days=max_bound_time_days) 227 ) 228 229 return ( 230 bound_time 231 if bound_time > oldest_sync_time or extreme_sync_times_delta 232 else None 233 )
The bound time is the limit at which long-running verification syncs should stop.
A value of None means verification syncs should be unbounded.
Like deriving a backtrack time from pipe.get_sync_time(),
the bound time is the sync time minus a large window (e.g. 366 days).
Unbound verification syncs (i.e. bound_time is None)
if the oldest sync time is less than the bound interval.
Returns
- A
datetimeorintcorresponding to the beginbound for verification and deduplication syncs.
12def delete( 13 self, 14 drop: bool = True, 15 debug: bool = False, 16 **kw 17) -> SuccessTuple: 18 """ 19 Call the Pipe's instance connector's `delete_pipe()` method. 20 21 Parameters 22 ---------- 23 drop: bool, default True 24 If `True`, drop the pipes' target table. 25 26 debug : bool, default False 27 Verbosity toggle. 28 29 Returns 30 ------- 31 A `SuccessTuple` of success (`bool`), message (`str`). 32 33 """ 34 from meerschaum.utils.warnings import warn 35 from meerschaum.utils.venv import Venv 36 from meerschaum.connectors import get_connector_plugin 37 38 if self.temporary: 39 if self.cache: 40 invalidate_success, invalidate_msg = self._invalidate_cache(hard=True, debug=debug) 41 if not invalidate_success: 42 return invalidate_success, invalidate_msg 43 44 return ( 45 False, 46 "Cannot delete pipes created with `temporary=True` (read-only). " 47 + "You may want to call `pipe.drop()` instead." 48 ) 49 50 ### Deleting drops the table first: a `drop` or `delete` lock refuses it before anything 51 ### changes, whatever the instance. 52 refusal = self._lock_refusal('delete_and_drop' if drop else 'delete', always=True, debug=debug) 53 if refusal: 54 return False, refusal 55 56 if drop: 57 drop_success, drop_msg = self.drop(debug=debug) 58 if not drop_success: 59 warn(f"Failed to drop {self}:\n{drop_msg}") 60 61 with Venv(get_connector_plugin(self.instance_connector)): 62 result = self.instance_connector.delete_pipe(self, debug=debug, **kw) 63 64 if not isinstance(result, tuple): 65 return False, f"Received an unexpected result from '{self.instance_connector}': {result}" 66 67 if result[0]: 68 self._invalidate_cache(hard=True, debug=debug) 69 self._clear_cache_key('_id', debug=debug) 70 from meerschaum.core.Pipeline._engine import forget_engine_state 71 forget_engine_state(self, 'delete_pipe') 72 73 return result
Call the Pipe's instance connector's delete_pipe() method.
Parameters
- drop (bool, default True):
If
True, drop the pipes' target table. - debug (bool, default False): Verbosity toggle.
Returns
- A
SuccessTupleof success (bool), message (str).
14def drop( 15 self, 16 debug: bool = False, 17 **kw: Any 18) -> SuccessTuple: 19 """ 20 Call the Pipe's instance connector's `drop_pipe()` method. 21 22 Parameters 23 ---------- 24 debug: bool, default False: 25 Verbosity toggle. 26 27 Returns 28 ------- 29 A `SuccessTuple` of success, message. 30 31 """ 32 from meerschaum.utils.venv import Venv 33 from meerschaum.connectors import get_connector_plugin 34 35 ### A `drop` lock refuses it (the core checks its own instances). 36 refusal = self._lock_refusal('drop', debug=debug) 37 if refusal: 38 return False, refusal 39 40 self._clear_cache_key('_exists', debug=debug) 41 42 with Venv(get_connector_plugin(self.instance_connector)): 43 if hasattr(self.instance_connector, 'drop_pipe'): 44 result = self.instance_connector.drop_pipe(self, debug=debug, **kw) 45 else: 46 result = ( 47 False, 48 ( 49 "Cannot drop pipes for instance connectors of type " 50 f"'{self.instance_connector.type}'." 51 ) 52 ) 53 54 self._clear_cache_key('_exists', debug=debug) 55 self._clear_cache_key('_exists_timestamp', debug=debug) 56 57 if isinstance(result, tuple) and result and result[0]: 58 from meerschaum.core.Pipeline._engine import forget_engine_state 59 forget_engine_state(self, 'drop_pipe') 60 61 return result
Call the Pipe's instance connector's drop_pipe() method.
Parameters
- debug (bool, default False:): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
64def drop_indices( 65 self, 66 columns: Optional[List[str]] = None, 67 debug: bool = False, 68 **kw: Any 69) -> SuccessTuple: 70 """ 71 Call the Pipe's instance connector's `drop_indices()` method. 72 73 Parameters 74 ---------- 75 columns: Optional[List[str]] = None 76 If provided, only drop indices in the given list. 77 78 debug: bool, default False: 79 Verbosity toggle. 80 81 Returns 82 ------- 83 A `SuccessTuple` of success, message. 84 85 """ 86 from meerschaum.utils.venv import Venv 87 from meerschaum.connectors import get_connector_plugin 88 89 ### Dropping indices changes the table's schema: a `drop` lock refuses it. 90 refusal = self._lock_refusal('schema', debug=debug) 91 if refusal: 92 return False, refusal 93 94 self._clear_cache_key('_columns_indices', debug=debug) 95 self._clear_cache_key('_columns_indices_timestamp', debug=debug) 96 self._clear_cache_key('_columns_types', debug=debug) 97 self._clear_cache_key('_columns_types_timestamp', debug=debug) 98 99 with Venv(get_connector_plugin(self.instance_connector)): 100 if hasattr(self.instance_connector, 'drop_pipe_indices'): 101 result = self.instance_connector.drop_pipe_indices( 102 self, 103 columns=columns, 104 debug=debug, 105 **kw 106 ) 107 else: 108 result = ( 109 False, 110 ( 111 "Cannot drop indices for instance connectors of type " 112 f"'{self.instance_connector.type}'." 113 ) 114 ) 115 116 self._clear_cache_key('_columns_indices', debug=debug) 117 self._clear_cache_key('_columns_indices_timestamp', debug=debug) 118 self._clear_cache_key('_columns_types', debug=debug) 119 self._clear_cache_key('_columns_types_timestamp', debug=debug) 120 121 return result
Call the Pipe's instance connector's drop_indices() method.
Parameters
- columns (Optional[List[str]] = None): If provided, only drop indices in the given list.
- debug (bool, default False:): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
14def compress( 15 self, 16 debug: bool = False, 17 **kw: Any 18) -> SuccessTuple: 19 """ 20 Call the Pipe's instance connector's `compress_pipe()` method. 21 22 For TimescaleDB hypertables this enables and applies native compression. 23 Other flavors fall back to their respective compression mechanisms where supported. 24 25 Parameters 26 ---------- 27 debug: bool, default False: 28 Verbosity toggle. 29 30 Returns 31 ------- 32 A `SuccessTuple` of success, message. 33 """ 34 from meerschaum.utils.venv import Venv 35 from meerschaum.connectors import get_connector_plugin 36 37 try: 38 with Venv(get_connector_plugin(self.instance_connector)): 39 if hasattr(self.instance_connector, 'compress_pipe'): 40 result = self.instance_connector.compress_pipe(self, debug=debug, **kw) 41 else: 42 result = ( 43 False, 44 ( 45 "Cannot compress pipes for instance connectors of type " 46 f"'{self.instance_connector.type}'." 47 ) 48 ) 49 except NotImplementedError: 50 result = ( 51 False, 52 ( 53 "Compression is not implemented for instance connectors of type " 54 f"'{self.instance_connector.type}'." 55 ) 56 ) 57 58 self._clear_cache_key('_exists', debug=debug) 59 return result
Call the Pipe's instance connector's compress_pipe() method.
For TimescaleDB hypertables this enables and applies native compression. Other flavors fall back to their respective compression mechanisms where supported.
Parameters
- debug (bool, default False:): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
62def decompress( 63 self, 64 debug: bool = False, 65 **kw: Any 66) -> SuccessTuple: 67 """ 68 Call the Pipe's instance connector's `decompress_pipe()` method, the inverse of `compress()`. 69 70 For TimescaleDB hypertables this removes the compression policy and converts compressed 71 chunks back to row-store. Other flavors fall back to their respective mechanisms where 72 supported. 73 74 Parameters 75 ---------- 76 debug: bool, default False: 77 Verbosity toggle. 78 79 Returns 80 ------- 81 A `SuccessTuple` of success, message. 82 """ 83 from meerschaum.utils.venv import Venv 84 from meerschaum.connectors import get_connector_plugin 85 86 try: 87 with Venv(get_connector_plugin(self.instance_connector)): 88 if hasattr(self.instance_connector, 'decompress_pipe'): 89 result = self.instance_connector.decompress_pipe(self, debug=debug, **kw) 90 else: 91 result = ( 92 False, 93 ( 94 "Cannot decompress pipes for instance connectors of type " 95 f"'{self.instance_connector.type}'." 96 ) 97 ) 98 except NotImplementedError: 99 result = ( 100 False, 101 ( 102 "Decompression is not implemented for instance connectors of type " 103 f"'{self.instance_connector.type}'." 104 ) 105 ) 106 107 self._clear_cache_key('_exists', debug=debug) 108 return result
Call the Pipe's instance connector's decompress_pipe() method, the inverse of compress().
For TimescaleDB hypertables this removes the compression policy and converts compressed chunks back to row-store. Other flavors fall back to their respective mechanisms where supported.
Parameters
- debug (bool, default False:): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
14def vacuum( 15 self, 16 full: bool = False, 17 debug: bool = False, 18 **kw: Any 19) -> SuccessTuple: 20 """ 21 Call the Pipe's instance connector's `vacuum_pipe()` method to reclaim disk space. 22 23 For PostgreSQL-family tables this runs `VACUUM` (optionally `VACUUM FULL`); other flavors 24 fall back to their respective space-reclaiming mechanisms where supported. 25 26 Parameters 27 ---------- 28 full: bool, default False 29 If `True` (PostgreSQL family only), run `VACUUM FULL` to return freed space to the OS. 30 31 debug: bool, default False 32 Verbosity toggle. 33 34 Returns 35 ------- 36 A `SuccessTuple` of success, message. 37 """ 38 from meerschaum.utils.venv import Venv 39 from meerschaum.connectors import get_connector_plugin 40 41 try: 42 with Venv(get_connector_plugin(self.instance_connector)): 43 if hasattr(self.instance_connector, 'vacuum_pipe'): 44 result = self.instance_connector.vacuum_pipe(self, full=full, debug=debug, **kw) 45 else: 46 result = ( 47 False, 48 ( 49 "Cannot vacuum pipes for instance connectors of type " 50 f"'{self.instance_connector.type}'." 51 ) 52 ) 53 except NotImplementedError: 54 result = ( 55 False, 56 ( 57 "Vacuuming is not implemented for instance connectors of type " 58 f"'{self.instance_connector.type}'." 59 ) 60 ) 61 62 self._clear_cache_key('_exists', debug=debug) 63 return result
Call the Pipe's instance connector's vacuum_pipe() method to reclaim disk space.
For PostgreSQL-family tables this runs VACUUM (optionally VACUUM FULL); other flavors
fall back to their respective space-reclaiming mechanisms where supported.
Parameters
- full (bool, default False):
If
True(PostgreSQL family only), runVACUUM FULLto return freed space to the OS. - debug (bool, default False): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
66def analyze( 67 self, 68 debug: bool = False, 69 **kw: Any 70) -> SuccessTuple: 71 """ 72 Call the Pipe's instance connector's `analyze_pipe()` method to refresh planner statistics. 73 74 Parameters 75 ---------- 76 debug: bool, default False 77 Verbosity toggle. 78 79 Returns 80 ------- 81 A `SuccessTuple` of success, message. 82 """ 83 from meerschaum.utils.venv import Venv 84 from meerschaum.connectors import get_connector_plugin 85 86 try: 87 with Venv(get_connector_plugin(self.instance_connector)): 88 if hasattr(self.instance_connector, 'analyze_pipe'): 89 result = self.instance_connector.analyze_pipe(self, debug=debug, **kw) 90 else: 91 result = ( 92 False, 93 ( 94 "Cannot analyze pipes for instance connectors of type " 95 f"'{self.instance_connector.type}'." 96 ) 97 ) 98 except NotImplementedError: 99 result = ( 100 False, 101 ( 102 "Analyzing is not implemented for instance connectors of type " 103 f"'{self.instance_connector.type}'." 104 ) 105 ) 106 107 return result
Call the Pipe's instance connector's analyze_pipe() method to refresh planner statistics.
Parameters
- debug (bool, default False): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
110def repartition( 111 self, 112 chunk_minutes: Optional[int] = None, 113 debug: bool = False, 114 **kw: Any 115) -> SuccessTuple: 116 """ 117 Call the Pipe's instance connector's `partition_pipe()` method to rebuild the target table 118 to a new partition (chunk) width. 119 120 On TimescaleDB this changes the chunk interval for future chunks. On PostgreSQL / PostGIS, 121 MySQL / MariaDB, and MSSQL it rebuilds the natively range-partitioned table at the new width. 122 123 Parameters 124 ---------- 125 chunk_minutes: Optional[int], default None 126 The new partition width in minutes. Defaults to the pipe's `verify.chunk_minutes`. 127 128 debug: bool, default False 129 Verbosity toggle. 130 131 Returns 132 ------- 133 A `SuccessTuple` of success, message. 134 """ 135 from meerschaum.utils.venv import Venv 136 from meerschaum.connectors import get_connector_plugin 137 138 ### Repartitioning rebuilds the table (`drop`) and records its width (`parameters`). 139 for op in ('schema', 'edit'): 140 refusal = self._lock_refusal(op, debug=debug) 141 if refusal: 142 return False, refusal 143 144 try: 145 with Venv(get_connector_plugin(self.instance_connector)): 146 if hasattr(self.instance_connector, 'partition_pipe'): 147 result = self.instance_connector.partition_pipe( 148 self, chunk_minutes=chunk_minutes, debug=debug, **kw 149 ) 150 else: 151 result = ( 152 False, 153 ( 154 "Cannot repartition pipes for instance connectors of type " 155 f"'{self.instance_connector.type}'." 156 ) 157 ) 158 except NotImplementedError: 159 result = ( 160 False, 161 ( 162 "Repartitioning is not implemented for instance connectors of type " 163 f"'{self.instance_connector.type}'." 164 ) 165 ) 166 167 self._clear_cache_key('_exists', debug=debug) 168 return result
Call the Pipe's instance connector's partition_pipe() method to rebuild the target table
to a new partition (chunk) width.
On TimescaleDB this changes the chunk interval for future chunks. On PostgreSQL / PostGIS, MySQL / MariaDB, and MSSQL it rebuilds the natively range-partitioned table at the new width.
Parameters
- 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
SuccessTupleof success, message.
14def create_indices( 15 self, 16 columns: Optional[List[str]] = None, 17 debug: bool = False, 18 **kw: Any 19) -> SuccessTuple: 20 """ 21 Call the Pipe's instance connector's `create_pipe_indices()` method. 22 23 Parameters 24 ---------- 25 debug: bool, default False: 26 Verbosity toggle. 27 28 Returns 29 ------- 30 A `SuccessTuple` of success, message. 31 32 """ 33 from meerschaum.utils.venv import Venv 34 from meerschaum.connectors import get_connector_plugin 35 36 self._clear_cache_key('_columns_indices', debug=debug) 37 self._clear_cache_key('_columns_indices_timestamp', debug=debug) 38 self._clear_cache_key('_columns_types', debug=debug) 39 self._clear_cache_key('_columns_types_timestamp', debug=debug) 40 41 with Venv(get_connector_plugin(self.instance_connector)): 42 if hasattr(self.instance_connector, 'create_pipe_indices'): 43 result = self.instance_connector.create_pipe_indices( 44 self, 45 columns=columns, 46 debug=debug, 47 **kw 48 ) 49 else: 50 result = ( 51 False, 52 ( 53 "Cannot create indices for instance connectors of type " 54 f"'{self.instance_connector.type}'." 55 ) 56 ) 57 58 self._clear_cache_key('_columns_indices', debug=debug) 59 self._clear_cache_key('_columns_indices_timestamp', debug=debug) 60 self._clear_cache_key('_columns_types', debug=debug) 61 self._clear_cache_key('_columns_types_timestamp', debug=debug) 62 63 return result
Call the Pipe's instance connector's create_pipe_indices() method.
Parameters
- debug (bool, default False:): Verbosity toggle.
Returns
- A
SuccessTupleof success, message.
16def clear( 17 self, 18 begin: Optional[datetime] = None, 19 end: Optional[datetime] = None, 20 params: Optional[Dict[str, Any]] = None, 21 debug: bool = False, 22 **kwargs: Any 23) -> SuccessTuple: 24 """ 25 Call the Pipe's instance connector's `clear_pipe` method. 26 27 Parameters 28 ---------- 29 begin: Optional[datetime], default None: 30 If provided, only remove rows newer than this datetime value. 31 32 end: Optional[datetime], default None: 33 If provided, only remove rows older than this datetime column (not including end). 34 35 params: Optional[Dict[str, Any]], default None 36 See `meerschaum.utils.sql.build_where`. 37 38 debug: bool, default False: 39 Verbositity toggle. 40 41 Returns 42 ------- 43 A `SuccessTuple` corresponding to whether this procedure completed successfully. 44 45 Examples 46 -------- 47 >>> pipe = mrsm.Pipe('test', 'test', columns={'datetime': 'dt'}, instance='sql:local') 48 >>> pipe.sync({'dt': [datetime(2020, 1, 1, 0, 0)]}) 49 >>> pipe.sync({'dt': [datetime(2021, 1, 1, 0, 0)]}) 50 >>> pipe.sync({'dt': [datetime(2022, 1, 1, 0, 0)]}) 51 >>> 52 >>> pipe.clear(begin=datetime(2021, 1, 1, 0, 0)) 53 >>> pipe.get_data() 54 dt 55 0 2020-01-01 56 57 """ 58 from meerschaum.utils.warnings import warn 59 from meerschaum.utils.venv import Venv 60 from meerschaum.connectors import get_connector_plugin 61 62 begin, end = self.parse_date_bounds(begin, end) 63 64 ### A `clear` lock refuses row deletes (the core checks its own instances). 65 refusal = self._lock_refusal('clear', debug=debug) 66 if refusal: 67 return False, refusal 68 69 with Venv(get_connector_plugin(self.instance_connector)): 70 result = self.instance_connector.clear_pipe( 71 self, 72 begin=begin, 73 end=end, 74 params=params, 75 debug=debug, 76 **kwargs 77 ) 78 if begin is None and end is None and not params and isinstance(result, tuple) and result[0]: 79 from meerschaum.core.Pipeline._engine import forget_engine_state 80 forget_engine_state(self, 'clear_pipe') 81 return result
Call the Pipe's instance connector's clear_pipe method.
Parameters
- begin (Optional[datetime], default None:): If provided, only remove rows newer than this datetime value.
- end (Optional[datetime], default None:): If provided, only remove rows older than this datetime column (not including end).
- params (Optional[Dict[str, Any]], default None):
See
meerschaum.utils.sql.build_where. - debug (bool, default False:): Verbositity toggle.
Returns
- A
SuccessTuplecorresponding to whether this procedure completed successfully.
Examples
>>> pipe = mrsm.Pipe('test', 'test', columns={'datetime': 'dt'}, instance='sql:local')
>>> pipe.sync({'dt': [datetime(2020, 1, 1, 0, 0)]})
>>> pipe.sync({'dt': [datetime(2021, 1, 1, 0, 0)]})
>>> pipe.sync({'dt': [datetime(2022, 1, 1, 0, 0)]})
>>>
>>> pipe.clear(begin=datetime(2021, 1, 1, 0, 0))
>>> pipe.get_data()
dt
0 2020-01-01
15def deduplicate( 16 self, 17 begin: Union[datetime, int, None] = None, 18 end: Union[datetime, int, None] = None, 19 params: Optional[Dict[str, Any]] = None, 20 chunk_interval: Union[datetime, int, None] = None, 21 bounded: Optional[bool] = None, 22 workers: Optional[int] = None, 23 debug: bool = False, 24 _use_instance_method: bool = True, 25 **kwargs: Any 26) -> SuccessTuple: 27 """ 28 Call the Pipe's instance connector's `delete_duplicates` method to delete duplicate rows. 29 30 Parameters 31 ---------- 32 begin: Union[datetime, int, None], default None: 33 If provided, only deduplicate rows newer than this datetime value. 34 35 end: Union[datetime, int, None], default None: 36 If provided, only deduplicate rows older than this datetime column (not including end). 37 38 params: Optional[Dict[str, Any]], default None 39 Restrict deduplication to this filter (for multiplexed data streams). 40 See `meerschaum.utils.sql.build_where`. 41 42 chunk_interval: Union[timedelta, int, None], default None 43 If provided, use this for the chunk bounds. 44 Defaults to the value set in `pipe.parameters['verify']['chunk_minutes']` (43200 — 30 days). 45 46 bounded: Optional[bool], default None 47 Only check outside the oldest and newest sync times if bounded is explicitly `False`. 48 49 workers: Optional[int], default None 50 If the instance connector is thread-safe, limit concurrenct syncs to this many threads. 51 52 debug: bool, default False: 53 Verbositity toggle. 54 55 kwargs: Any 56 All other keyword arguments are passed to 57 `pipe.sync()`, `pipe.clear()`, and `pipe.get_data(). 58 59 Returns 60 ------- 61 A `SuccessTuple` corresponding to whether all of the chunks were successfully deduplicated. 62 """ 63 from meerschaum import _core 64 from meerschaum.core.Pipe._data import _rust_context 65 context = _rust_context(self, debug=debug) 66 begin, end = self.parse_date_bounds(begin, end) 67 return _core.pipe_deduplicate( 68 self, 69 context, 70 begin=begin, 71 end=end, 72 params=params, 73 chunk_interval=chunk_interval, 74 bounded=bounded, 75 use_instance_method=_use_instance_method, 76 workers=self.get_num_workers(workers=workers), 77 kwargs=kwargs, 78 debug=debug, 79 )
Call the Pipe's instance connector's delete_duplicates method to delete duplicate rows.
Parameters
- begin (Union[datetime, int, None], default None:): If provided, only deduplicate rows newer than this datetime value.
- end (Union[datetime, int, None], default None:): If provided, only deduplicate rows older than this datetime column (not including end).
- params (Optional[Dict[str, Any]], default None):
Restrict deduplication to this filter (for multiplexed data streams).
See
meerschaum.utils.sql.build_where. - chunk_interval (Union[timedelta, int, None], default None):
If provided, use this for the chunk bounds.
Defaults to the value set in
pipe.parameters['verify']['chunk_minutes'](43200, 30 days). - bounded (Optional[bool], default None):
Only check outside the oldest and newest sync times if bounded is explicitly
False. - workers (Optional[int], default None): If the instance connector is thread-safe, limit concurrenct syncs to this many threads.
- debug (bool, default False:): Verbositity toggle.
- kwargs (Any):
All other keyword arguments are passed to
pipe.sync(),pipe.clear(), and `pipe.get_data().
Returns
- A
SuccessTuplecorresponding to whether all of the chunks were successfully deduplicated.
16def bootstrap( 17 self, 18 debug: bool = False, 19 yes: bool = False, 20 force: bool = False, 21 noask: bool = False, 22 shell: bool = False, 23 **kw 24) -> SuccessTuple: 25 """ 26 Prompt the user to create a pipe's requirements all from one method. 27 This method shouldn't be used in any automated scripts because it interactively 28 prompts the user and therefore may hang. 29 30 Parameters 31 ---------- 32 debug: bool, default False: 33 Verbosity toggle. 34 35 yes: bool, default False: 36 Print the questions and automatically agree. 37 38 force: bool, default False: 39 Skip the questions and agree anyway. 40 41 noask: bool, default False: 42 Print the questions but go with the default answer. 43 44 shell: bool, default False: 45 Used to determine if we are in the interactive shell. 46 47 Returns 48 ------- 49 A `SuccessTuple` corresponding to the success of this procedure. 50 51 """ 52 53 from meerschaum.utils.warnings import info 54 from meerschaum.utils.prompt import prompt, yes_no 55 from meerschaum.utils.formatting import pprint 56 from meerschaum.config import get_config 57 from meerschaum.utils.formatting._shell import clear_screen 58 from meerschaum.utils.formatting import print_tuple 59 from meerschaum.actions import actions 60 from meerschaum.utils.venv import Venv 61 from meerschaum.connectors import get_connector_plugin 62 63 _clear = get_config('shell', 'clear_screen', patch=True) 64 65 if self.id is not None: 66 delete_tuple = self.delete(debug=debug) 67 if not delete_tuple[0]: 68 return delete_tuple 69 70 if _clear: 71 clear_screen(debug=debug) 72 73 _parameters = _get_parameters(self, debug=debug) 74 self.parameters = _parameters 75 pprint(self.parameters) 76 try: 77 prompt( 78 f"\n Press [Enter] to register {self} with the above configuration:", 79 icon = False 80 ) 81 except KeyboardInterrupt: 82 return False, f"Aborted bootstrapping {self}." 83 84 with Venv(get_connector_plugin(self.instance_connector)): 85 register_tuple = self.instance_connector.register_pipe(self, debug=debug) 86 87 if not register_tuple[0]: 88 return register_tuple 89 90 if _clear: 91 clear_screen(debug=debug) 92 93 try: 94 if yes_no( 95 f"Would you like to edit the definition for {self}?", 96 yes=yes, 97 noask=noask, 98 default='n', 99 ): 100 edit_tuple = self.edit_definition(debug=debug) 101 if not edit_tuple[0]: 102 return edit_tuple 103 104 if yes_no( 105 f"Would you like to try syncing {self} now?", 106 yes=yes, 107 noask=noask, 108 default='n', 109 ): 110 sync_tuple = actions['sync']( 111 ['pipes'], 112 connector_keys=[self.connector_keys], 113 metric_keys=[self.metric_key], 114 location_keys=[self.location_key], 115 mrsm_instance=str(self.instance_connector), 116 debug=debug, 117 shell=shell, 118 ) 119 if not sync_tuple[0]: 120 return sync_tuple 121 except Exception as e: 122 return False, f"Failed to bootstrap {self}:\n" + str(e) 123 124 print_tuple((True, f"Finished bootstrapping {self}!")) 125 info( 126 "You can edit this pipe later with `edit pipes` " 127 + "or set the definition with `edit pipes definition`.\n" 128 + " To sync data into your pipe, run `sync pipes`." 129 ) 130 131 return True, "Success"
Prompt the user to create a pipe's requirements all from one method. This method shouldn't be used in any automated scripts because it interactively prompts the user and therefore may hang.
Parameters
- debug (bool, default False:): Verbosity toggle.
- yes (bool, default False:): Print the questions and automatically agree.
- force (bool, default False:): Skip the questions and agree anyway.
- noask (bool, default False:): Print the questions but go with the default answer.
- shell (bool, default False:): Used to determine if we are in the interactive shell.
Returns
- A
SuccessTuplecorresponding to the success of this procedure.
19def enforce_dtypes( 20 self, 21 df: 'pd.DataFrame', 22 chunksize: Optional[int] = -1, 23 enforce: bool = True, 24 safe_copy: bool = True, 25 dtypes: Optional[Dict[str, str]] = None, 26 as_polars: Optional[bool] = None, 27 debug: bool = False, 28 as_pandas: Optional[bool] = None, 29) -> 'pd.DataFrame': 30 """ 31 Cast the input dataframe to the pipe's registered data types. 32 If the pipe does not exist and dtypes are not set, return the dataframe. 33 34 The result is the library `df` came in (a Polars frame stays Polars, a pandas frame 35 pandas); documents, dictionaries and Arrow data come back in the library of the 36 `meerschaum:dataframe` setting. `as_polars=True` / `as_pandas=True` ask for one library. 37 """ 38 from meerschaum import _core 39 from meerschaum.core.Pipe._data import _rust_context 40 from meerschaum.core.Pipe._sync import is_accepted_data, unsupported_data_error 41 from meerschaum.utils.dataframe import output_library_for, ensure_dataframe_library 42 context = _rust_context(self, debug=debug) 43 if df is not None and not is_accepted_data(df): 44 raise unsupported_data_error(df, f"enforce the dtypes of {self}") 45 library = output_library_for(df, as_polars=as_polars, as_pandas=as_pandas) 46 if df is not None: 47 ensure_dataframe_library(library) 48 as_polars = library == 'polars' 49 return _core.pipe_enforce_dtypes( 50 self, 51 context, 52 df, 53 enforce=enforce, 54 safe_copy=safe_copy, 55 dtypes=dtypes, 56 as_polars=as_polars, 57 debug=debug, 58 )
Cast the input dataframe to the pipe's registered data types. If the pipe does not exist and dtypes are not set, return the dataframe.
The result is the library df came in (a Polars frame stays Polars, a pandas frame
pandas); documents, dictionaries and Arrow data come back in the library of the
meerschaum:dataframe setting. as_polars=True / as_pandas=True ask for one library.
61def infer_dtypes( 62 self, 63 persist: bool = False, 64 refresh: bool = False, 65 debug: bool = False, 66) -> Dict[str, Any]: 67 """ 68 If `dtypes` is not set in `meerschaum.Pipe.parameters`, 69 infer the data types from the underlying table if it exists. 70 71 Parameters 72 ---------- 73 persist: bool, default False 74 If `True`, persist the inferred data types to `meerschaum.Pipe.parameters`. 75 NOTE: Use with caution! Generally `dtypes` is meant to be user-configurable only. 76 77 refresh: bool, default False 78 If `True`, retrieve the latest columns-types for the pipe. 79 See `Pipe.get_columns.types()`. 80 81 Returns 82 ------- 83 A dictionary of strings containing the pandas data types for this Pipe. 84 """ 85 from meerschaum import _core 86 from meerschaum.core.Pipe._data import _rust_context 87 context = _rust_context(self, debug=debug, refresh=refresh) 88 remote_pd_dtypes = _core.pipe_infer_dtypes(self, context, refresh=refresh, debug=debug) 89 ### A table that does not exist has no dtypes to persist. 90 if not persist or not remote_pd_dtypes: 91 return remote_pd_dtypes 92 parameters = self.get_parameters(refresh=refresh, debug=debug) 93 dtypes = parameters.get('dtypes', {}) 94 dtypes.update({ 95 col: typ 96 for col, typ in remote_pd_dtypes.items() 97 if col not in dtypes 98 }) 99 self.dtypes = dtypes 100 self.edit(interactive=False, debug=debug) 101 return remote_pd_dtypes
If dtypes is not set in meerschaum.Pipe.parameters,
infer the data types from the underlying table if it exists.
Parameters
- persist (bool, default False):
If
True, persist the inferred data types tomeerschaum.Pipe.parameters. NOTE: Use with caution! Generallydtypesis meant to be user-configurable only. - refresh (bool, default False):
If
True, retrieve the latest columns-types for the pipe. SeePipe.get_columns.types().
Returns
- A dictionary of strings containing the pandas data types for this Pipe.
15def copy_to( 16 self, 17 instance_keys: str, 18 sync: bool = True, 19 begin: Union[datetime, int, None] = None, 20 end: Union[datetime, int, None] = None, 21 params: Optional[Dict[str, Any]] = None, 22 chunk_interval: Union[timedelta, int, None] = None, 23 debug: bool = False, 24 **kwargs: Any 25) -> SuccessTuple: 26 """ 27 Copy a pipe to another instance. 28 29 Parameters 30 ---------- 31 instance_keys: str 32 The instance to which to copy this pipe. 33 34 sync: bool, default True 35 If `True`, sync the source pipe's documents 36 37 begin: Union[datetime, int, None], default None 38 Beginning datetime value to pass to `Pipe.get_data()`. 39 40 end: Union[datetime, int, None], default None 41 End datetime value to pass to `Pipe.get_data()`. 42 43 params: Optional[Dict[str, Any]], default None 44 Parameters filter to pass to `Pipe.get_data()`. 45 46 chunk_interval: Union[timedelta, int, None], default None 47 The size of chunks to retrieve from `Pipe.get_data()` for syncing. 48 49 kwargs: Any 50 Additional flags to pass to `Pipe.get_data()` and `Pipe.sync()`, e.g. `workers`. 51 52 Returns 53 ------- 54 A SuccessTuple indicating success. 55 """ 56 if str(instance_keys) == self.instance_keys: 57 return False, f"Cannot copy {self} to instance '{instance_keys}'." 58 59 begin, end = self.parse_date_bounds(begin, end) 60 61 new_pipe = mrsm.Pipe( 62 self.connector_keys, 63 self.metric_key, 64 self.location_key, 65 parameters=self.parameters.copy(), 66 instance=instance_keys, 67 ) 68 69 new_pipe_is_registered = new_pipe.id is not None 70 71 metadata_method = new_pipe.edit if new_pipe_is_registered else new_pipe.register 72 metadata_success, metadata_msg = metadata_method(debug=debug) 73 if not metadata_success: 74 return metadata_success, metadata_msg 75 76 if not self.exists(debug=debug): 77 return True, f"{self} does not exist; nothing to sync." 78 79 original_as_iterator = kwargs.get('as_iterator', None) 80 kwargs['as_iterator'] = True 81 82 chunk_generator = self.get_data( 83 begin=begin, 84 end=end, 85 params=params, 86 chunk_interval=chunk_interval, 87 debug=debug, 88 **kwargs 89 ) 90 91 if original_as_iterator is None: 92 _ = kwargs.pop('as_iterator', None) 93 else: 94 kwargs['as_iterator'] = original_as_iterator 95 96 sync_success, sync_msg = new_pipe.sync( 97 chunk_generator, 98 begin=begin, 99 end=end, 100 params=params, 101 debug=debug, 102 **kwargs 103 ) 104 msg = ( 105 f"Successfully synced {new_pipe}:\n{sync_msg}" 106 if sync_success 107 else f"Failed to sync {new_pipe}:\n{sync_msg}" 108 ) 109 return sync_success, msg
Copy a pipe to another instance.
Parameters
- instance_keys (str): The instance to which to copy this pipe.
- sync (bool, default True):
If
True, sync the source pipe's documents - begin (Union[datetime, int, None], default None):
Beginning datetime value to pass to
Pipe.get_data(). - end (Union[datetime, int, None], default None):
End datetime value to pass to
Pipe.get_data(). - params (Optional[Dict[str, Any]], default None):
Parameters filter to pass to
Pipe.get_data(). - chunk_interval (Union[timedelta, int, None], default None):
The size of chunks to retrieve from
Pipe.get_data()for syncing. - kwargs (Any):
Additional flags to pass to
Pipe.get_data()andPipe.sync(), e.g.workers.
Returns
- A SuccessTuple indicating success.
39def get_sync_status(self, debug: bool = False) -> Optional[Dict[str, Any]]: 40 """ 41 Return how this pipe's latest recorded sync went. 42 43 Every sync records its outcome on the pipe's instance: `mrsm sync pipes`, background jobs, 44 Pipeline runs, the API server and `pipe.sync()`. The outcome is written in the background 45 (a sync never waits for it): successes of one pipe at most every few seconds, failures at 46 once. A read in the same process sees the latest sync, and a script writes its last 47 outcomes when it exits. 48 49 Parameters 50 ---------- 51 debug: bool, default False 52 Verbosity toggle. 53 54 Returns 55 ------- 56 A dictionary (see `meerschaum.core.Pipe._status`), or `None` if no sync of this pipe has 57 been recorded. Reading the status from an unreachable instance raises an exception. 58 59 Examples 60 -------- 61 >>> pipe = mrsm.Pipe('sql:main', 'weather', instance='sql:local') 62 >>> pipe.sync([{'ts': '2026-01-01', 'temp': 20.1}]) 63 >>> status = pipe.get_sync_status() 64 >>> status['success'], status['consecutive_failures'] 65 (True, 0) 66 """ 67 from meerschaum import _core 68 return _core.pipe_get_sync_status(self, _status_context(self, debug=debug), debug=debug)
Return how this pipe's latest recorded sync went.
Every sync records its outcome on the pipe's instance: mrsm sync pipes, background jobs,
Pipeline runs, the API server and pipe.sync(). The outcome is written in the background
(a sync never waits for it): successes of one pipe at most every few seconds, failures at
once. A read in the same process sees the latest sync, and a script writes its last
outcomes when it exits.
Parameters
- debug (bool, default False): Verbosity toggle.
Returns
- A dictionary (see
meerschaum.core.Pipe._status), orNoneif no sync of this pipe has - been recorded. Reading the status from an unreachable instance raises an exception.
Examples
>>> pipe = mrsm.Pipe('sql:main', 'weather', instance='sql:local')
>>> pipe.sync([{'ts': '2026-01-01', 'temp': 20.1}])
>>> status = pipe.get_sync_status()
>>> status['success'], status['consecutive_failures']
(True, 0)
33@property 34def access(self) -> Dict[str, Any]: 35 """ 36 The pipe's access: `{'owner', 'team', 'visibility', 'edit'}` (`team` `None` is 37 `Everyone`). Change it with `set_access()`. 38 """ 39 from meerschaum.core.access import get_pipe_access 40 return get_pipe_access(_raw_parameters(self))
The pipe's access: {'owner', 'team', 'visibility', 'edit'} (team None is
Everyone). Change it with set_access().
43@property 44def owner(self) -> Optional[str]: 45 """The username of the pipe's owner (`None`: no owner).""" 46 return self.access.get('owner', None)
The username of the pipe's owner (None: no owner).
49@property 50def team(self) -> str: 51 """The name of the pipe's team (`'Everyone'` when it names none).""" 52 from meerschaum.core.access import EVERYONE 53 return self.access.get('team', None) or EVERYONE
The name of the pipe's team ('Everyone' when it names none).
56@property 57def visibility(self) -> str: 58 """Who can see the pipe: `'team'` (default), `'private'` or `'everyone'`.""" 59 return self.access.get('visibility', 'team')
Who can see the pipe: 'team' (default), 'private' or 'everyone'.
62@property 63def edit_mode(self) -> str: 64 """Who can change the pipe: `'team'` (default: anyone who can see it) or `'owner'`.""" 65 return self.access.get('edit', 'team')
Who can change the pipe: 'team' (default: anyone who can see it) or 'owner'.
68@property 69def locks(self) -> List[str]: 70 """ 71 The pipe's locks (`parameters['locks']`): `'parameters'`, `'sync'`, `'clear'`, `'drop'`, 72 `'delete'` or `'all'`. Change them with `lock()` and `unlock()`. 73 """ 74 from meerschaum.core.access import get_locks 75 return get_locks(_raw_parameters(self))
107def lock(self, *locks: str, debug: bool = False) -> mrsm.SuccessTuple: 108 """ 109 Lock the pipe against writes. Every write path refuses what a lock refuses, locally too: 110 111 - `'parameters'`: parameter edits (changing `locks` stays allowed); 112 - `'sync'`: syncs, replace windows, deduplication and verification syncs; 113 - `'clear'`: row deletes (clears, replace windows, rebuilds); 114 - `'drop'`: drops, rebuilds and table schema changes (`verify schema`, repartitioning); 115 - `'delete'`: deleting (unregistering) the pipe; 116 - `'all'` (the default): all of them. 117 118 A registered pipe is saved at once; an unregistered one keeps the locks in memory until it 119 is registered. 120 121 Parameters 122 ---------- 123 *locks: str 124 The locks to add (names or comma-separated text). Without any, `'all'`. 125 126 Returns 127 ------- 128 A `SuccessTuple`. 129 130 Examples 131 -------- 132 >>> pipe.lock('drop', 'delete') 133 (True, "Locked Pipe('sql:main', 'weather') (drop, delete).") 134 >>> pipe.clear() 135 (True, 'Success') 136 >>> pipe.drop() 137 (False, "Cannot drop Pipe('sql:main', 'weather'): it is locked (drop). Unlock it with ...") 138 """ 139 from meerschaum.core.access import parse_locks, locks_with 140 try: 141 wanted = parse_locks(list(locks)) 142 except ValueError as e: 143 return False, str(e) 144 new = locks_with(self.locks, wanted) 145 if not _is_registered(self, debug=debug): 146 _set_in_memory(self, 'locks', new) 147 return True, f"Locked {self} ({', '.join(new)}); saved when it is registered." 148 result = self.instance_connector.lock_pipe(self, wanted, debug=debug) 149 if result[0]: 150 ### The in-memory parameters follow (they patch the registered ones on reads). 151 _set_in_memory(self, 'locks', new) 152 _after_write(self, debug=debug) 153 return tuple(result)
Lock the pipe against writes. Every write path refuses what a lock refuses, locally too:
'parameters': parameter edits (changinglocksstays allowed);'sync': syncs, replace windows, deduplication and verification syncs;'clear': row deletes (clears, replace windows, rebuilds);'drop': drops, rebuilds and table schema changes (verify schema, repartitioning);'delete': deleting (unregistering) the pipe;'all'(the default): all of them.
A registered pipe is saved at once; an unregistered one keeps the locks in memory until it is registered.
Parameters
- *locks (str):
The locks to add (names or comma-separated text). Without any,
'all'.
Returns
- A
SuccessTuple.
Examples
>>> pipe.lock('drop', 'delete')
(True, "Locked Pipe('sql:main', 'weather') (drop, delete).")
>>> pipe.clear()
(True, 'Success')
>>> pipe.drop()
(False, "Cannot drop Pipe('sql:main', 'weather'): it is locked (drop). Unlock it with ...")
156def unlock(self, *locks: str, debug: bool = False) -> mrsm.SuccessTuple: 157 """ 158 Remove locks from the pipe (without any, every lock). Removing one kind from `'all'` keeps 159 the other kinds. 160 161 Parameters 162 ---------- 163 *locks: str 164 The locks to remove (names or comma-separated text). Without any, `'all'`. 165 166 Returns 167 ------- 168 A `SuccessTuple`. 169 """ 170 from meerschaum.core.access import parse_locks, locks_without 171 try: 172 removed = parse_locks(list(locks)) 173 except ValueError as e: 174 return False, str(e) 175 new = locks_without(self.locks, removed) 176 if not _is_registered(self, debug=debug): 177 _set_in_memory(self, 'locks', new or None) 178 return True, f"Unlocked {self}" + (f" (still locked: {', '.join(new)})." if new else ".") 179 result = self.instance_connector.unlock_pipe(self, removed, debug=debug) 180 if result[0]: 181 _set_in_memory(self, 'locks', new or None) 182 _after_write(self, debug=debug) 183 return tuple(result)
Remove locks from the pipe (without any, every lock). Removing one kind from 'all' keeps
the other kinds.
Parameters
- *locks (str):
The locks to remove (names or comma-separated text). Without any,
'all'.
Returns
- A
SuccessTuple.
186def set_access( 187 self, 188 team: Any = _UNSET, 189 visibility: Any = _UNSET, 190 edit: Any = _UNSET, 191 owner: Any = _UNSET, 192 debug: bool = False, 193) -> mrsm.SuccessTuple: 194 """ 195 Set who owns the pipe and who may see and change it (`parameters['access']`). Arguments not 196 given keep their current values; `None` clears the owner or the team (back to 197 `Everyone`). 198 199 The API server, the MCP server and the web console enforce access; through an `api:` 200 instance only the pipe's owner, its team's managers and administrators may change it. A 201 `parameters` lock refuses the change. 202 203 Parameters 204 ---------- 205 team: Optional[str] 206 The pipe's team (`None` or `'Everyone'`: every user's). 207 208 visibility: str 209 `'team'` (its team's members and its owner), `'private'` (its owner, its team's 210 managers and administrators) or `'everyone'` (every user). 211 212 edit: str 213 `'team'` (anyone who can see it may change it within their permissions) or `'owner'` 214 (its owner, its team's managers and administrators). 215 216 owner: Optional[str] 217 The owner's username (`None`: no owner). 218 219 Returns 220 ------- 221 A `SuccessTuple`. 222 223 Examples 224 -------- 225 >>> pipe.set_access(team='ops', visibility='private') 226 (True, "Set the access of Pipe('sql:main', 'weather'): team ops, visibility private, edit team.") 227 >>> pipe.team, pipe.visibility 228 ('ops', 'private') 229 """ 230 from meerschaum.core.access import parse_access 231 current = dict(self.access) 232 new = dict(current) 233 if team is not _UNSET: 234 new['team'] = team 235 if visibility is not _UNSET: 236 new['visibility'] = visibility 237 if edit is not _UNSET: 238 new['edit'] = edit 239 if owner is not _UNSET: 240 new['owner'] = owner 241 try: 242 new = parse_access(new) 243 except ValueError as e: 244 return False, str(e) 245 if not _is_registered(self, debug=debug): 246 _set_in_memory(self, 'access', new) 247 return True, f"Set the access of {self}; saved when it is registered." 248 result = self.instance_connector.set_pipe_access(self, new, debug=debug) 249 if result[0]: 250 _set_in_memory(self, 'access', new) 251 _after_write(self, debug=debug) 252 return tuple(result)
Set who owns the pipe and who may see and change it (parameters['access']). Arguments not
given keep their current values; None clears the owner or the team (back to
Everyone).
The API server, the MCP server and the web console enforce access; through an api:
instance only the pipe's owner, its team's managers and administrators may change it. A
parameters lock refuses the change.
Parameters
- team (Optional[str]):
The pipe's team (
Noneor'Everyone': every user's). - visibility (str):
'team'(its team's members and its owner),'private'(its owner, its team's managers and administrators) or'everyone'(every user). - edit (str):
'team'(anyone who can see it may change it within their permissions) or'owner'(its owner, its team's managers and administrators). - owner (Optional[str]):
The owner's username (
None: no owner).
Returns
- A
SuccessTuple.
Examples
>>> pipe.set_access(team='ops', visibility='private')
(True, "Set the access of Pipe('sql:main', 'weather'): team ops, visibility private, edit team.")
>>> pipe.team, pipe.visibility
('ops', 'private')
588class Pipeline: 589 """ 590 A selection over the pipe graph: member pipes plus traversal depths (design §5.2, D4). 591 592 Parameters 593 ---------- 594 pipes: Union[str, mrsm.Pipe, Iterable[...], None], default None 595 What to select: 596 597 - a `str` without `|` is the **name of a saved Pipeline**, loaded from `instance` 598 (`mrsm.Pipeline('daily')`; `ValueError` if it does not exist); 599 - a `str` with `|` is a pipe reference, `'instance|connector|metric|location'`; 600 - a `Pipe`, or a `(connector_keys, metric_key[, location_key])` tuple, is one member; 601 - a list may mix all of these: the members of every named Pipeline plus the given 602 pipes (the dictionary `mrsm.get_pipes()` returns works too). 603 604 parents: Union[bool, int, None], default None 605 `True` includes every ancestor of the members; an `int` includes that many generations. 606 For a saved Pipeline, `None` keeps its saved setting and `False` turns traversal off. 607 608 children: Union[bool, int, None], default None 609 `True` includes every descendant of the members; an `int` that many generations. 610 For a saved Pipeline, `None` keeps its saved setting and `False` turns traversal off. 611 612 tags, connector_keys, metric_keys, location_keys: Union[str, List[str], None], default None 613 Select the members by the same filters as `-t`, `-c`, `-m` and `-l` 614 (negate a value with a leading `_`). Used when `pipes` is not provided. 615 616 instance: Union[str, InstanceConnector, None], default None 617 The instance that keeps the engine state and the saved Pipelines (default: the member 618 pipes' instance, else the configured one). Saved Pipelines are loaded from here. 619 620 name: Optional[str], default None 621 The name to save this selection under (`save()`), also used in its run IDs. 622 Without `pipes` or filters, `Pipeline(name='daily')` loads the saved Pipeline. 623 624 graph: Optional[bool], default None 625 Whether the run respects the dependencies between the members even without traversal 626 (a child member waits for its parent member and is capped at its watermark). 627 A saved Pipeline keeps its `graph` setting (default `True`), like 628 `mrsm sync pipelines NAME`; an unsaved selection without `parents` / `children` syncs 629 each member on its own, like `mrsm sync pipes`. 630 631 Examples 632 -------- 633 >>> pipeline = mrsm.Pipeline(pipe, parents=True) 634 >>> plan = pipeline.plan() 635 >>> run = pipeline.sync(workers=4) 636 >>> run.success, run.run_id, run.inserted 637 >>> pipeline.save('daily') 638 >>> mrsm.Pipeline('daily').sync() 639 """ 640 641 def __init__( 642 self, 643 pipes: Union[str, mrsm.Pipe, Iterable[Any], None] = None, 644 parents: Union[bool, int, None] = None, 645 children: Union[bool, int, None] = None, 646 tags: Union[str, List[str], None] = None, 647 connector_keys: Union[str, List[str], None] = None, 648 metric_keys: Union[str, List[str], None] = None, 649 location_keys: Union[str, List[str], None] = None, 650 instance: Union[str, Any, None] = None, 651 name: Optional[str] = None, 652 graph: Optional[bool] = None, 653 ): 654 if name is not None and not isinstance(name, str): 655 raise TypeError(f"A Pipeline name must be a string (got {name!r}).") 656 names, targets = _split_selection(pipes) 657 filters = { 658 'tags': tags, 659 'connector_keys': connector_keys, 660 'metric_keys': metric_keys, 661 'location_keys': location_keys, 662 } 663 filters = {k: ([v] if isinstance(v, str) else v) for k, v in filters.items() if v} 664 if not names and not targets and not filters: 665 if not name: 666 raise ValueError( 667 "A Pipeline needs member pipes, a tags / keys filter or the name of a saved " 668 "Pipeline, e.g. `mrsm.Pipeline(pipe)`, `mrsm.Pipeline(tags=['daily'])` or " 669 "`mrsm.Pipeline('daily')`." 670 ) 671 names = [name] 672 if names and filters: 673 raise ValueError( 674 "A saved Pipeline cannot be combined with a tags / keys filter: " 675 "pass the extra pipes themselves." 676 ) 677 definitions = [] 678 registry = None 679 if names: 680 registry = _registry_of(instance) 681 seen = set() 682 for saved_name in names: 683 if saved_name in seen: 684 continue 685 seen.add(saved_name) 686 definitions.append(_read_definition(saved_name, registry)) 687 self._configure( 688 definitions, 689 targets, 690 filters, 691 parents=parents, 692 children=children, 693 instance=(registry if registry is not None else instance), 694 name=name, 695 graph=graph, 696 ) 697 698 def _configure( 699 self, 700 definitions: List[Dict[str, Any]], 701 targets: List[Any], 702 filters: Dict[str, Any], 703 parents: Union[bool, int, None] = None, 704 children: Union[bool, int, None] = None, 705 instance: Union[str, Any, None] = None, 706 name: Optional[str] = None, 707 graph: Optional[bool] = None, 708 ) -> None: 709 """Set the selection: saved definitions (in order), extra members and filters.""" 710 names = [definition['name'] for definition in definitions] 711 ### A member given as a `Pipe` object keeps that object (and its in-memory parameters); 712 ### the other members of a saved Pipeline are built from their references. 713 given = { 714 _pipe_ref(target): target 715 for target in targets 716 if isinstance(target, mrsm.Pipe) 717 } 718 members: List[Any] = [] 719 seen = set() 720 loaded: Dict[str, mrsm.Pipe] = {} 721 for definition in definitions: 722 for ref in definition.get('targets', None) or []: 723 if ref in seen: 724 continue 725 seen.add(ref) 726 if ref in given: 727 members.append(given[ref]) 728 continue 729 member = _pipe_from_ref(ref) 730 members.append(member) 731 loaded[ref] = member 732 for target in targets: 733 if isinstance(target, mrsm.Pipe): 734 ref = _pipe_ref(target) 735 if ref in seen: 736 continue 737 seen.add(ref) 738 members.append(target) 739 self._definitions: List[Dict[str, Any]] = [deepcopy(d) for d in definitions] 740 self._loaded = loaded 741 self._sources: List[Any] = list(names) + list(targets) 742 self._targets: List[Any] = members 743 self._filters: Dict[str, Any] = filters 744 self.parents = ( 745 get_depth(parents) 746 if parents is not None or not definitions 747 else _common([get_depth(d.get('parents')) for d in definitions], 'parents', names) 748 ) 749 self.children = ( 750 get_depth(children) 751 if children is not None or not definitions 752 else _common([get_depth(d.get('children')) for d in definitions], 'children', names) 753 ) 754 self._graph: Optional[bool] = None if graph is None else bool(graph) 755 if name is None and len(definitions) == 1 and not targets: 756 name = definitions[0]['name'] 757 self.name: Optional[str] = name 758 if definitions: 759 self.instance = _registry_of(instance) 760 else: 761 pipe_targets = [target for target in members if isinstance(target, mrsm.Pipe)] 762 self.instance = _instance_of(pipe_targets, instance) 763 764 @classmethod 765 def _from_definitions( 766 cls, 767 definitions: List[Dict[str, Any]], 768 instance: Any, 769 name: Optional[str] = None, 770 ) -> 'Pipeline': 771 """A Pipeline bound to saved definitions which were already read.""" 772 pipeline = cls.__new__(cls) 773 pipeline._configure(definitions, [], {}, instance=instance, name=name) 774 return pipeline 775 776 @classmethod 777 def load(cls, name: str, instance=None) -> 'Pipeline': 778 """ 779 Load the saved Pipeline `name` from `instance` (default: the configured instance). 780 The same as `mrsm.Pipeline(name, instance=instance)`. 781 """ 782 return cls(name, instance=instance) 783 784 @property 785 def definition(self) -> Optional[Dict[str, Any]]: 786 """ 787 The saved definition this Pipeline was loaded from or saved as (`name`, `registry`, 788 `id`, `revision`, `targets`, `parents`, `children`, `description`, `defaults`, ...), 789 or `None` for an unsaved selection (or one combined from several saved Pipelines). 790 """ 791 return self._definitions[0] if len(self._definitions) == 1 else None 792 793 @definition.setter 794 def definition(self, value: Optional[Dict[str, Any]]) -> None: 795 self._bind([value] if value else []) 796 797 def _bind(self, definitions: List[Dict[str, Any]]) -> None: 798 """Bind this object to saved definitions (after a save, a rename, a refresh, ...).""" 799 self._definitions = list(definitions) 800 801 def _owner_of(self, pipe: Any) -> Optional[str]: 802 """ 803 The name of the saved Pipeline that `pipe` was loaded from as a member reference 804 (`None` for a pipe given as a `Pipe` object, or one which is not a member). 805 A member loaded from a reference must be registered already: registering it would 806 create an empty pipe, so `sync()` refuses instead. 807 """ 808 if not isinstance(pipe, mrsm.Pipe): 809 return None 810 ref = _pipe_ref(pipe) 811 if self._loaded.get(ref, None) is not pipe: 812 return None 813 for definition in self._definitions: 814 if ref in (definition.get('targets', None) or []): 815 return definition.get('name', None) 816 return self.name or '' 817 818 @property 819 def graph(self) -> bool: 820 """ 821 Whether runs respect the dependencies between members even without traversal 822 (see the `graph` argument). 823 """ 824 if self._graph is not None: 825 return self._graph 826 if not self._definitions: 827 return False 828 return _common([d.get('graph', True) for d in self._definitions], 829 'graph', [d['name'] for d in self._definitions]) 830 831 @graph.setter 832 def graph(self, value: Optional[bool]) -> None: 833 self._graph = None if value is None else bool(value) 834 835 @property 836 def members(self) -> List[Any]: 837 """ 838 The member pipes (or keys) the selection starts from, before traversal 839 (empty for a tags / keys filter). Also available as `targets`. 840 841 Assign a new list (`Pipe` objects, key tuples or `pipe_ref` strings) to change the 842 selection, then `save()` to keep the change: 843 844 ```python 845 daily = mrsm.Pipeline('daily') 846 daily.members = daily.members + [mrsm.Pipe('sql:main', 'weekly')] 847 daily.save() 848 ``` 849 """ 850 return list(self._targets) 851 852 @members.setter 853 def members(self, pipes: Any) -> None: 854 names, targets = _split_selection(pipes) 855 if names: 856 raise ValueError( 857 "Members are pipes (Pipe objects, key tuples or pipe references), not the names " 858 "of saved Pipelines: combine saved Pipelines with `mrsm.Pipeline([...])`." 859 ) 860 members: List[Any] = [] 861 seen = set() 862 for target in targets: 863 if isinstance(target, mrsm.Pipe): 864 ref = _pipe_ref(target) 865 if ref in seen: 866 continue 867 seen.add(ref) 868 members.append(target) 869 self._loaded = { 870 ref: member 871 for ref, member in self._loaded.items() 872 if any(target is member for target in members) 873 } 874 self._targets = members 875 self._filters = {} 876 self._sources = list(members) 877 878 @property 879 def targets(self) -> List[Any]: 880 """The member pipes (or keys) the selection starts from (the same as `members`).""" 881 return list(self._targets) 882 883 @targets.setter 884 def targets(self, pipes: Any) -> None: 885 self.members = pipes 886 887 @property 888 def pipes(self) -> List[mrsm.Pipe]: 889 """ 890 Every pipe the selection resolves to (members plus the traversed parents and children), 891 in dependency order. This plans the selection, registering nothing: member pipes that 892 are not registered yet come last (their dependencies are unknown until they are). 893 """ 894 plan = self.plan() 895 planned = plan.pipes 896 return planned + [pipe for pipe in plan.unregistered if pipe not in planned] 897 898 def _unregistered(self) -> List[mrsm.Pipe]: 899 """Member pipes that are not registered on their instance (temporary pipes excluded).""" 900 return [ 901 target 902 for target in self._targets 903 if isinstance(target, mrsm.Pipe) 904 and not target.temporary 905 and target.get_id() is None 906 ] 907 908 def _execution_defaults(self, explicit: Dict[str, Any]) -> Dict[str, Any]: 909 """The saved execution defaults of the bound definitions (explicit options win).""" 910 from meerschaum.core.Pipeline._saved import validate_execution_defaults 911 merged: Dict[str, Any] = {} 912 owners: Dict[str, str] = {} 913 for definition in self._definitions: 914 defaults = validate_execution_defaults(definition.get('defaults', None) or {}) 915 for key, value in defaults.items(): 916 if key in merged and merged[key] != value and key not in explicit: 917 raise ValueError( 918 f"The saved Pipelines '{owners[key]}' and '{definition['name']}' have " 919 f"different defaults for `{key}`; pass `{key}=` to choose one." 920 ) 921 if key not in merged: 922 merged[key] = value 923 owners[key] = definition['name'] 924 return merged 925 926 def _request(self, targets: Optional[List[Any]] = None, **kwargs: Any) -> Dict[str, Any]: 927 if self._definitions: 928 kwargs = {**self._execution_defaults(kwargs), **kwargs} 929 kwargs["pipeline_definitions"] = deepcopy(self._definitions) 930 run_name = self.name or ('combined' if len(self._definitions) > 1 else None) 931 if run_name: 932 kwargs.setdefault("run_name", run_name) 933 if self.graph: 934 kwargs.setdefault("graph", True) 935 parents = kwargs.pop('parents', self.parents) 936 children = kwargs.pop('children', self.children) 937 for reserved in ('instance', 'targets', 'filters'): 938 if reserved in kwargs: 939 raise ValueError(f"Set Pipeline {reserved} on the definition, not as an execution option.") 940 return build_request( 941 self.instance, 942 targets=(self._targets if targets is None else targets), 943 filters=self._filters, 944 parents=parents, 945 children=children, 946 **kwargs 947 ) 948 949 def _register_targets(self) -> Optional[SuccessTuple]: 950 """The engine reads the graph from the instance, so register the members first.""" 951 for target in self._targets: 952 if not isinstance(target, mrsm.Pipe) or target.temporary: 953 continue 954 if target.get_id() is None: 955 owner = self._owner_of(target) 956 if owner is not None: 957 return False, ( 958 f"{target} is a member of Pipeline '{owner}' but is not registered " 959 f"on '{target.instance_keys}'. Register it, or remove it from the " 960 "Pipeline." 961 ) 962 success, message = target.register() 963 if not success: 964 return False, message 965 return None 966 967 def plan( 968 self, 969 begin: Any = _UNSET_BOUND, 970 end: Any = _UNSET_BOUND, 971 expected: bool = False, 972 **kwargs: Any 973 ) -> Plan: 974 """ 975 Plan the run without moving any data (and without registering anything). 976 977 Parameters 978 ---------- 979 begin, end: Union[datetime, int, str, None], default None 980 Optionally bound the sync window. `None` and `''` leave it to the planner 981 (a root syncs from its sync time minus the backtrack). 982 983 expected: bool, default False 984 If `False`, every node is planned against the state as it is now: a node whose 985 parents run first is provisional (`*`) and is planned again by the executor once 986 they finish. If `True`, return what `mrsm plan pipes` prints: each provisional node 987 re-planned as if every node that runs before it synced new rows after its 988 watermark, so its window starts where the executor will start it (often the start 989 of a parent's chunk-grid cell). Only the start is predicted; the executor caps the 990 end at the parents' new watermarks. 991 992 Returns 993 ------- 994 A `Plan` (`str(plan)` is the table; `plan.nodes` is the data). Member pipes that are 995 not registered yet are in `plan.unregistered` and named in `plan.warnings`. 996 """ 997 options = {key: value for key, value in {'begin': begin, 'end': end, **kwargs}.items() 998 if value is not _UNSET_BOUND} 999 unregistered = self._unregistered() 1000 request = self._request( 1001 targets=( 1002 [target for target in self._targets if target not in unregistered] 1003 if unregistered 1004 else None 1005 ), 1006 **options 1007 ) 1008 if expected: 1009 request['expected'] = True 1010 data = call('engine_plan', request) 1011 return Plan( 1012 data, 1013 pipeline=self, 1014 options={ 1015 key: value 1016 for key, value in options.items() 1017 if value not in (None, '') 1018 }, 1019 unregistered=unregistered, 1020 ) 1021 1022 def sync(self, begin: Any = _UNSET_BOUND, end: Any = _UNSET_BOUND, **kwargs: Any) -> RunResult: 1023 """ 1024 Plan and run the selection, registering member pipes that are not registered yet 1025 (a saved Pipeline's members must already be registered). 1026 1027 Parameters 1028 ---------- 1029 begin, end: Union[datetime, int, str, None] 1030 Bound the sync window of the root nodes (other nodes are clamped to it). 1031 `None` and `''` leave the window to the planner: a root syncs from its sync time 1032 minus the backtrack, as a bare `pipe.sync()` does by default. (A bare 1033 `pipe.sync(begin=None)` means "no lower bound"; in an engine run it does not.) 1034 1035 workers: Optional[int], default None 1036 Run up to this many nodes at once (default `system.engine.workers`). 1037 1038 force, retries, min_seconds, check_existing, enforce_dtypes, chunksize, params, yes: 1039 The `Pipe.sync()` options, applied to every node. `yes` (or `force`) confirms a 1040 `rebuild`; without it a node whose definition changed is left `rebuild_pending`. 1041 1042 resume: Optional[str], default None 1043 Re-plan only the nodes that failed or never ran in this earlier run ID. 1044 1045 on_event: Optional[Callable[[Dict[str, Any]], Any]], default None 1046 Called with every event of the run as it happens (`run_started`, `node_planned`, 1047 `node_started`, `chunk_started`, `chunk_finished`, `node_finished`, `node_failed`, 1048 `node_skipped`, `watermark_advanced`, ..., `run_finished`), each a `dict` with 1049 `event`, `run_id`, `seq`, `ts` (an aware UTC `datetime`), `pipe_ref` and the 1050 event's own fields. It runs on this thread; the sync work runs on the engine's 1051 threads without the GIL. If it raises an `Exception`, the run continues, the error 1052 is warned about once and kept in `RunResult.callback_error`, and no further events 1053 are delivered. `KeyboardInterrupt` cancels the run. 1054 1055 Returns 1056 ------- 1057 A `RunResult`: a `SuccessTuple` with `run_id`, `plan`, `outcomes`, `events()`, the 1058 totals `inserted`, `updated`, `upserted` and `deleted` over every node, and `duration` 1059 (the run's wall-clock seconds). 1060 """ 1061 on_event = kwargs.pop('on_event', None) 1062 failure = self._register_targets() 1063 if failure is not None: 1064 return RunResult(*failure) 1065 options = {key: value for key, value in {'begin': begin, 'end': end, **kwargs}.items() 1066 if value is not _UNSET_BOUND} 1067 try: 1068 request = self._request(**options) 1069 except ValueError as e: 1070 return RunResult(False, str(e)) 1071 callback = EventCallback(on_event) if on_event is not None else None 1072 if callback is None: 1073 data = call('engine_run', request) 1074 else: 1075 data = call('engine_run', request, on_event=callback) 1076 if callback.warning is not None: 1077 data['warnings'] = list(data.get('warnings', None) or []) + [callback.warning] 1078 run = RunResult(data['success'], data['message'], data, pipeline=self) 1079 run._instance_request = self._instance_request() 1080 if callback is not None: 1081 run.callback_error = callback.error 1082 return run 1083 1084 def run(self, begin: Any = _UNSET_BOUND, end: Any = _UNSET_BOUND, **kwargs: Any) -> RunResult: 1085 """An alias of `Pipeline.sync()`.""" 1086 return self.sync(begin=begin, end=end, **kwargs) 1087 1088 def _instance_request(self) -> Dict[str, Any]: 1089 return build_request(self.instance) 1090 1091 def _members_to_save(self) -> List[Any]: 1092 """The members a saved definition stores: the members, or the pipes the filters match.""" 1093 if self._targets: 1094 return list(self._targets) 1095 if not self._filters: 1096 return [] 1097 return [ 1098 pipe 1099 for pipe in mrsm.get_pipes( 1100 connector_keys=self._filters.get('connector_keys', None), 1101 metric_keys=self._filters.get('metric_keys', None), 1102 location_keys=self._filters.get('location_keys', None), 1103 tags=self._filters.get('tags', None), 1104 instance=self.instance, 1105 as_list=True, 1106 ) 1107 if str(pipe.connector_keys) != 'mrsm' 1108 ] 1109 1110 def _settings_to_carry(self, with_defaults: bool = True) -> Dict[str, Any]: 1111 """The description, defaults and layout a new saved definition starts from.""" 1112 definition = self.definition 1113 if definition is not None: 1114 return { 1115 'description': definition.get('description', None), 1116 'defaults': definition.get('defaults', None) if with_defaults else None, 1117 'layout': definition.get('layout', None), 1118 } 1119 if self._definitions and with_defaults: 1120 try: 1121 return {'defaults': self._execution_defaults({})} 1122 except ValueError as e: 1123 raise ValueError(f"{e} (pass `defaults={{...}}` to `save()`)") from None 1124 return {} 1125 1126 def save( 1127 self, 1128 name: Optional[str] = None, 1129 overwrite: bool = False, 1130 description: Optional[str] = None, 1131 defaults: Optional[Dict[str, Any]] = None, 1132 ) -> SuccessTuple: 1133 """ 1134 Save this selection's members, traversal and execution mode under `name` 1135 (default: `self.name`). Saving never changes the members' tags or registrations. 1136 1137 Parameters 1138 ---------- 1139 name: Optional[str], default None 1140 The name to save under (letters, numbers, dots, underscores and hyphens). 1141 1142 overwrite: bool, default False 1143 A different Pipeline already saved under `name` is only replaced with 1144 `overwrite=True` (its description, defaults and layout are kept unless passed). 1145 A Pipeline loaded from `name` saves its changes without it, unless someone else 1146 saved a newer revision since it was loaded (load it again). Saving the same 1147 members, traversal and execution mode again is a no-op success. 1148 1149 description: Optional[str], default None 1150 Set the saved description. 1151 1152 defaults: Optional[Dict[str, Any]], default None 1153 Set the saved execution defaults (`begin`, `end`, `workers`, `retries`, 1154 `min_seconds`, `chunksize`, `check_existing`, `enforce_dtypes`, `force`, `params`, 1155 `yes`), used by every run of the saved Pipeline unless a run passes its own. 1156 1157 Returns 1158 ------- 1159 A `SuccessTuple`. On success, `self.name` and `self.definition` are the saved ones. 1160 """ 1161 from meerschaum.core.Pipeline._saved import ( 1162 _read, _refs, registration, save_pipeline, validate_name, 1163 ) 1164 name = name or self.name 1165 if not name: 1166 return False, "Name the Pipeline to save it, e.g. `pipeline.save('daily')`." 1167 try: 1168 validate_name(name) 1169 except ValueError as e: 1170 return False, f"Cannot save Pipeline {name!r}: {e}" 1171 1172 registry = self.instance 1173 try: 1174 members = self._members_to_save() 1175 graph = True if self._graph is None and not self._definitions else self.graph 1176 registration_pipe = registration(name, registry) 1177 registry_keys = registration_pipe.instance_keys 1178 exists = registration_pipe.get_id() is not None 1179 source = self.definition 1180 editing = ( 1181 exists 1182 and source is not None 1183 and source.get('name', None) == name 1184 and str(source.get('registry', None)) == str(registry_keys) 1185 ) 1186 expected_revision = None 1187 if exists and not overwrite: 1188 ### Saving what is already saved is a no-op (no new revision for other editors 1189 ### to trip over). Otherwise a Pipeline loaded from `name` saves its changes 1190 ### against the revision it loaded, and replacing a different one needs 1191 ### `overwrite`. 1192 current = _read(name, registry) 1193 same_selection = ( 1194 sorted(current.get('targets', None) or []) == _refs(members, registry_keys) 1195 and get_depth(current.get('parents', None)) == self.parents 1196 and get_depth(current.get('children', None)) == self.children 1197 and current.get('graph', True) == graph 1198 ) 1199 unchanged = ( 1200 (description is None or description == current.get('description', '')) 1201 and (defaults is None or defaults == current.get('defaults', {})) 1202 ) 1203 if same_selection and unchanged: 1204 self._bind([current]) 1205 self.name = name 1206 return True, ( 1207 f"Pipeline '{name}' is already saved on '{registry_keys}' " 1208 "with these members." 1209 ) 1210 if not editing and not same_selection: 1211 return False, ( 1212 f"A different Pipeline '{name}' is already saved on '{registry_keys}'. " 1213 f"Load it with `mrsm.Pipeline({name!r})` to change it, or pass " 1214 "`overwrite=True` to replace it." 1215 ) 1216 expected_revision = source['revision'] if editing else current['revision'] 1217 1218 if exists: 1219 definition = save_pipeline( 1220 name, members, registry, self.parents, self.children, 1221 expected_revision=expected_revision, 1222 description=description, 1223 defaults=defaults, 1224 graph=graph, 1225 ) 1226 else: 1227 carried = self._settings_to_carry(with_defaults=(defaults is None)) 1228 definition = save_pipeline( 1229 name, members, registry, self.parents, self.children, 1230 description=(description if description is not None else carried.get('description', None)), 1231 defaults=(defaults if defaults is not None else carried.get('defaults', None)), 1232 layout=carried.get('layout', None), 1233 create=True, 1234 graph=graph, 1235 ) 1236 except Exception as e: 1237 return False, f"Failed to save Pipeline '{name}' on '{registry}': {e}" 1238 1239 self._bind([definition]) 1240 self.name = name 1241 num_members = len(definition.get('targets', None) or []) 1242 try: 1243 num_unregistered = len(self._unregistered()) 1244 except Exception: 1245 num_unregistered = 0 1246 note = ( 1247 f" {num_unregistered} of them {'is' if num_unregistered == 1 else 'are'} not " 1248 "registered yet: `sync()` registers them." 1249 if num_unregistered 1250 else '' 1251 ) 1252 return True, ( 1253 f"Saved Pipeline '{name}' on '{definition['registry']}' " 1254 f"({num_members} member{'s' if num_members != 1 else ''}, " 1255 f"revision {definition['revision']}).{note}" 1256 ) 1257 1258 def exists(self) -> bool: 1259 """Whether a Pipeline is saved under this Pipeline's `name` on its instance.""" 1260 if not self.name: 1261 return False 1262 from meerschaum.core.Pipeline._saved import registration 1263 try: 1264 return registration(self.name, self.instance).get_id() is not None 1265 except ValueError: 1266 return False 1267 1268 def delete(self) -> SuccessTuple: 1269 """ 1270 Delete the saved Pipeline (its definition and the membership tags it owns). 1271 The member pipes and their data are kept, and this object stays usable as an unsaved 1272 selection. Scheduled jobs that sync the Pipeline must be removed first. 1273 """ 1274 from meerschaum.core.Pipeline._saved import delete_pipeline 1275 if not self.name: 1276 return False, "This Pipeline has no name, so it is not saved." 1277 try: 1278 success, message = delete_pipeline(self.name, self.instance) 1279 except Exception as e: 1280 return False, f"Failed to delete Pipeline '{self.name}': {e}" 1281 if not success: 1282 return success, message 1283 self._bind([]) 1284 return True, ( 1285 f"Deleted Pipeline '{self.name}' from '{self.instance}' " 1286 "(its member pipes and their data are kept)." 1287 ) 1288 1289 def rename(self, new_name: str) -> SuccessTuple: 1290 """ 1291 Rename the saved Pipeline (its identity and run history are kept). 1292 Scheduled jobs that sync the Pipeline must be removed first. 1293 """ 1294 from meerschaum.core.Pipeline._saved import rename_pipeline 1295 if not self.exists(): 1296 return False, f"Pipeline '{self.name}' is not saved on '{self.instance}'." 1297 definition = self.definition 1298 expected_revision = ( 1299 definition['revision'] 1300 if definition is not None and definition.get('name', None) == self.name 1301 else None 1302 ) 1303 old_name = self.name 1304 try: 1305 renamed = rename_pipeline( 1306 old_name, new_name, self.instance, expected_revision=expected_revision, 1307 ) 1308 except Exception as e: 1309 return False, f"Failed to rename Pipeline '{old_name}': {e}" 1310 self._bind([renamed]) 1311 self.name = new_name 1312 if old_name == new_name: 1313 return True, f"Pipeline '{old_name}' already has that name." 1314 return True, f"Renamed Pipeline '{old_name}' to '{new_name}' on '{self.instance}'." 1315 1316 def copy(self, new_name: str, instance: Union[str, Any, None] = None) -> SuccessTuple: 1317 """ 1318 Save a copy of this Pipeline as `new_name`, on `instance` (default: this Pipeline's 1319 instance). The copy has the same members (which keep their own instances), traversal, 1320 description and defaults, and a new identity; this object is unchanged. 1321 """ 1322 from meerschaum.core.Pipeline._saved import save_pipeline, validate_name 1323 try: 1324 validate_name(new_name) 1325 except ValueError as e: 1326 return False, f"Cannot copy to {new_name!r}: {e}" 1327 registry = _registry_of(instance) if instance is not None else self.instance 1328 try: 1329 carried = self._settings_to_carry() 1330 definition = save_pipeline( 1331 new_name, self._members_to_save(), registry, self.parents, self.children, 1332 description=carried.get('description', None), 1333 defaults=carried.get('defaults', None), 1334 layout=carried.get('layout', None), 1335 create=True, 1336 ) 1337 except Exception as e: 1338 return False, f"Failed to copy to Pipeline '{new_name}' on '{registry}': {e}" 1339 source = f"Pipeline '{self.name}'" if self.name else 'this selection' 1340 return True, f"Copied {source} to Pipeline '{new_name}' on '{definition['registry']}'." 1341 1342 def analyze(self) -> Dict[str, Any]: 1343 """ 1344 Resolve the saved Pipeline's members and traversal and preview the membership tags 1345 (`pipeline-NAME`) it would add or remove, without changing anything (like 1346 `mrsm analyze pipelines NAME`). Apply the proposal with `apply_analysis()`. 1347 1348 Returns 1349 ------- 1350 The proposal: `resolved` (each pipe and why it is included), `changes` (tag edits), 1351 `shared` (pipes other Pipelines also use), `errors` and a `fingerprint`. 1352 """ 1353 from meerschaum.core.Pipeline._saved import analyze_pipeline 1354 if not self.name: 1355 raise ValueError("Save the Pipeline before analyzing it, e.g. `pipeline.save('daily')`.") 1356 return analyze_pipeline(self.name, self.instance) 1357 1358 def apply_analysis(self, proposal: Optional[Dict[str, Any]] = None) -> SuccessTuple: 1359 """ 1360 Apply the membership tag changes of a proposal from `analyze()` (without one, analyze 1361 now and apply that, like `mrsm analyze pipelines NAME --yes`). A proposal whose inputs 1362 changed since it was made is refused: analyze again and review the new one. 1363 """ 1364 from meerschaum.core.Pipeline._saved import _read, apply_pipeline_analysis 1365 if not self.name: 1366 return False, "This Pipeline has no name, so it is not saved." 1367 try: 1368 proposal = proposal if proposal is not None else self.analyze() 1369 result = apply_pipeline_analysis(proposal, self.instance) 1370 except Exception as e: 1371 return False, f"Failed to apply the analysis of Pipeline '{self.name}': {e}" 1372 message = result.get('message', '') 1373 failures = [row for row in result.get('results', None) or [] if not row.get('success', True)] 1374 if failures: 1375 message += ''.join(f"\n{row['pipe_ref']}: {row['message']}" for row in failures) 1376 try: 1377 self._bind([_read(self.name, self.instance)]) 1378 except Exception: 1379 pass 1380 return bool(result.get('success', False)), message 1381 1382 def schedule( 1383 self, 1384 schedule: str, 1385 name: Optional[str] = None, 1386 workers: Optional[int] = None, 1387 retries: int = 0, 1388 force: bool = False, 1389 executor_keys: Optional[str] = None, 1390 start: bool = True, 1391 ) -> 'mrsm.Job': 1392 """ 1393 Create (and start) a background job that syncs the saved Pipeline on a schedule, 1394 like Manage → Schedule on the console's Pipelines page. 1395 1396 Parameters 1397 ---------- 1398 schedule: str 1399 When to run, e.g. `'every 10 minutes'` or `'daily starting 00:30'`. 1400 1401 name: Optional[str], default None 1402 The job's name (default `pipeline-NAME`). 1403 1404 workers: Optional[int], default None 1405 Run up to this many pipes at once (`--workers`). 1406 1407 retries: int, default 0 1408 Retry a failed pipe this many times (`--retries`). 1409 1410 force: bool, default False 1411 Confirm rebuilds without asking (`--force`). 1412 1413 executor_keys: Optional[str], default None 1414 Where the job runs (`'local'`, `'systemd'` or an `api:` instance; default: as `Job`). 1415 1416 start: bool, default True 1417 Start the job now (a job that is already running is left running). 1418 1419 Returns 1420 ------- 1421 The `mrsm.Job`. Raises `ValueError` for a Pipeline that is not saved, an invalid 1422 schedule, or a job of that name that runs a different command, and `RuntimeError` if 1423 the job does not start. 1424 """ 1425 from meerschaum.utils.schedule import parse_schedule 1426 from meerschaum.core.Pipeline._saved import job_arguments 1427 if not self.exists(): 1428 raise ValueError( 1429 "Save the Pipeline before scheduling it, e.g. `pipeline.save('daily')`." 1430 ) 1431 sysargs = job_arguments( 1432 self.name, self.instance, schedule, workers=workers, retries=retries, force=force, 1433 ) 1434 parse_schedule(str(schedule)) 1435 job_name = name or f'pipeline-{self.name}' 1436 job = mrsm.Job(job_name, executor_keys=executor_keys) 1437 if job.exists(): 1438 if list(job.sysargs) != sysargs: 1439 raise ValueError( 1440 f"A job named '{job_name}' already exists with a different command " 1441 f"({' '.join(job.sysargs)}). Pass `name=` for a new job, or delete that " 1442 "one first." 1443 ) 1444 else: 1445 job = mrsm.Job(job_name, sysargs, executor_keys=executor_keys) 1446 if start and not job.is_running(): 1447 success, message = job.start() 1448 if not success: 1449 raise RuntimeError(f"Job '{job_name}' did not start: {message}") 1450 return job 1451 1452 def get_runs( 1453 self, 1454 limit: int = 20, 1455 status: Optional[str] = None, 1456 all_runs: bool = False, 1457 since: Union[datetime, timedelta, str, None] = None, 1458 ) -> List[Dict[str, Any]]: 1459 """ 1460 Recent runs on the instance, newest first. 1461 1462 Parameters 1463 ---------- 1464 limit: int, default 20 1465 The most runs to return. 1466 1467 status: Optional[str], default None 1468 Only `'running'`, `'succeeded'` or `'failed'` runs. 1469 1470 all_runs: bool, default False 1471 By default a named Pipeline returns its own runs (those of the saved Pipeline, or 1472 whose run IDs carry its name), and an unnamed one the runs which targeted or 1473 contained one of its member pipes. If `True` (or the selection is a tags / keys 1474 filter), return every run on the instance. 1475 1476 since: Union[datetime, timedelta, str, None], default None 1477 Only runs started at or after this time: a `datetime` (naive means UTC), a 1478 `timedelta` (that long ago, e.g. `timedelta(hours=1)`) or an ISO 8601 string. 1479 1480 Returns 1481 ------- 1482 Run summaries: `run` (the stored row), `status`, `duration_seconds` and `counts` 1483 (nodes by status). 1484 """ 1485 request = self._instance_request() 1486 request['limit'] = limit 1487 if status is not None: 1488 request['status'] = status 1489 since = since_text(since) 1490 if since is not None: 1491 request['since'] = since 1492 if all_runs: 1493 return call('engine_runs', request) 1494 if self.name: 1495 ### The runs of the saved Pipeline (and of combined selections that included it), or 1496 ### those whose run IDs carry the name. 1497 request['run_name'] = self.name 1498 return call('engine_runs', request) 1499 pipe_targets = [t for t in self._targets if isinstance(t, mrsm.Pipe)] 1500 if len(pipe_targets) != 1 or len(self._targets) != 1: 1501 runs = call('engine_runs', request) 1502 if pipe_targets: 1503 refs = {_pipe_ref(pipe) for pipe in pipe_targets} 1504 runs = [ 1505 run for run in runs 1506 if refs.intersection(run['run'].get('targets', []) or []) 1507 ] 1508 return runs 1509 request['pipe'] = _pipe_ref(pipe_targets[0]) 1510 return call('engine_runs', request) 1511 1512 def get_run(self, run_id: str) -> Optional[Dict[str, Any]]: 1513 """ 1514 One run with its node rows and the planned-vs-actual Gantt bars 1515 (`None` if the run is unknown). 1516 """ 1517 request = self._instance_request() 1518 request['run_id'] = run_id 1519 return call('engine_run_detail', request) 1520 1521 def get_events(self, run_id: str, after_seq: Optional[int] = None) -> List[Dict[str, Any]]: 1522 """The persisted events of a run, in order (see `RunResult.events()`).""" 1523 request = self._instance_request() 1524 request['run_id'] = run_id 1525 if after_seq is not None: 1526 request['after_seq'] = after_seq 1527 return _decode_events(call('engine_run_events', request)) 1528 1529 def __repr__(self) -> str: 1530 if self.name: 1531 import sys 1532 from meerschaum.config import get_config 1533 if hasattr(sys, 'ps1'): 1534 from meerschaum.utils.formatting._pipes import pipeline_repr 1535 return pipeline_repr(self.name, str(self.instance)) 1536 suffix = f', instance={str(self.instance)!r}' if str(self.instance) != get_config('meerschaum', 'instance') else '' 1537 return f'Pipeline({self.name!r}{suffix})' 1538 if self._definitions: 1539 parts = [repr(self._sources)] 1540 else: 1541 parts = [repr(self._targets)] if self._targets else [repr(self._filters)] 1542 if self.parents: 1543 parts.append(f"parents={self.parents!r}") 1544 if self.children: 1545 parts.append(f"children={self.children!r}") 1546 return f"Pipeline({', '.join(parts)})"
A selection over the pipe graph: member pipes plus traversal depths (design §5.2, D4).
Parameters
pipes (Union[str, mrsm.Pipe, Iterable[...], None], default None): What to select:
- a
strwithout|is the name of a saved Pipeline, loaded frominstance(mrsm.Pipeline('daily');ValueErrorif it does not exist); - a
strwith|is a pipe reference,'instance|connector|metric|location'; - a
Pipe, or a(connector_keys, metric_key[, location_key])tuple, is one member; - a list may mix all of these: the members of every named Pipeline plus the given
pipes (the dictionary
mrsm.get_pipes()returns works too).
- a
- parents (Union[bool, int, None], default None):
Trueincludes every ancestor of the members; anintincludes that many generations. For a saved Pipeline,Nonekeeps its saved setting andFalseturns traversal off. - children (Union[bool, int, None], default None):
Trueincludes every descendant of the members; anintthat many generations. For a saved Pipeline,Nonekeeps its saved setting andFalseturns traversal off. - tags, connector_keys, metric_keys, location_keys (Union[str, List[str], None], default None):
Select the members by the same filters as
-t,-c,-mand-l(negate a value with a leading_). Used whenpipesis not provided. - instance (Union[str, InstanceConnector, None], default None): The instance that keeps the engine state and the saved Pipelines (default: the member pipes' instance, else the configured one). Saved Pipelines are loaded from here.
- name (Optional[str], default None):
The name to save this selection under (
save()), also used in its run IDs. Withoutpipesor filters,Pipeline(name='daily')loads the saved Pipeline. - graph (Optional[bool], default None):
Whether the run respects the dependencies between the members even without traversal
(a child member waits for its parent member and is capped at its watermark).
A saved Pipeline keeps its
graphsetting (defaultTrue), likemrsm sync pipelines NAME; an unsaved selection withoutparents/childrensyncs each member on its own, likemrsm sync pipes.
Examples
>>> pipeline = mrsm.Pipeline(pipe, parents=True)
>>> plan = pipeline.plan()
>>> run = pipeline.sync(workers=4)
>>> run.success, run.run_id, run.inserted
>>> pipeline.save('daily')
>>> mrsm.Pipeline('daily').sync()
641 def __init__( 642 self, 643 pipes: Union[str, mrsm.Pipe, Iterable[Any], None] = None, 644 parents: Union[bool, int, None] = None, 645 children: Union[bool, int, None] = None, 646 tags: Union[str, List[str], None] = None, 647 connector_keys: Union[str, List[str], None] = None, 648 metric_keys: Union[str, List[str], None] = None, 649 location_keys: Union[str, List[str], None] = None, 650 instance: Union[str, Any, None] = None, 651 name: Optional[str] = None, 652 graph: Optional[bool] = None, 653 ): 654 if name is not None and not isinstance(name, str): 655 raise TypeError(f"A Pipeline name must be a string (got {name!r}).") 656 names, targets = _split_selection(pipes) 657 filters = { 658 'tags': tags, 659 'connector_keys': connector_keys, 660 'metric_keys': metric_keys, 661 'location_keys': location_keys, 662 } 663 filters = {k: ([v] if isinstance(v, str) else v) for k, v in filters.items() if v} 664 if not names and not targets and not filters: 665 if not name: 666 raise ValueError( 667 "A Pipeline needs member pipes, a tags / keys filter or the name of a saved " 668 "Pipeline, e.g. `mrsm.Pipeline(pipe)`, `mrsm.Pipeline(tags=['daily'])` or " 669 "`mrsm.Pipeline('daily')`." 670 ) 671 names = [name] 672 if names and filters: 673 raise ValueError( 674 "A saved Pipeline cannot be combined with a tags / keys filter: " 675 "pass the extra pipes themselves." 676 ) 677 definitions = [] 678 registry = None 679 if names: 680 registry = _registry_of(instance) 681 seen = set() 682 for saved_name in names: 683 if saved_name in seen: 684 continue 685 seen.add(saved_name) 686 definitions.append(_read_definition(saved_name, registry)) 687 self._configure( 688 definitions, 689 targets, 690 filters, 691 parents=parents, 692 children=children, 693 instance=(registry if registry is not None else instance), 694 name=name, 695 graph=graph, 696 )
776 @classmethod 777 def load(cls, name: str, instance=None) -> 'Pipeline': 778 """ 779 Load the saved Pipeline `name` from `instance` (default: the configured instance). 780 The same as `mrsm.Pipeline(name, instance=instance)`. 781 """ 782 return cls(name, instance=instance)
Load the saved Pipeline name from instance (default: the configured instance).
The same as mrsm.Pipeline(name, instance=instance).
784 @property 785 def definition(self) -> Optional[Dict[str, Any]]: 786 """ 787 The saved definition this Pipeline was loaded from or saved as (`name`, `registry`, 788 `id`, `revision`, `targets`, `parents`, `children`, `description`, `defaults`, ...), 789 or `None` for an unsaved selection (or one combined from several saved Pipelines). 790 """ 791 return self._definitions[0] if len(self._definitions) == 1 else None
The saved definition this Pipeline was loaded from or saved as (name, registry,
id, revision, targets, parents, children, description, defaults, ...),
or None for an unsaved selection (or one combined from several saved Pipelines).
818 @property 819 def graph(self) -> bool: 820 """ 821 Whether runs respect the dependencies between members even without traversal 822 (see the `graph` argument). 823 """ 824 if self._graph is not None: 825 return self._graph 826 if not self._definitions: 827 return False 828 return _common([d.get('graph', True) for d in self._definitions], 829 'graph', [d['name'] for d in self._definitions])
Whether runs respect the dependencies between members even without traversal
(see the graph argument).
835 @property 836 def members(self) -> List[Any]: 837 """ 838 The member pipes (or keys) the selection starts from, before traversal 839 (empty for a tags / keys filter). Also available as `targets`. 840 841 Assign a new list (`Pipe` objects, key tuples or `pipe_ref` strings) to change the 842 selection, then `save()` to keep the change: 843 844 ```python 845 daily = mrsm.Pipeline('daily') 846 daily.members = daily.members + [mrsm.Pipe('sql:main', 'weekly')] 847 daily.save() 848 ``` 849 """ 850 return list(self._targets)
The member pipes (or keys) the selection starts from, before traversal
(empty for a tags / keys filter). Also available as targets.
Assign a new list (Pipe objects, key tuples or pipe_ref strings) to change the
selection, then save() to keep the change:
daily = mrsm.Pipeline('daily')
daily.members = daily.members + [mrsm.Pipe('sql:main', 'weekly')]
daily.save()
878 @property 879 def targets(self) -> List[Any]: 880 """The member pipes (or keys) the selection starts from (the same as `members`).""" 881 return list(self._targets)
The member pipes (or keys) the selection starts from (the same as members).
887 @property 888 def pipes(self) -> List[mrsm.Pipe]: 889 """ 890 Every pipe the selection resolves to (members plus the traversed parents and children), 891 in dependency order. This plans the selection, registering nothing: member pipes that 892 are not registered yet come last (their dependencies are unknown until they are). 893 """ 894 plan = self.plan() 895 planned = plan.pipes 896 return planned + [pipe for pipe in plan.unregistered if pipe not in planned]
Every pipe the selection resolves to (members plus the traversed parents and children), in dependency order. This plans the selection, registering nothing: member pipes that are not registered yet come last (their dependencies are unknown until they are).
967 def plan( 968 self, 969 begin: Any = _UNSET_BOUND, 970 end: Any = _UNSET_BOUND, 971 expected: bool = False, 972 **kwargs: Any 973 ) -> Plan: 974 """ 975 Plan the run without moving any data (and without registering anything). 976 977 Parameters 978 ---------- 979 begin, end: Union[datetime, int, str, None], default None 980 Optionally bound the sync window. `None` and `''` leave it to the planner 981 (a root syncs from its sync time minus the backtrack). 982 983 expected: bool, default False 984 If `False`, every node is planned against the state as it is now: a node whose 985 parents run first is provisional (`*`) and is planned again by the executor once 986 they finish. If `True`, return what `mrsm plan pipes` prints: each provisional node 987 re-planned as if every node that runs before it synced new rows after its 988 watermark, so its window starts where the executor will start it (often the start 989 of a parent's chunk-grid cell). Only the start is predicted; the executor caps the 990 end at the parents' new watermarks. 991 992 Returns 993 ------- 994 A `Plan` (`str(plan)` is the table; `plan.nodes` is the data). Member pipes that are 995 not registered yet are in `plan.unregistered` and named in `plan.warnings`. 996 """ 997 options = {key: value for key, value in {'begin': begin, 'end': end, **kwargs}.items() 998 if value is not _UNSET_BOUND} 999 unregistered = self._unregistered() 1000 request = self._request( 1001 targets=( 1002 [target for target in self._targets if target not in unregistered] 1003 if unregistered 1004 else None 1005 ), 1006 **options 1007 ) 1008 if expected: 1009 request['expected'] = True 1010 data = call('engine_plan', request) 1011 return Plan( 1012 data, 1013 pipeline=self, 1014 options={ 1015 key: value 1016 for key, value in options.items() 1017 if value not in (None, '') 1018 }, 1019 unregistered=unregistered, 1020 )
Plan the run without moving any data (and without registering anything).
Parameters
- begin, end (Union[datetime, int, str, None], default None):
Optionally bound the sync window.
Noneand''leave it to the planner (a root syncs from its sync time minus the backtrack). - expected (bool, default False):
If
False, every node is planned against the state as it is now: a node whose parents run first is provisional (*) and is planned again by the executor once they finish. IfTrue, return whatmrsm plan pipesprints: each provisional node re-planned as if every node that runs before it synced new rows after its watermark, so its window starts where the executor will start it (often the start of a parent's chunk-grid cell). Only the start is predicted; the executor caps the end at the parents' new watermarks.
Returns
- A
Plan(str(plan)is the table;plan.nodesis the data). Member pipes that are - not registered yet are in
plan.unregisteredand named inplan.warnings.
1022 def sync(self, begin: Any = _UNSET_BOUND, end: Any = _UNSET_BOUND, **kwargs: Any) -> RunResult: 1023 """ 1024 Plan and run the selection, registering member pipes that are not registered yet 1025 (a saved Pipeline's members must already be registered). 1026 1027 Parameters 1028 ---------- 1029 begin, end: Union[datetime, int, str, None] 1030 Bound the sync window of the root nodes (other nodes are clamped to it). 1031 `None` and `''` leave the window to the planner: a root syncs from its sync time 1032 minus the backtrack, as a bare `pipe.sync()` does by default. (A bare 1033 `pipe.sync(begin=None)` means "no lower bound"; in an engine run it does not.) 1034 1035 workers: Optional[int], default None 1036 Run up to this many nodes at once (default `system.engine.workers`). 1037 1038 force, retries, min_seconds, check_existing, enforce_dtypes, chunksize, params, yes: 1039 The `Pipe.sync()` options, applied to every node. `yes` (or `force`) confirms a 1040 `rebuild`; without it a node whose definition changed is left `rebuild_pending`. 1041 1042 resume: Optional[str], default None 1043 Re-plan only the nodes that failed or never ran in this earlier run ID. 1044 1045 on_event: Optional[Callable[[Dict[str, Any]], Any]], default None 1046 Called with every event of the run as it happens (`run_started`, `node_planned`, 1047 `node_started`, `chunk_started`, `chunk_finished`, `node_finished`, `node_failed`, 1048 `node_skipped`, `watermark_advanced`, ..., `run_finished`), each a `dict` with 1049 `event`, `run_id`, `seq`, `ts` (an aware UTC `datetime`), `pipe_ref` and the 1050 event's own fields. It runs on this thread; the sync work runs on the engine's 1051 threads without the GIL. If it raises an `Exception`, the run continues, the error 1052 is warned about once and kept in `RunResult.callback_error`, and no further events 1053 are delivered. `KeyboardInterrupt` cancels the run. 1054 1055 Returns 1056 ------- 1057 A `RunResult`: a `SuccessTuple` with `run_id`, `plan`, `outcomes`, `events()`, the 1058 totals `inserted`, `updated`, `upserted` and `deleted` over every node, and `duration` 1059 (the run's wall-clock seconds). 1060 """ 1061 on_event = kwargs.pop('on_event', None) 1062 failure = self._register_targets() 1063 if failure is not None: 1064 return RunResult(*failure) 1065 options = {key: value for key, value in {'begin': begin, 'end': end, **kwargs}.items() 1066 if value is not _UNSET_BOUND} 1067 try: 1068 request = self._request(**options) 1069 except ValueError as e: 1070 return RunResult(False, str(e)) 1071 callback = EventCallback(on_event) if on_event is not None else None 1072 if callback is None: 1073 data = call('engine_run', request) 1074 else: 1075 data = call('engine_run', request, on_event=callback) 1076 if callback.warning is not None: 1077 data['warnings'] = list(data.get('warnings', None) or []) + [callback.warning] 1078 run = RunResult(data['success'], data['message'], data, pipeline=self) 1079 run._instance_request = self._instance_request() 1080 if callback is not None: 1081 run.callback_error = callback.error 1082 return run
Plan and run the selection, registering member pipes that are not registered yet (a saved Pipeline's members must already be registered).
Parameters
- begin, end (Union[datetime, int, str, None]):
Bound the sync window of the root nodes (other nodes are clamped to it).
Noneand''leave the window to the planner: a root syncs from its sync time minus the backtrack, as a barepipe.sync()does by default. (A barepipe.sync(begin=None)means "no lower bound"; in an engine run it does not.) - workers (Optional[int], default None):
Run up to this many nodes at once (default
system.engine.workers). - force, retries, min_seconds, check_existing, enforce_dtypes, chunksize, params, yes:: The
Pipe.sync()options, applied to every node.yes(orforce) confirms arebuild; without it a node whose definition changed is leftrebuild_pending. - resume (Optional[str], default None): Re-plan only the nodes that failed or never ran in this earlier run ID.
- on_event (Optional[Callable[[Dict[str, Any]], Any]], default None):
Called with every event of the run as it happens (
run_started,node_planned,node_started,chunk_started,chunk_finished,node_finished,node_failed,node_skipped,watermark_advanced, ...,run_finished), each adictwithevent,run_id,seq,ts(an aware UTCdatetime),pipe_refand the event's own fields. It runs on this thread; the sync work runs on the engine's threads without the GIL. If it raises anException, the run continues, the error is warned about once and kept inRunResult.callback_error, and no further events are delivered.KeyboardInterruptcancels the run.
Returns
A
RunResult(aSuccessTuplewithrun_id,plan,outcomes,events(), the):totals
inserted,updated,upsertedanddeletedover every node, andduration- (the run's wall-clock seconds).
1084 def run(self, begin: Any = _UNSET_BOUND, end: Any = _UNSET_BOUND, **kwargs: Any) -> RunResult: 1085 """An alias of `Pipeline.sync()`.""" 1086 return self.sync(begin=begin, end=end, **kwargs)
An alias of Pipeline.sync().
1126 def save( 1127 self, 1128 name: Optional[str] = None, 1129 overwrite: bool = False, 1130 description: Optional[str] = None, 1131 defaults: Optional[Dict[str, Any]] = None, 1132 ) -> SuccessTuple: 1133 """ 1134 Save this selection's members, traversal and execution mode under `name` 1135 (default: `self.name`). Saving never changes the members' tags or registrations. 1136 1137 Parameters 1138 ---------- 1139 name: Optional[str], default None 1140 The name to save under (letters, numbers, dots, underscores and hyphens). 1141 1142 overwrite: bool, default False 1143 A different Pipeline already saved under `name` is only replaced with 1144 `overwrite=True` (its description, defaults and layout are kept unless passed). 1145 A Pipeline loaded from `name` saves its changes without it, unless someone else 1146 saved a newer revision since it was loaded (load it again). Saving the same 1147 members, traversal and execution mode again is a no-op success. 1148 1149 description: Optional[str], default None 1150 Set the saved description. 1151 1152 defaults: Optional[Dict[str, Any]], default None 1153 Set the saved execution defaults (`begin`, `end`, `workers`, `retries`, 1154 `min_seconds`, `chunksize`, `check_existing`, `enforce_dtypes`, `force`, `params`, 1155 `yes`), used by every run of the saved Pipeline unless a run passes its own. 1156 1157 Returns 1158 ------- 1159 A `SuccessTuple`. On success, `self.name` and `self.definition` are the saved ones. 1160 """ 1161 from meerschaum.core.Pipeline._saved import ( 1162 _read, _refs, registration, save_pipeline, validate_name, 1163 ) 1164 name = name or self.name 1165 if not name: 1166 return False, "Name the Pipeline to save it, e.g. `pipeline.save('daily')`." 1167 try: 1168 validate_name(name) 1169 except ValueError as e: 1170 return False, f"Cannot save Pipeline {name!r}: {e}" 1171 1172 registry = self.instance 1173 try: 1174 members = self._members_to_save() 1175 graph = True if self._graph is None and not self._definitions else self.graph 1176 registration_pipe = registration(name, registry) 1177 registry_keys = registration_pipe.instance_keys 1178 exists = registration_pipe.get_id() is not None 1179 source = self.definition 1180 editing = ( 1181 exists 1182 and source is not None 1183 and source.get('name', None) == name 1184 and str(source.get('registry', None)) == str(registry_keys) 1185 ) 1186 expected_revision = None 1187 if exists and not overwrite: 1188 ### Saving what is already saved is a no-op (no new revision for other editors 1189 ### to trip over). Otherwise a Pipeline loaded from `name` saves its changes 1190 ### against the revision it loaded, and replacing a different one needs 1191 ### `overwrite`. 1192 current = _read(name, registry) 1193 same_selection = ( 1194 sorted(current.get('targets', None) or []) == _refs(members, registry_keys) 1195 and get_depth(current.get('parents', None)) == self.parents 1196 and get_depth(current.get('children', None)) == self.children 1197 and current.get('graph', True) == graph 1198 ) 1199 unchanged = ( 1200 (description is None or description == current.get('description', '')) 1201 and (defaults is None or defaults == current.get('defaults', {})) 1202 ) 1203 if same_selection and unchanged: 1204 self._bind([current]) 1205 self.name = name 1206 return True, ( 1207 f"Pipeline '{name}' is already saved on '{registry_keys}' " 1208 "with these members." 1209 ) 1210 if not editing and not same_selection: 1211 return False, ( 1212 f"A different Pipeline '{name}' is already saved on '{registry_keys}'. " 1213 f"Load it with `mrsm.Pipeline({name!r})` to change it, or pass " 1214 "`overwrite=True` to replace it." 1215 ) 1216 expected_revision = source['revision'] if editing else current['revision'] 1217 1218 if exists: 1219 definition = save_pipeline( 1220 name, members, registry, self.parents, self.children, 1221 expected_revision=expected_revision, 1222 description=description, 1223 defaults=defaults, 1224 graph=graph, 1225 ) 1226 else: 1227 carried = self._settings_to_carry(with_defaults=(defaults is None)) 1228 definition = save_pipeline( 1229 name, members, registry, self.parents, self.children, 1230 description=(description if description is not None else carried.get('description', None)), 1231 defaults=(defaults if defaults is not None else carried.get('defaults', None)), 1232 layout=carried.get('layout', None), 1233 create=True, 1234 graph=graph, 1235 ) 1236 except Exception as e: 1237 return False, f"Failed to save Pipeline '{name}' on '{registry}': {e}" 1238 1239 self._bind([definition]) 1240 self.name = name 1241 num_members = len(definition.get('targets', None) or []) 1242 try: 1243 num_unregistered = len(self._unregistered()) 1244 except Exception: 1245 num_unregistered = 0 1246 note = ( 1247 f" {num_unregistered} of them {'is' if num_unregistered == 1 else 'are'} not " 1248 "registered yet: `sync()` registers them." 1249 if num_unregistered 1250 else '' 1251 ) 1252 return True, ( 1253 f"Saved Pipeline '{name}' on '{definition['registry']}' " 1254 f"({num_members} member{'s' if num_members != 1 else ''}, " 1255 f"revision {definition['revision']}).{note}" 1256 )
Save this selection's members, traversal and execution mode under name
(default: self.name). Saving never changes the members' tags or registrations.
Parameters
- name (Optional[str], default None): The name to save under (letters, numbers, dots, underscores and hyphens).
- overwrite (bool, default False):
A different Pipeline already saved under
nameis only replaced withoverwrite=True(its description, defaults and layout are kept unless passed). A Pipeline loaded fromnamesaves its changes without it, unless someone else saved a newer revision since it was loaded (load it again). Saving the same members, traversal and execution mode again is a no-op success. - description (Optional[str], default None): Set the saved description.
- defaults (Optional[Dict[str, Any]], default None):
Set the saved execution defaults (
begin,end,workers,retries,min_seconds,chunksize,check_existing,enforce_dtypes,force,params,yes), used by every run of the saved Pipeline unless a run passes its own.
Returns
- A
SuccessTuple. On success,self.nameandself.definitionare the saved ones.
1258 def exists(self) -> bool: 1259 """Whether a Pipeline is saved under this Pipeline's `name` on its instance.""" 1260 if not self.name: 1261 return False 1262 from meerschaum.core.Pipeline._saved import registration 1263 try: 1264 return registration(self.name, self.instance).get_id() is not None 1265 except ValueError: 1266 return False
Whether a Pipeline is saved under this Pipeline's name on its instance.
1268 def delete(self) -> SuccessTuple: 1269 """ 1270 Delete the saved Pipeline (its definition and the membership tags it owns). 1271 The member pipes and their data are kept, and this object stays usable as an unsaved 1272 selection. Scheduled jobs that sync the Pipeline must be removed first. 1273 """ 1274 from meerschaum.core.Pipeline._saved import delete_pipeline 1275 if not self.name: 1276 return False, "This Pipeline has no name, so it is not saved." 1277 try: 1278 success, message = delete_pipeline(self.name, self.instance) 1279 except Exception as e: 1280 return False, f"Failed to delete Pipeline '{self.name}': {e}" 1281 if not success: 1282 return success, message 1283 self._bind([]) 1284 return True, ( 1285 f"Deleted Pipeline '{self.name}' from '{self.instance}' " 1286 "(its member pipes and their data are kept)." 1287 )
Delete the saved Pipeline (its definition and the membership tags it owns). The member pipes and their data are kept, and this object stays usable as an unsaved selection. Scheduled jobs that sync the Pipeline must be removed first.
1289 def rename(self, new_name: str) -> SuccessTuple: 1290 """ 1291 Rename the saved Pipeline (its identity and run history are kept). 1292 Scheduled jobs that sync the Pipeline must be removed first. 1293 """ 1294 from meerschaum.core.Pipeline._saved import rename_pipeline 1295 if not self.exists(): 1296 return False, f"Pipeline '{self.name}' is not saved on '{self.instance}'." 1297 definition = self.definition 1298 expected_revision = ( 1299 definition['revision'] 1300 if definition is not None and definition.get('name', None) == self.name 1301 else None 1302 ) 1303 old_name = self.name 1304 try: 1305 renamed = rename_pipeline( 1306 old_name, new_name, self.instance, expected_revision=expected_revision, 1307 ) 1308 except Exception as e: 1309 return False, f"Failed to rename Pipeline '{old_name}': {e}" 1310 self._bind([renamed]) 1311 self.name = new_name 1312 if old_name == new_name: 1313 return True, f"Pipeline '{old_name}' already has that name." 1314 return True, f"Renamed Pipeline '{old_name}' to '{new_name}' on '{self.instance}'."
Rename the saved Pipeline (its identity and run history are kept). Scheduled jobs that sync the Pipeline must be removed first.
1316 def copy(self, new_name: str, instance: Union[str, Any, None] = None) -> SuccessTuple: 1317 """ 1318 Save a copy of this Pipeline as `new_name`, on `instance` (default: this Pipeline's 1319 instance). The copy has the same members (which keep their own instances), traversal, 1320 description and defaults, and a new identity; this object is unchanged. 1321 """ 1322 from meerschaum.core.Pipeline._saved import save_pipeline, validate_name 1323 try: 1324 validate_name(new_name) 1325 except ValueError as e: 1326 return False, f"Cannot copy to {new_name!r}: {e}" 1327 registry = _registry_of(instance) if instance is not None else self.instance 1328 try: 1329 carried = self._settings_to_carry() 1330 definition = save_pipeline( 1331 new_name, self._members_to_save(), registry, self.parents, self.children, 1332 description=carried.get('description', None), 1333 defaults=carried.get('defaults', None), 1334 layout=carried.get('layout', None), 1335 create=True, 1336 ) 1337 except Exception as e: 1338 return False, f"Failed to copy to Pipeline '{new_name}' on '{registry}': {e}" 1339 source = f"Pipeline '{self.name}'" if self.name else 'this selection' 1340 return True, f"Copied {source} to Pipeline '{new_name}' on '{definition['registry']}'."
Save a copy of this Pipeline as new_name, on instance (default: this Pipeline's
instance). The copy has the same members (which keep their own instances), traversal,
description and defaults, and a new identity; this object is unchanged.
1342 def analyze(self) -> Dict[str, Any]: 1343 """ 1344 Resolve the saved Pipeline's members and traversal and preview the membership tags 1345 (`pipeline-NAME`) it would add or remove, without changing anything (like 1346 `mrsm analyze pipelines NAME`). Apply the proposal with `apply_analysis()`. 1347 1348 Returns 1349 ------- 1350 The proposal: `resolved` (each pipe and why it is included), `changes` (tag edits), 1351 `shared` (pipes other Pipelines also use), `errors` and a `fingerprint`. 1352 """ 1353 from meerschaum.core.Pipeline._saved import analyze_pipeline 1354 if not self.name: 1355 raise ValueError("Save the Pipeline before analyzing it, e.g. `pipeline.save('daily')`.") 1356 return analyze_pipeline(self.name, self.instance)
Resolve the saved Pipeline's members and traversal and preview the membership tags
(pipeline-NAME) it would add or remove, without changing anything (like
mrsm analyze pipelines NAME). Apply the proposal with apply_analysis().
Returns
The proposal (
resolved(each pipe and why it is included),changes(tag edits),):shared(pipes other Pipelines also use),errorsand afingerprint.
1358 def apply_analysis(self, proposal: Optional[Dict[str, Any]] = None) -> SuccessTuple: 1359 """ 1360 Apply the membership tag changes of a proposal from `analyze()` (without one, analyze 1361 now and apply that, like `mrsm analyze pipelines NAME --yes`). A proposal whose inputs 1362 changed since it was made is refused: analyze again and review the new one. 1363 """ 1364 from meerschaum.core.Pipeline._saved import _read, apply_pipeline_analysis 1365 if not self.name: 1366 return False, "This Pipeline has no name, so it is not saved." 1367 try: 1368 proposal = proposal if proposal is not None else self.analyze() 1369 result = apply_pipeline_analysis(proposal, self.instance) 1370 except Exception as e: 1371 return False, f"Failed to apply the analysis of Pipeline '{self.name}': {e}" 1372 message = result.get('message', '') 1373 failures = [row for row in result.get('results', None) or [] if not row.get('success', True)] 1374 if failures: 1375 message += ''.join(f"\n{row['pipe_ref']}: {row['message']}" for row in failures) 1376 try: 1377 self._bind([_read(self.name, self.instance)]) 1378 except Exception: 1379 pass 1380 return bool(result.get('success', False)), message
Apply the membership tag changes of a proposal from analyze() (without one, analyze
now and apply that, like mrsm analyze pipelines NAME --yes). A proposal whose inputs
changed since it was made is refused: analyze again and review the new one.
1382 def schedule( 1383 self, 1384 schedule: str, 1385 name: Optional[str] = None, 1386 workers: Optional[int] = None, 1387 retries: int = 0, 1388 force: bool = False, 1389 executor_keys: Optional[str] = None, 1390 start: bool = True, 1391 ) -> 'mrsm.Job': 1392 """ 1393 Create (and start) a background job that syncs the saved Pipeline on a schedule, 1394 like Manage → Schedule on the console's Pipelines page. 1395 1396 Parameters 1397 ---------- 1398 schedule: str 1399 When to run, e.g. `'every 10 minutes'` or `'daily starting 00:30'`. 1400 1401 name: Optional[str], default None 1402 The job's name (default `pipeline-NAME`). 1403 1404 workers: Optional[int], default None 1405 Run up to this many pipes at once (`--workers`). 1406 1407 retries: int, default 0 1408 Retry a failed pipe this many times (`--retries`). 1409 1410 force: bool, default False 1411 Confirm rebuilds without asking (`--force`). 1412 1413 executor_keys: Optional[str], default None 1414 Where the job runs (`'local'`, `'systemd'` or an `api:` instance; default: as `Job`). 1415 1416 start: bool, default True 1417 Start the job now (a job that is already running is left running). 1418 1419 Returns 1420 ------- 1421 The `mrsm.Job`. Raises `ValueError` for a Pipeline that is not saved, an invalid 1422 schedule, or a job of that name that runs a different command, and `RuntimeError` if 1423 the job does not start. 1424 """ 1425 from meerschaum.utils.schedule import parse_schedule 1426 from meerschaum.core.Pipeline._saved import job_arguments 1427 if not self.exists(): 1428 raise ValueError( 1429 "Save the Pipeline before scheduling it, e.g. `pipeline.save('daily')`." 1430 ) 1431 sysargs = job_arguments( 1432 self.name, self.instance, schedule, workers=workers, retries=retries, force=force, 1433 ) 1434 parse_schedule(str(schedule)) 1435 job_name = name or f'pipeline-{self.name}' 1436 job = mrsm.Job(job_name, executor_keys=executor_keys) 1437 if job.exists(): 1438 if list(job.sysargs) != sysargs: 1439 raise ValueError( 1440 f"A job named '{job_name}' already exists with a different command " 1441 f"({' '.join(job.sysargs)}). Pass `name=` for a new job, or delete that " 1442 "one first." 1443 ) 1444 else: 1445 job = mrsm.Job(job_name, sysargs, executor_keys=executor_keys) 1446 if start and not job.is_running(): 1447 success, message = job.start() 1448 if not success: 1449 raise RuntimeError(f"Job '{job_name}' did not start: {message}") 1450 return job
Create (and start) a background job that syncs the saved Pipeline on a schedule, like Manage → Schedule on the console's Pipelines page.
Parameters
- schedule (str):
When to run, e.g.
'every 10 minutes'or'daily starting 00:30'. - name (Optional[str], default None):
The job's name (default
pipeline-NAME). - workers (Optional[int], default None):
Run up to this many pipes at once (
--workers). - retries (int, default 0):
Retry a failed pipe this many times (
--retries). - force (bool, default False):
Confirm rebuilds without asking (
--force). - executor_keys (Optional[str], default None):
Where the job runs (
'local','systemd'or anapi:instance; default: asJob). - start (bool, default True): Start the job now (a job that is already running is left running).
Returns
- The
mrsm.Job. RaisesValueErrorfor a Pipeline that is not saved, an invalid - schedule, or a job of that name that runs a different command, and
RuntimeErrorif - the job does not start.
1452 def get_runs( 1453 self, 1454 limit: int = 20, 1455 status: Optional[str] = None, 1456 all_runs: bool = False, 1457 since: Union[datetime, timedelta, str, None] = None, 1458 ) -> List[Dict[str, Any]]: 1459 """ 1460 Recent runs on the instance, newest first. 1461 1462 Parameters 1463 ---------- 1464 limit: int, default 20 1465 The most runs to return. 1466 1467 status: Optional[str], default None 1468 Only `'running'`, `'succeeded'` or `'failed'` runs. 1469 1470 all_runs: bool, default False 1471 By default a named Pipeline returns its own runs (those of the saved Pipeline, or 1472 whose run IDs carry its name), and an unnamed one the runs which targeted or 1473 contained one of its member pipes. If `True` (or the selection is a tags / keys 1474 filter), return every run on the instance. 1475 1476 since: Union[datetime, timedelta, str, None], default None 1477 Only runs started at or after this time: a `datetime` (naive means UTC), a 1478 `timedelta` (that long ago, e.g. `timedelta(hours=1)`) or an ISO 8601 string. 1479 1480 Returns 1481 ------- 1482 Run summaries: `run` (the stored row), `status`, `duration_seconds` and `counts` 1483 (nodes by status). 1484 """ 1485 request = self._instance_request() 1486 request['limit'] = limit 1487 if status is not None: 1488 request['status'] = status 1489 since = since_text(since) 1490 if since is not None: 1491 request['since'] = since 1492 if all_runs: 1493 return call('engine_runs', request) 1494 if self.name: 1495 ### The runs of the saved Pipeline (and of combined selections that included it), or 1496 ### those whose run IDs carry the name. 1497 request['run_name'] = self.name 1498 return call('engine_runs', request) 1499 pipe_targets = [t for t in self._targets if isinstance(t, mrsm.Pipe)] 1500 if len(pipe_targets) != 1 or len(self._targets) != 1: 1501 runs = call('engine_runs', request) 1502 if pipe_targets: 1503 refs = {_pipe_ref(pipe) for pipe in pipe_targets} 1504 runs = [ 1505 run for run in runs 1506 if refs.intersection(run['run'].get('targets', []) or []) 1507 ] 1508 return runs 1509 request['pipe'] = _pipe_ref(pipe_targets[0]) 1510 return call('engine_runs', request)
Recent runs on the instance, newest first.
Parameters
- limit (int, default 20): The most runs to return.
- status (Optional[str], default None):
Only
'running','succeeded'or'failed'runs. - all_runs (bool, default False):
By default a named Pipeline returns its own runs (those of the saved Pipeline, or
whose run IDs carry its name), and an unnamed one the runs which targeted or
contained one of its member pipes. If
True(or the selection is a tags / keys filter), return every run on the instance. - since (Union[datetime, timedelta, str, None], default None):
Only runs started at or after this time: a
datetime(naive means UTC), atimedelta(that long ago, e.g.timedelta(hours=1)) or an ISO 8601 string.
Returns
Run summaries (
run(the stored row),status,duration_secondsandcounts):(nodes by status).
1512 def get_run(self, run_id: str) -> Optional[Dict[str, Any]]: 1513 """ 1514 One run with its node rows and the planned-vs-actual Gantt bars 1515 (`None` if the run is unknown). 1516 """ 1517 request = self._instance_request() 1518 request['run_id'] = run_id 1519 return call('engine_run_detail', request)
One run with its node rows and the planned-vs-actual Gantt bars
(None if the run is unknown).
1521 def get_events(self, run_id: str, after_seq: Optional[int] = None) -> List[Dict[str, Any]]: 1522 """The persisted events of a run, in order (see `RunResult.events()`).""" 1523 request = self._instance_request() 1524 request['run_id'] = run_id 1525 if after_seq is not None: 1526 request['after_seq'] = after_seq 1527 return _decode_events(call('engine_run_events', request))
The persisted events of a run, in order (see RunResult.events()).
178class Plan: 179 """ 180 A dry run: what the engine would do, with no data movement. 181 182 `str(plan)` is the plan table (`node | action | bounds | chunks | reason`, `*` marks a 183 provisional plan). 184 185 Attributes 186 ---------- 187 nodes: List[PlannedNode] 188 The nodes in dependency order. 189 190 edges: List[Tuple[str, str]] 191 The `(parent, child)` pairs inside the selection. 192 193 flat: bool 194 `True` for a selection with no traversal and no dependencies between its members: 195 every node syncs on its own, like `mrsm sync pipes`. 196 197 warnings: List[str] 198 Graph and selection warnings (including one per member pipe that is not registered). 199 200 unregistered: List[mrsm.Pipe] 201 Member pipes that are not registered on their instance, so the engine cannot plan 202 them yet. `Pipeline.sync()` registers them first (except for a saved Pipeline, whose 203 members must already be registered); `pipe.register()` registers one now. 204 """ 205 206 def __init__( 207 self, 208 data: Dict[str, Any], 209 pipeline: Optional['Pipeline'] = None, 210 options: Optional[Dict[str, Any]] = None, 211 unregistered: Optional[List[mrsm.Pipe]] = None, 212 ): 213 self.raw = data 214 plan = data['plan'] 215 self.text: str = data.get('text', '') 216 self.nodes: List[PlannedNode] = [PlannedNode(node) for node in plan['nodes']] 217 self.edges: List[Tuple[str, str]] = [tuple(edge) for edge in plan.get('edges', [])] 218 self.flat: bool = bool(plan.get('flat', False)) 219 self.warnings: List[str] = list(data.get('warnings', None) or []) 220 self.unregistered: List[mrsm.Pipe] = list(unregistered or []) 221 self._pipeline = pipeline 222 self._options = options or {} 223 self._unregistered_warnings: List[str] = [ 224 _unregistered_warning(pipe, pipeline) 225 for pipe in self.unregistered 226 ] 227 self.warnings.extend(self._unregistered_warnings) 228 229 @property 230 def pipes(self) -> List[mrsm.Pipe]: 231 """The pipes of the plan, in dependency order (`unregistered` pipes are not planned).""" 232 return [node.pipe for node in self.nodes] 233 234 def node(self, pipe: Union[mrsm.Pipe, str]) -> Optional[PlannedNode]: 235 """The planned node of a pipe (or `pipe_ref`).""" 236 pipe_ref = pipe if isinstance(pipe, str) else _pipe_ref(pipe) 237 for node in self.nodes: 238 if node.pipe_ref == pipe_ref: 239 return node 240 return None 241 242 def to_dicts(self) -> List[Dict[str, Any]]: 243 """The plan table as a list of rows.""" 244 return [node.to_dict() for node in self.nodes] 245 246 def to_dataframe( 247 self, 248 as_polars: Optional[bool] = None, 249 as_pandas: Optional[bool] = None, 250 ) -> Any: 251 """ 252 The plan table (`node`, `action`, `bounds`, `chunks`, `reason`) as a DataFrame of the 253 `meerschaum:dataframe` library (Polars for a new root); `as_polars=True` or 254 `as_pandas=True` asks for one. 255 """ 256 from meerschaum.utils.dataframe import resolve_dataframe_library, ensure_dataframe_library 257 columns = ['node', 'action', 'bounds', 'chunks', 'reason'] 258 library = resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) 259 module = ensure_dataframe_library(library) 260 if library == 'polars': 261 schema = { 262 'node': module.String, 263 'action': module.String, 264 'bounds': module.String, 265 'chunks': module.Int64, 266 'reason': module.String, 267 } 268 rows = [{col: row.get(col) for col in columns} for row in self.to_dicts()] 269 return module.DataFrame(rows, schema=schema, strict=False) 270 return module.DataFrame(self.to_dicts(), columns=columns) 271 272 def execute(self, **kwargs: Any) -> 'RunResult': 273 """ 274 Run the pipeline this plan came from with the options it was planned with 275 (the engine re-plans against current state as the run proceeds). 276 """ 277 if self._pipeline is None: 278 raise ValueError("This plan does not belong to a pipeline.") 279 options = {**self._options, **kwargs} 280 return self._pipeline.sync(**options) 281 282 def __len__(self) -> int: 283 return len(self.nodes) 284 285 def __iter__(self): 286 return iter(self.nodes) 287 288 def __str__(self) -> str: 289 if not self._unregistered_warnings: 290 return self.text 291 text = self.text if (not self.text or self.text.endswith('\n')) else self.text + '\n' 292 return text + ''.join(f"warning: {warning}\n" for warning in self._unregistered_warnings) 293 294 def __repr__(self) -> str: 295 return f"Plan({len(self.nodes)} nodes)"
A dry run: what the engine would do, with no data movement.
str(plan) is the plan table (node | action | bounds | chunks | reason, * marks a
provisional plan).
Attributes
- nodes (List[PlannedNode]): The nodes in dependency order.
- edges (List[Tuple[str, str]]):
The
(parent, child)pairs inside the selection. - flat (bool):
Truefor a selection with no traversal and no dependencies between its members: every node syncs on its own, likemrsm sync pipes. - warnings (List[str]): Graph and selection warnings (including one per member pipe that is not registered).
- unregistered (List[mrsm.Pipe]):
Member pipes that are not registered on their instance, so the engine cannot plan
them yet.
Pipeline.sync()registers them first (except for a saved Pipeline, whose members must already be registered);pipe.register()registers one now.
206 def __init__( 207 self, 208 data: Dict[str, Any], 209 pipeline: Optional['Pipeline'] = None, 210 options: Optional[Dict[str, Any]] = None, 211 unregistered: Optional[List[mrsm.Pipe]] = None, 212 ): 213 self.raw = data 214 plan = data['plan'] 215 self.text: str = data.get('text', '') 216 self.nodes: List[PlannedNode] = [PlannedNode(node) for node in plan['nodes']] 217 self.edges: List[Tuple[str, str]] = [tuple(edge) for edge in plan.get('edges', [])] 218 self.flat: bool = bool(plan.get('flat', False)) 219 self.warnings: List[str] = list(data.get('warnings', None) or []) 220 self.unregistered: List[mrsm.Pipe] = list(unregistered or []) 221 self._pipeline = pipeline 222 self._options = options or {} 223 self._unregistered_warnings: List[str] = [ 224 _unregistered_warning(pipe, pipeline) 225 for pipe in self.unregistered 226 ] 227 self.warnings.extend(self._unregistered_warnings)
229 @property 230 def pipes(self) -> List[mrsm.Pipe]: 231 """The pipes of the plan, in dependency order (`unregistered` pipes are not planned).""" 232 return [node.pipe for node in self.nodes]
The pipes of the plan, in dependency order (unregistered pipes are not planned).
234 def node(self, pipe: Union[mrsm.Pipe, str]) -> Optional[PlannedNode]: 235 """The planned node of a pipe (or `pipe_ref`).""" 236 pipe_ref = pipe if isinstance(pipe, str) else _pipe_ref(pipe) 237 for node in self.nodes: 238 if node.pipe_ref == pipe_ref: 239 return node 240 return None
The planned node of a pipe (or pipe_ref).
242 def to_dicts(self) -> List[Dict[str, Any]]: 243 """The plan table as a list of rows.""" 244 return [node.to_dict() for node in self.nodes]
The plan table as a list of rows.
246 def to_dataframe( 247 self, 248 as_polars: Optional[bool] = None, 249 as_pandas: Optional[bool] = None, 250 ) -> Any: 251 """ 252 The plan table (`node`, `action`, `bounds`, `chunks`, `reason`) as a DataFrame of the 253 `meerschaum:dataframe` library (Polars for a new root); `as_polars=True` or 254 `as_pandas=True` asks for one. 255 """ 256 from meerschaum.utils.dataframe import resolve_dataframe_library, ensure_dataframe_library 257 columns = ['node', 'action', 'bounds', 'chunks', 'reason'] 258 library = resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) 259 module = ensure_dataframe_library(library) 260 if library == 'polars': 261 schema = { 262 'node': module.String, 263 'action': module.String, 264 'bounds': module.String, 265 'chunks': module.Int64, 266 'reason': module.String, 267 } 268 rows = [{col: row.get(col) for col in columns} for row in self.to_dicts()] 269 return module.DataFrame(rows, schema=schema, strict=False) 270 return module.DataFrame(self.to_dicts(), columns=columns)
The plan table (node, action, bounds, chunks, reason) as a DataFrame of the
meerschaum:dataframe library (Polars for a new root); as_polars=True or
as_pandas=True asks for one.
272 def execute(self, **kwargs: Any) -> 'RunResult': 273 """ 274 Run the pipeline this plan came from with the options it was planned with 275 (the engine re-plans against current state as the run proceeds). 276 """ 277 if self._pipeline is None: 278 raise ValueError("This plan does not belong to a pipeline.") 279 options = {**self._options, **kwargs} 280 return self._pipeline.sync(**options)
Run the pipeline this plan came from with the options it was planned with (the engine re-plans against current state as the run proceeds).
321class RunResult(tuple): 322 """ 323 The result of an engine run: a `SuccessTuple` (`success, message = run` still unpacks) 324 with the run's details. `pipe.sync(parents=..., children=...)`, `Pipeline.sync()` and 325 `Plan.execute()` return one. 326 327 Attributes 328 ---------- 329 success: bool 330 Whether the run succeeded (a failed node fails the run). 331 332 message: str 333 The run summary. 334 335 run_id: Optional[str] 336 The ID in the run history (`Pipeline.get_run(run_id)`); `None` if the run never started. 337 338 plan: Optional[Plan] 339 The plan that was executed. 340 341 inserted, updated, upserted, deleted: Optional[int] 342 Rows inserted (updated, ...) by every node of the run together, like the same fields of 343 the `SyncResult` a plain `pipe.sync()` returns. `None` if the run never started. 344 345 duration: float 346 How long the run took, in wall-clock seconds (nodes that run in parallel overlap, so 347 this is not the sum of the nodes' durations). 348 349 outcomes: List[Dict[str, Any]] 350 One entry per node that synced, in completion order: `pipe_ref`, `pipe` (`str(pipe)`) 351 and `result` (`success`, `message`, `inserted`, `updated`, `upserted`, `deleted`, 352 `changed_intervals`, `duration` (float seconds, or `None`), `begin`, `end`). 353 354 critical_path: List[str] 355 The `pipe_ref`s of the plan's critical path. 356 357 warnings: List[str] 358 Graph, selection and state warnings. 359 360 record: Optional[Dict[str, Any]] 361 The persisted run row and node rows. 362 363 callback_error: Optional[Exception] 364 The exception an `on_event` callback raised (the run continued without it), else 365 `None`. 366 """ 367 368 success: bool 369 message: str 370 371 def __new__(cls, success: bool, message: str, data: Optional[Dict[str, Any]] = None, **kwargs): 372 self = super().__new__(cls, (bool(success), message)) 373 data = data or {} 374 self.success = bool(success) 375 self.message = message 376 self.run_id = data.get('run_id', None) 377 self.outcomes = [with_seconds(outcome) for outcome in data.get('outcomes', None) or []] 378 self.critical_path = list(data.get('critical_path', None) or []) 379 self.warnings = list(data.get('warnings', None) or []) 380 self.duration = float(data.get('duration_seconds', 0.0) or 0.0) 381 self.record = data.get('record', None) 382 self.plan = Plan(data, **kwargs) if data.get('plan', None) else None 383 ran = 'outcomes' in data 384 for field in _RUN_TOTALS: 385 setattr(self, field, _total(self.outcomes, field) if ran else None) 386 self._data = data 387 self._instance_request: Optional[Dict[str, Any]] = None 388 self.callback_error: Optional[BaseException] = None 389 return self 390 391 def __reduce__(self): 392 ### The details are JSON data; the pipeline back-reference is dropped. 393 return (_rebuild_run_result, (self.success, self.message, self._data)) 394 395 @property 396 def result(self) -> 'RunResult': 397 """The run result (this object: it is already `SuccessTuple`-compatible).""" 398 return self 399 400 def get_outcome(self, pipe: Union[mrsm.Pipe, str]) -> Optional[Dict[str, Any]]: 401 """The outcome of a node of the run.""" 402 pipe_ref = pipe if isinstance(pipe, str) else _pipe_ref(pipe) 403 for outcome in self.outcomes: 404 if outcome['pipe_ref'] == pipe_ref: 405 return outcome 406 return None 407 408 def to_dict(self) -> Dict[str, Any]: 409 """The run's summary fields as a dictionary.""" 410 return { 411 'success': self.success, 412 'message': self.message, 413 'run_id': self.run_id, 414 **{field: getattr(self, field) for field in _RUN_TOTALS}, 415 'duration': self.duration, 416 } 417 418 def events(self, after_seq: Optional[int] = None) -> List[Dict[str, Any]]: 419 """ 420 The run's persisted events (`run_started`, `node_started`, `node_finished`, 421 `run_finished`, ...) in order, read from the instance after the run 422 (`system.engine.events` sets how fine-grained they are). Each row has the keys of a live 423 `on_event` event (`event`, `run_id`, `seq`, `ts`, `pipe_ref` and the event's own 424 fields) plus `payload`, the event's own fields as one dict. `ts` is the ISO 8601 text 425 the instance stored. 426 427 To receive every event live while the run is going, pass `on_event=` to 428 `Pipeline.sync()` (or `pipe.sync(parents=..., children=...)`). 429 """ 430 if self.run_id is None or self._instance_request is None: 431 return [] 432 request = {**self._instance_request, 'run_id': self.run_id} 433 if after_seq is not None: 434 request['after_seq'] = after_seq 435 return _decode_events(call('engine_run_events', request)) 436 437 def __repr__(self) -> str: 438 return f"RunResult(success={self.success}, run_id={self.run_id!r}, message={self.message!r})"
The result of an engine run: a SuccessTuple (success, message = run still unpacks)
with the run's details. pipe.sync(parents=..., children=...), Pipeline.sync() and
Plan.execute() return one.
Attributes
- success (bool): Whether the run succeeded (a failed node fails the run).
- message (str): The run summary.
- run_id (Optional[str]):
The ID in the run history (
Pipeline.get_run(run_id));Noneif the run never started. - plan (Optional[Plan]): The plan that was executed.
- inserted, updated, upserted, deleted (Optional[int]):
Rows inserted (updated, ...) by every node of the run together, like the same fields of
the
SyncResulta plainpipe.sync()returns.Noneif the run never started. - duration (float): How long the run took, in wall-clock seconds (nodes that run in parallel overlap, so this is not the sum of the nodes' durations).
- outcomes (List[Dict[str, Any]]):
One entry per node that synced, in completion order:
pipe_ref,pipe(str(pipe)) andresult(success,message,inserted,updated,upserted,deleted,changed_intervals,duration(float seconds, orNone),begin,end). - critical_path (List[str]):
The
pipe_refs of the plan's critical path. - warnings (List[str]): Graph, selection and state warnings.
- record (Optional[Dict[str, Any]]): The persisted run row and node rows.
- callback_error (Optional[Exception]):
The exception an
on_eventcallback raised (the run continued without it), elseNone.
395 @property 396 def result(self) -> 'RunResult': 397 """The run result (this object: it is already `SuccessTuple`-compatible).""" 398 return self
The run result (this object: it is already SuccessTuple-compatible).
400 def get_outcome(self, pipe: Union[mrsm.Pipe, str]) -> Optional[Dict[str, Any]]: 401 """The outcome of a node of the run.""" 402 pipe_ref = pipe if isinstance(pipe, str) else _pipe_ref(pipe) 403 for outcome in self.outcomes: 404 if outcome['pipe_ref'] == pipe_ref: 405 return outcome 406 return None
The outcome of a node of the run.
408 def to_dict(self) -> Dict[str, Any]: 409 """The run's summary fields as a dictionary.""" 410 return { 411 'success': self.success, 412 'message': self.message, 413 'run_id': self.run_id, 414 **{field: getattr(self, field) for field in _RUN_TOTALS}, 415 'duration': self.duration, 416 }
The run's summary fields as a dictionary.
418 def events(self, after_seq: Optional[int] = None) -> List[Dict[str, Any]]: 419 """ 420 The run's persisted events (`run_started`, `node_started`, `node_finished`, 421 `run_finished`, ...) in order, read from the instance after the run 422 (`system.engine.events` sets how fine-grained they are). Each row has the keys of a live 423 `on_event` event (`event`, `run_id`, `seq`, `ts`, `pipe_ref` and the event's own 424 fields) plus `payload`, the event's own fields as one dict. `ts` is the ISO 8601 text 425 the instance stored. 426 427 To receive every event live while the run is going, pass `on_event=` to 428 `Pipeline.sync()` (or `pipe.sync(parents=..., children=...)`). 429 """ 430 if self.run_id is None or self._instance_request is None: 431 return [] 432 request = {**self._instance_request, 'run_id': self.run_id} 433 if after_seq is not None: 434 request['after_seq'] = after_seq 435 return _decode_events(call('engine_run_events', request))
The run's persisted events (run_started, node_started, node_finished,
run_finished, ...) in order, read from the instance after the run
(system.engine.events sets how fine-grained they are). Each row has the keys of a live
on_event event (event, run_id, seq, ts, pipe_ref and the event's own
fields) plus payload, the event's own fields as one dict. ts is the ISO 8601 text
the instance stored.
To receive every event live while the run is going, pass on_event= to
Pipeline.sync() (or pipe.sync(parents=..., children=...)).
39class SyncResult(tuple): 40 """ 41 The result of `Pipe.sync()`: a `SuccessTuple` (`(success, message)`, so 42 `success, msg = pipe.sync()` unpacks as ever) with the structured fields of the sync. 43 44 The fields are `None` when the sync did not report them (a plugin's own `sync()`, 45 the Python implementation, a failure before the data was written). 46 47 Attributes 48 ---------- 49 success: bool 50 Whether the sync succeeded. 51 52 message: str 53 The message. 54 55 inserted: Optional[int] 56 Rows inserted. 57 58 updated: Optional[int] 59 Rows updated. 60 61 upserted: Optional[int] 62 Rows upserted. 63 64 deleted: Optional[int] 65 Rows deleted. 66 67 changed_intervals: Optional[List[Tuple[Any, Any]]] 68 The `(begin, end)` intervals of the datetime axis that changed: the epoch-aligned grid 69 cells (one chunk interval wide) that hold the rows the sync handed to the instance, 70 merged into runs. This is a superset of the rows that actually changed (rows that 71 turned out to be unchanged duplicates are inside it too). An open end is `None`. 72 73 duration: Optional[float] 74 Seconds the sync took. 75 76 begin: Union[datetime, int, None] 77 The lower bound of the sync, if one was given. 78 79 end: Union[datetime, int, None] 80 The upper bound of the sync, if one was given. 81 """ 82 83 _FIELDS = ( 84 'inserted', 'updated', 'upserted', 'deleted', 85 'changed_intervals', 'duration', 'begin', 'end', 86 ) 87 88 success: bool 89 message: str 90 91 def __new__(cls, success: bool, message: str = 'Success', **fields: Any): 92 self = super().__new__(cls, (success, message)) 93 self.success = success 94 self.message = message 95 for name in cls._FIELDS: 96 setattr(self, name, fields.pop(name, None)) 97 if fields: 98 raise TypeError(f"Unexpected sync result fields: {', '.join(sorted(fields))}.") 99 return self 100 101 @classmethod 102 def from_tuple(cls, result: Any) -> Any: 103 """ 104 Return `result` as a `SyncResult` if it is a plain `(bool, str)` tuple, 105 otherwise return it as it is (e.g. a `RunResult` or a plugin's own tuple). 106 """ 107 if type(result) is not tuple or len(result) != 2: 108 return result 109 return cls(result[0], result[1]) 110 111 def to_dict(self) -> Dict[str, Any]: 112 """ 113 Return the result's fields as a dictionary. 114 """ 115 return { 116 'success': self.success, 117 'message': self.message, 118 **{name: getattr(self, name) for name in self._FIELDS}, 119 } 120 121 def __reduce__(self): 122 return _rebuild_sync_result, (self.to_dict(),)
The result of Pipe.sync(): a SuccessTuple ((success, message), so
success, msg = pipe.sync() unpacks as ever) with the structured fields of the sync.
The fields are None when the sync did not report them (a plugin's own sync(),
the Python implementation, a failure before the data was written).
Attributes
- success (bool): Whether the sync succeeded.
- message (str): The message.
- inserted (Optional[int]): Rows inserted.
- updated (Optional[int]): Rows updated.
- upserted (Optional[int]): Rows upserted.
- deleted (Optional[int]): Rows deleted.
- changed_intervals (Optional[List[Tuple[Any, Any]]]):
The
(begin, end)intervals of the datetime axis that changed: the epoch-aligned grid cells (one chunk interval wide) that hold the rows the sync handed to the instance, merged into runs. This is a superset of the rows that actually changed (rows that turned out to be unchanged duplicates are inside it too). An open end isNone. - duration (Optional[float]): Seconds the sync took.
- begin (Union[datetime, int, None]): The lower bound of the sync, if one was given.
- end (Union[datetime, int, None]): The upper bound of the sync, if one was given.
101 @classmethod 102 def from_tuple(cls, result: Any) -> Any: 103 """ 104 Return `result` as a `SyncResult` if it is a plain `(bool, str)` tuple, 105 otherwise return it as it is (e.g. a `RunResult` or a plugin's own tuple). 106 """ 107 if type(result) is not tuple or len(result) != 2: 108 return result 109 return cls(result[0], result[1])
Return result as a SyncResult if it is a plain (bool, str) tuple,
otherwise return it as it is (e.g. a RunResult or a plugin's own tuple).
111 def to_dict(self) -> Dict[str, Any]: 112 """ 113 Return the result's fields as a dictionary. 114 """ 115 return { 116 'success': self.success, 117 'message': self.message, 118 **{name: getattr(self, name) for name in self._FIELDS}, 119 }
Return the result's fields as a dictionary.
44class Team: 45 """ 46 A team of users on an instance (the API server's user store). 47 48 Every user is a member of the built-in team `Everyone`, which cannot be deleted or left. 49 Administrators create more teams; a user can be in several, as a `'member'` or a 50 `'manager'`. Managers add and remove members of their team and manage the access of its 51 pipes. Renaming a team renames it in the pipes and jobs that name it; deleting one moves 52 them to `Everyone`. 53 54 Parameters 55 ---------- 56 name: str 57 The team's name (the username rules; names are unique without case). 58 59 instance: Union[str, InstanceConnector, None], default None 60 The instance that holds the team (the configured instance by default). Through an 61 `api:` instance the server checks who may change the team. 62 63 members: Optional[Iterable[str]], default None 64 Members to add (in memory, until `register()` or `edit()`). 65 66 managers: Optional[Iterable[str]], default None 67 Managers to add (in memory, until `register()` or `edit()`). 68 69 attributes: Optional[Dict[str, Any]], default None 70 Free-form attributes. 71 72 Examples 73 -------- 74 >>> team = mrsm.Team('ops', instance='sql:local', members=['alice'], managers=['mia']) 75 >>> team.register() 76 (True, "Registered team 'ops'.") 77 >>> team.add_members('bob') 78 (True, "Edited team 'ops'.") 79 >>> team.members 80 ['alice', 'mia', 'bob'] 81 >>> team.managers 82 ['mia'] 83 """ 84 85 def __init__( 86 self, 87 name: str, 88 instance: Union[str, 'mrsm.connectors.InstanceConnector', None] = None, 89 members: Optional[Iterable[Any]] = None, 90 managers: Optional[Iterable[Any]] = None, 91 attributes: Optional[Dict[str, Any]] = None, 92 team_id: Optional[str] = None, 93 ): 94 self.name = str(name).strip() 95 if instance is None: 96 from meerschaum.config import get_config 97 instance = get_config('meerschaum', 'instance', patch=True) 98 if not isinstance(instance, str): 99 self._instance_connector = instance 100 self._instance_keys = str(instance) 101 else: 102 self._instance_keys = instance 103 self._id = team_id 104 self._roles: Dict[str, str] = {} 105 self._attributes: Dict[str, Any] = dict(attributes or {}) 106 self._loaded = False 107 for username in _usernames(members or []): 108 self._roles[username] = 'member' 109 for username in _usernames(managers or []): 110 self._roles[username] = 'manager' 111 self._pending = bool(self._roles) or bool(self._attributes) 112 113 # -------------------------------------------------------------------------------------- 114 # The instance 115 # -------------------------------------------------------------------------------------- 116 117 @property 118 def instance_keys(self) -> str: 119 """The keys of the instance holding the team.""" 120 return self._instance_keys 121 122 @property 123 def instance_connector(self) -> 'mrsm.connectors.InstanceConnector': 124 """The instance connector holding the team.""" 125 if '_instance_connector' not in self.__dict__: 126 from meerschaum.connectors.parse import parse_instance_keys 127 self._instance_connector = parse_instance_keys(self._instance_keys) 128 return self._instance_connector 129 130 @property 131 def is_everyone(self) -> bool: 132 """Whether this is the built-in team `Everyone`.""" 133 return self.name.lower() == EVERYONE.lower() or not self.name 134 135 def _load(self, refresh: bool = False, debug: bool = False) -> Optional[Dict[str, Any]]: 136 """Read the team from the instance (its record, or `None` when it does not exist).""" 137 if self._loaded and not refresh: 138 return None 139 record = self.instance_connector.get_team(self.name, debug=debug) 140 self._loaded = True 141 if not record: 142 return None 143 if not self._pending: 144 self._apply_record(record) 145 else: 146 ### Names compare without case: the stored spelling wins. 147 self.name = str(record.get('name', None) or self.name) 148 self._id = record.get('id', None) or self._id 149 return record 150 151 def _apply_record(self, record: Dict[str, Any]) -> None: 152 self.name = str(record.get('name', self.name) or self.name) 153 self._id = record.get('id', None) or record.get('team_id', None) or self._id 154 members = record.get('members', None) or {} 155 self._roles = { 156 str(username): _role(role) 157 for username, role in ( 158 members.items() if isinstance(members, dict) else ((m, 'member') for m in members) 159 ) 160 } 161 self._attributes = dict(record.get('attributes', None) or {}) 162 163 def refresh(self, debug: bool = False) -> 'Team': 164 """Read the team again from the instance (in-memory changes are dropped).""" 165 self._pending = False 166 self._loaded = False 167 self._load(refresh=True, debug=debug) 168 return self 169 170 def exists(self, debug: bool = False) -> bool: 171 """ 172 Whether the team exists on the instance (`Everyone` always does). An existing team's 173 `name` takes its stored spelling (`Team('OPS').name` is `'ops'`). 174 """ 175 if self.is_everyone: 176 return True 177 return self._load(refresh=True, debug=debug) is not None 178 179 # -------------------------------------------------------------------------------------- 180 # Members 181 # -------------------------------------------------------------------------------------- 182 183 @property 184 def id(self) -> Optional[str]: 185 """The team's id (`None` before it is registered).""" 186 self._load() 187 return self._id 188 189 @property 190 def roles(self) -> Dict[str, str]: 191 """`{username: 'member' | 'manager'}` of the listed members.""" 192 self._load() 193 return dict(self._roles) 194 195 @property 196 def members(self) -> List[str]: 197 """ 198 The usernames of the members (managers included). For `Everyone`, every registered 199 user. 200 """ 201 self._load() 202 if self.is_everyone: 203 users = list(self.instance_connector.get_users() or []) 204 return users + [u for u in self._roles if u not in users] 205 return list(self._roles) 206 207 @property 208 def managers(self) -> List[str]: 209 """The usernames of the managers.""" 210 self._load() 211 return [username for username, role in self._roles.items() if role == 'manager'] 212 213 @property 214 def attributes(self) -> Dict[str, Any]: 215 """The team's free-form attributes (edit them, then call `edit()`).""" 216 self._load() 217 self._pending = True 218 return self._attributes 219 220 def role_of(self, user: Any) -> Optional[str]: 221 """A user's role in the team (`'member'` for anyone in `Everyone`), or `None`.""" 222 username = str(getattr(user, 'username', user)) 223 role = self.roles.get(username, None) 224 if role is None and self.is_everyone: 225 return 'member' 226 return role 227 228 def to_dict(self) -> Dict[str, Any]: 229 """The team as stored: `{'id', 'name', 'members': {username: role}, 'attributes'}`.""" 230 return { 231 'id': self._id, 232 'name': EVERYONE if self.is_everyone else self.name, 233 'members': dict(self._roles), 234 'attributes': dict(self._attributes), 235 } 236 237 def _write(self, debug: bool = False) -> mrsm.SuccessTuple: 238 record = self.to_dict() 239 result = self.instance_connector.edit_team(record, debug=debug) 240 if result[0]: 241 self._pending = False 242 return result 243 244 def add_members(self, *usernames: Any, role: str = 'member', debug: bool = False) -> mrsm.SuccessTuple: 245 """ 246 Add users to the team as `role` (`'member'` or `'manager'`; an existing member takes the 247 new role). A registered team is saved at once. 248 """ 249 names = _usernames(usernames) 250 if not names: 251 return False, "No users to add." 252 role = _role(role) 253 self._load(debug=debug) 254 for username in names: 255 self._roles[username] = role 256 self._pending = True 257 if not self.is_everyone and not self._id: 258 return True, f"Added {len(names)} user(s) to team '{self.name}' (not registered yet)." 259 return self._write(debug=debug) 260 261 def add_managers(self, *usernames: Any, debug: bool = False) -> mrsm.SuccessTuple: 262 """Make users managers of the team (`add_members(..., role='manager')`).""" 263 return self.add_members(*usernames, role='manager', debug=debug) 264 265 def remove_members(self, *usernames: Any, debug: bool = False) -> mrsm.SuccessTuple: 266 """ 267 Remove users from the team (a manager of `Everyone` becomes a plain member: nobody leaves 268 `Everyone`). A registered team is saved at once. 269 """ 270 names = _usernames(usernames) 271 if not names: 272 return False, "No users to remove." 273 self._load(debug=debug) 274 missing = [u for u in names if u not in self._roles] 275 if missing and not self.is_everyone: 276 return False, ( 277 f"Not in team '{self.name}': " + ', '.join(f"'{u}'" for u in missing) + "." 278 ) 279 for username in names: 280 self._roles.pop(username, None) 281 self._pending = True 282 if not self.is_everyone and not self._id: 283 return True, f"Removed {len(names)} user(s) from team '{self.name}' (not registered yet)." 284 return self._write(debug=debug) 285 286 # -------------------------------------------------------------------------------------- 287 # Lifecycle 288 # -------------------------------------------------------------------------------------- 289 290 def register(self, debug: bool = False) -> mrsm.SuccessTuple: 291 """Register the team (administrators only through the API).""" 292 if self.is_everyone: 293 return False, f"'{EVERYONE}' is the built-in team of every user: edit it instead." 294 record = self.to_dict() 295 result = self.instance_connector.register_team(record, debug=debug) 296 if result[0]: 297 self._id = record.get('id', None) or self._id 298 self._pending = False 299 self._loaded = True 300 return result 301 302 def edit(self, debug: bool = False) -> mrsm.SuccessTuple: 303 """Save the team's members and attributes.""" 304 self._load(debug=debug) 305 return self._write(debug=debug) 306 307 def rename(self, new_name: str, debug: bool = False) -> mrsm.SuccessTuple: 308 """Rename the team (the pipes and jobs that name it follow).""" 309 old_name = self.name 310 result = self.instance_connector.rename_team(old_name, str(new_name).strip(), debug=debug) 311 if result[0]: 312 self.name = str(new_name).strip() 313 _rename_jobs(old_name, self.name, debug=debug) 314 return result 315 316 def delete(self, debug: bool = False) -> mrsm.SuccessTuple: 317 """Delete the team (its pipes and jobs move to `Everyone`).""" 318 result = self.instance_connector.delete_team(self.name, debug=debug) 319 if result[0]: 320 _rename_jobs(self.name, None, debug=debug) 321 self._id = None 322 self._loaded = False 323 return result 324 325 def get_pipes(self, debug: bool = False) -> List['mrsm.Pipe']: 326 """The pipes (and saved Pipelines' registry pipes) whose access names this team.""" 327 from meerschaum.core.access import get_pipe_access, same_team 328 pipes = mrsm.get_pipes(instance=self.instance_connector, as_list=True, debug=debug) 329 return [ 330 pipe for pipe in pipes 331 if same_team(get_pipe_access(pipe).get('team', None), None if self.is_everyone else self.name) 332 ] 333 334 def __repr__(self) -> str: 335 return f"Team('{self.name}')" 336 337 def __str__(self) -> str: 338 return self.name 339 340 def __eq__(self, other: Any) -> bool: 341 from meerschaum.core.access import same_team 342 return ( 343 isinstance(other, Team) 344 and same_team(self.name, other.name) 345 and self.instance_keys == other.instance_keys 346 ) 347 348 def __hash__(self) -> int: 349 return hash((self.name.lower(), self.instance_keys))
A team of users on an instance (the API server's user store).
Every user is a member of the built-in team Everyone, which cannot be deleted or left.
Administrators create more teams; a user can be in several, as a 'member' or a
'manager'. Managers add and remove members of their team and manage the access of its
pipes. Renaming a team renames it in the pipes and jobs that name it; deleting one moves
them to Everyone.
Parameters
- name (str): The team's name (the username rules; names are unique without case).
- instance (Union[str, InstanceConnector, None], default None):
The instance that holds the team (the configured instance by default). Through an
api:instance the server checks who may change the team. - members (Optional[Iterable[str]], default None):
Members to add (in memory, until
register()oredit()). - managers (Optional[Iterable[str]], default None):
Managers to add (in memory, until
register()oredit()). - attributes (Optional[Dict[str, Any]], default None): Free-form attributes.
Examples
>>> team = mrsm.Team('ops', instance='sql:local', members=['alice'], managers=['mia'])
>>> team.register()
(True, "Registered team 'ops'.")
>>> team.add_members('bob')
(True, "Edited team 'ops'.")
>>> team.members
['alice', 'mia', 'bob']
>>> team.managers
['mia']
85 def __init__( 86 self, 87 name: str, 88 instance: Union[str, 'mrsm.connectors.InstanceConnector', None] = None, 89 members: Optional[Iterable[Any]] = None, 90 managers: Optional[Iterable[Any]] = None, 91 attributes: Optional[Dict[str, Any]] = None, 92 team_id: Optional[str] = None, 93 ): 94 self.name = str(name).strip() 95 if instance is None: 96 from meerschaum.config import get_config 97 instance = get_config('meerschaum', 'instance', patch=True) 98 if not isinstance(instance, str): 99 self._instance_connector = instance 100 self._instance_keys = str(instance) 101 else: 102 self._instance_keys = instance 103 self._id = team_id 104 self._roles: Dict[str, str] = {} 105 self._attributes: Dict[str, Any] = dict(attributes or {}) 106 self._loaded = False 107 for username in _usernames(members or []): 108 self._roles[username] = 'member' 109 for username in _usernames(managers or []): 110 self._roles[username] = 'manager' 111 self._pending = bool(self._roles) or bool(self._attributes)
117 @property 118 def instance_keys(self) -> str: 119 """The keys of the instance holding the team.""" 120 return self._instance_keys
The keys of the instance holding the team.
122 @property 123 def instance_connector(self) -> 'mrsm.connectors.InstanceConnector': 124 """The instance connector holding the team.""" 125 if '_instance_connector' not in self.__dict__: 126 from meerschaum.connectors.parse import parse_instance_keys 127 self._instance_connector = parse_instance_keys(self._instance_keys) 128 return self._instance_connector
The instance connector holding the team.
130 @property 131 def is_everyone(self) -> bool: 132 """Whether this is the built-in team `Everyone`.""" 133 return self.name.lower() == EVERYONE.lower() or not self.name
Whether this is the built-in team Everyone.
163 def refresh(self, debug: bool = False) -> 'Team': 164 """Read the team again from the instance (in-memory changes are dropped).""" 165 self._pending = False 166 self._loaded = False 167 self._load(refresh=True, debug=debug) 168 return self
Read the team again from the instance (in-memory changes are dropped).
170 def exists(self, debug: bool = False) -> bool: 171 """ 172 Whether the team exists on the instance (`Everyone` always does). An existing team's 173 `name` takes its stored spelling (`Team('OPS').name` is `'ops'`). 174 """ 175 if self.is_everyone: 176 return True 177 return self._load(refresh=True, debug=debug) is not None
Whether the team exists on the instance (Everyone always does). An existing team's
name takes its stored spelling (Team('OPS').name is 'ops').
183 @property 184 def id(self) -> Optional[str]: 185 """The team's id (`None` before it is registered).""" 186 self._load() 187 return self._id
The team's id (None before it is registered).
189 @property 190 def roles(self) -> Dict[str, str]: 191 """`{username: 'member' | 'manager'}` of the listed members.""" 192 self._load() 193 return dict(self._roles)
{username: 'member' | 'manager'} of the listed members.
195 @property 196 def members(self) -> List[str]: 197 """ 198 The usernames of the members (managers included). For `Everyone`, every registered 199 user. 200 """ 201 self._load() 202 if self.is_everyone: 203 users = list(self.instance_connector.get_users() or []) 204 return users + [u for u in self._roles if u not in users] 205 return list(self._roles)
The usernames of the members (managers included). For Everyone, every registered
user.
207 @property 208 def managers(self) -> List[str]: 209 """The usernames of the managers.""" 210 self._load() 211 return [username for username, role in self._roles.items() if role == 'manager']
The usernames of the managers.
213 @property 214 def attributes(self) -> Dict[str, Any]: 215 """The team's free-form attributes (edit them, then call `edit()`).""" 216 self._load() 217 self._pending = True 218 return self._attributes
The team's free-form attributes (edit them, then call edit()).
220 def role_of(self, user: Any) -> Optional[str]: 221 """A user's role in the team (`'member'` for anyone in `Everyone`), or `None`.""" 222 username = str(getattr(user, 'username', user)) 223 role = self.roles.get(username, None) 224 if role is None and self.is_everyone: 225 return 'member' 226 return role
A user's role in the team ('member' for anyone in Everyone), or None.
228 def to_dict(self) -> Dict[str, Any]: 229 """The team as stored: `{'id', 'name', 'members': {username: role}, 'attributes'}`.""" 230 return { 231 'id': self._id, 232 'name': EVERYONE if self.is_everyone else self.name, 233 'members': dict(self._roles), 234 'attributes': dict(self._attributes), 235 }
The team as stored: {'id', 'name', 'members': {username: role}, 'attributes'}.
244 def add_members(self, *usernames: Any, role: str = 'member', debug: bool = False) -> mrsm.SuccessTuple: 245 """ 246 Add users to the team as `role` (`'member'` or `'manager'`; an existing member takes the 247 new role). A registered team is saved at once. 248 """ 249 names = _usernames(usernames) 250 if not names: 251 return False, "No users to add." 252 role = _role(role) 253 self._load(debug=debug) 254 for username in names: 255 self._roles[username] = role 256 self._pending = True 257 if not self.is_everyone and not self._id: 258 return True, f"Added {len(names)} user(s) to team '{self.name}' (not registered yet)." 259 return self._write(debug=debug)
Add users to the team as role ('member' or 'manager'; an existing member takes the
new role). A registered team is saved at once.
261 def add_managers(self, *usernames: Any, debug: bool = False) -> mrsm.SuccessTuple: 262 """Make users managers of the team (`add_members(..., role='manager')`).""" 263 return self.add_members(*usernames, role='manager', debug=debug)
Make users managers of the team (add_members(..., role='manager')).
265 def remove_members(self, *usernames: Any, debug: bool = False) -> mrsm.SuccessTuple: 266 """ 267 Remove users from the team (a manager of `Everyone` becomes a plain member: nobody leaves 268 `Everyone`). A registered team is saved at once. 269 """ 270 names = _usernames(usernames) 271 if not names: 272 return False, "No users to remove." 273 self._load(debug=debug) 274 missing = [u for u in names if u not in self._roles] 275 if missing and not self.is_everyone: 276 return False, ( 277 f"Not in team '{self.name}': " + ', '.join(f"'{u}'" for u in missing) + "." 278 ) 279 for username in names: 280 self._roles.pop(username, None) 281 self._pending = True 282 if not self.is_everyone and not self._id: 283 return True, f"Removed {len(names)} user(s) from team '{self.name}' (not registered yet)." 284 return self._write(debug=debug)
Remove users from the team (a manager of Everyone becomes a plain member: nobody leaves
Everyone). A registered team is saved at once.
290 def register(self, debug: bool = False) -> mrsm.SuccessTuple: 291 """Register the team (administrators only through the API).""" 292 if self.is_everyone: 293 return False, f"'{EVERYONE}' is the built-in team of every user: edit it instead." 294 record = self.to_dict() 295 result = self.instance_connector.register_team(record, debug=debug) 296 if result[0]: 297 self._id = record.get('id', None) or self._id 298 self._pending = False 299 self._loaded = True 300 return result
Register the team (administrators only through the API).
302 def edit(self, debug: bool = False) -> mrsm.SuccessTuple: 303 """Save the team's members and attributes.""" 304 self._load(debug=debug) 305 return self._write(debug=debug)
Save the team's members and attributes.
307 def rename(self, new_name: str, debug: bool = False) -> mrsm.SuccessTuple: 308 """Rename the team (the pipes and jobs that name it follow).""" 309 old_name = self.name 310 result = self.instance_connector.rename_team(old_name, str(new_name).strip(), debug=debug) 311 if result[0]: 312 self.name = str(new_name).strip() 313 _rename_jobs(old_name, self.name, debug=debug) 314 return result
Rename the team (the pipes and jobs that name it follow).
316 def delete(self, debug: bool = False) -> mrsm.SuccessTuple: 317 """Delete the team (its pipes and jobs move to `Everyone`).""" 318 result = self.instance_connector.delete_team(self.name, debug=debug) 319 if result[0]: 320 _rename_jobs(self.name, None, debug=debug) 321 self._id = None 322 self._loaded = False 323 return result
Delete the team (its pipes and jobs move to Everyone).
325 def get_pipes(self, debug: bool = False) -> List['mrsm.Pipe']: 326 """The pipes (and saved Pipelines' registry pipes) whose access names this team.""" 327 from meerschaum.core.access import get_pipe_access, same_team 328 pipes = mrsm.get_pipes(instance=self.instance_connector, as_list=True, debug=debug) 329 return [ 330 pipe for pipe in pipes 331 if same_team(get_pipe_access(pipe).get('team', None), None if self.is_everyone else self.name) 332 ]
The pipes (and saved Pipelines' registry pipes) whose access names this team.
50class Plugin: 51 """Handle packaging of Meerschaum plugins.""" 52 53 def __init__( 54 self, 55 name: str, 56 version: Optional[str] = None, 57 user_id: Optional[int] = None, 58 required: Optional[List[str]] = None, 59 attributes: Optional[Dict[str, Any]] = None, 60 archive_path: Optional[pathlib.Path] = None, 61 venv_path: Optional[pathlib.Path] = None, 62 repo_connector: Optional['mrsm.connectors.api.APIConnector'] = None, 63 repo: Union['mrsm.connectors.api.APIConnector', str, None] = None, 64 ): 65 import meerschaum.config.paths as paths 66 from meerschaum._internal.static import STATIC_CONFIG 67 sep = STATIC_CONFIG['plugins']['repo_separator'] 68 _repo = None 69 if sep in name: 70 try: 71 name, _repo = name.split(sep) 72 except Exception as e: 73 error(f"Invalid plugin name: '{name}'") 74 self._repo_in_name = _repo 75 76 if attributes is None: 77 attributes = {} 78 self.name = name 79 self.attributes = attributes 80 self.user_id = user_id 81 self._version = version 82 if required: 83 self._required = required 84 self.archive_path = ( 85 archive_path if archive_path is not None 86 else paths.PLUGINS_ARCHIVES_RESOURCES_PATH / f"{self.name}.tar.gz" 87 ) 88 self.venv_path = ( 89 venv_path if venv_path is not None 90 else paths.VIRTENV_RESOURCES_PATH / self.name 91 ) 92 self._repo_connector = repo_connector 93 self._repo_keys = repo 94 95 96 @property 97 def repo_connector(self): 98 """ 99 Return the repository connector for this plugin. 100 NOTE: This imports the `connectors` module, which imports certain plugin modules. 101 """ 102 if self._repo_connector is None: 103 from meerschaum.connectors.parse import parse_repo_keys 104 105 repo_keys = self._repo_keys or self._repo_in_name 106 if self._repo_in_name and self._repo_keys and self._repo_keys != self._repo_in_name: 107 error( 108 f"Received inconsistent repos: '{self._repo_in_name}' and '{self._repo_keys}'." 109 ) 110 repo_connector = parse_repo_keys(repo_keys) 111 self._repo_connector = repo_connector 112 return self._repo_connector 113 114 115 @property 116 def version(self): 117 """ 118 Return the plugin's module version is defined (`__version__`) if it's defined. 119 """ 120 if self._version is None: 121 try: 122 self._version = self.module.__version__ 123 except Exception as e: 124 self._version = None 125 return self._version 126 127 128 @property 129 def module(self): 130 """ 131 Return the Python module of the underlying plugin, 132 or `None` if the plugin is not installed or its import failed 133 (check `Plugin.import_error` for the swallowed exception). 134 """ 135 if '_module' not in self.__dict__ or self.__dict__.get('_module', None) is None: 136 from meerschaum.plugins import _get_loaded_module, _get_plugins_package_name 137 loaded_module = _get_loaded_module(f'{_get_plugins_package_name()}.{self.name}') 138 if loaded_module is not None: 139 self._module = loaded_module 140 return self._module 141 142 if self.__file__ is None: 143 return None 144 145 from meerschaum.plugins import import_plugins 146 self._module = import_plugins(str(self), warn=False) 147 148 return self._module 149 150 @property 151 def import_error(self) -> Union[Exception, None]: 152 """ 153 Return the exception raised by this plugin's most recent failed import 154 (i.e. when `Plugin.module` is `None` even though the plugin is installed), 155 otherwise `None`. 156 """ 157 from meerschaum.plugins import _plugins_import_errors 158 return _plugins_import_errors.get(self.name, None) 159 160 161 @property 162 def __file__(self) -> Union[str, None]: 163 """ 164 Return the file path (str) of the plugin if it exists, otherwise `None`. 165 """ 166 if self.__dict__.get('_module', None) is not None: 167 return self.module.__file__ 168 169 import meerschaum.config.paths as paths 170 171 potential_dir = paths.PLUGINS_RESOURCES_PATH / self.name 172 if ( 173 potential_dir.exists() 174 and potential_dir.is_dir() 175 and (potential_dir / '__init__.py').exists() 176 ): 177 return str((potential_dir / '__init__.py').as_posix()) 178 179 potential_file = paths.PLUGINS_RESOURCES_PATH / (self.name + '.py') 180 if potential_file.exists() and not potential_file.is_dir(): 181 return str(potential_file.as_posix()) 182 183 return None 184 185 186 @property 187 def requirements_file_path(self) -> Union[pathlib.Path, None]: 188 """ 189 If a file named `requirements.txt` exists, return its path. 190 """ 191 if self.__file__ is None: 192 return None 193 path = pathlib.Path(self.__file__).parent / 'requirements.txt' 194 if not path.exists(): 195 return None 196 return path 197 198 199 def is_installed(self, **kw) -> bool: 200 """ 201 Check whether a plugin is correctly installed. 202 203 Returns 204 ------- 205 A `bool` indicating whether a plugin exists and is successfully imported. 206 """ 207 return self.__file__ is not None 208 209 210 def make_tar(self, debug: bool = False) -> pathlib.Path: 211 """ 212 Compress the plugin's source files into a `.tar.gz` archive and return the archive's path. 213 214 Parameters 215 ---------- 216 debug: bool, default False 217 Verbosity toggle. 218 219 Returns 220 ------- 221 A `pathlib.Path` to the archive file's path. 222 223 """ 224 import tarfile, pathlib, subprocess, fnmatch 225 from meerschaum.utils.debug import dprint 226 from meerschaum.utils.packages import attempt_import 227 pathspec = attempt_import('pathspec', debug=debug) 228 229 if not self.__file__: 230 from meerschaum.utils.warnings import error 231 error(f"Could not find file for plugin '{self}'.") 232 if '__init__.py' in self.__file__ or os.path.isdir(self.__file__): 233 path = self.__file__.replace('__init__.py', '') 234 is_dir = True 235 else: 236 path = self.__file__ 237 is_dir = False 238 239 old_cwd = os.getcwd() 240 real_parent_path = pathlib.Path(os.path.realpath(path)).parent 241 os.chdir(real_parent_path) 242 243 default_patterns_to_ignore = [ 244 '.pyc', 245 '__pycache__/', 246 'eggs/', 247 '__pypackages__/', 248 '.git', 249 ] 250 251 def parse_gitignore() -> 'Set[str]': 252 gitignore_path = pathlib.Path(path) / '.gitignore' 253 if not gitignore_path.exists(): 254 return set(default_patterns_to_ignore) 255 with open(gitignore_path, 'r', encoding='utf-8') as f: 256 gitignore_text = f.read() 257 return set(pathspec.PathSpec.from_lines( 258 pathspec.patterns.GitWildMatchPattern, 259 default_patterns_to_ignore + gitignore_text.splitlines() 260 ).match_tree(path)) 261 262 patterns_to_ignore = parse_gitignore() if is_dir else set() 263 264 if debug: 265 dprint(f"Patterns to ignore:\n{patterns_to_ignore}") 266 267 with tarfile.open(self.archive_path, 'w:gz') as tarf: 268 if not is_dir: 269 tarf.add(f"{self.name}.py") 270 else: 271 for root, dirs, files in os.walk(self.name): 272 for f in files: 273 good_file = True 274 fp = os.path.join(root, f) 275 for pattern in patterns_to_ignore: 276 if pattern in str(fp) or f.startswith('.'): 277 good_file = False 278 break 279 if good_file: 280 if debug: 281 dprint(f"Adding '{fp}'...") 282 tarf.add(fp) 283 284 ### clean up and change back to old directory 285 os.chdir(old_cwd) 286 287 ### change to 775 to avoid permissions issues with the API in a Docker container 288 self.archive_path.chmod(0o775) 289 290 if debug: 291 dprint(f"Created archive '{self.archive_path}'.") 292 return self.archive_path 293 294 295 def install( 296 self, 297 skip_deps: bool = False, 298 force: bool = False, 299 debug: bool = False, 300 ) -> SuccessTuple: 301 """ 302 Extract a plugin's tar archive to the plugins directory. 303 304 This function checks if the plugin is already installed and if the version is equal or 305 greater than the existing installation. 306 307 Parameters 308 ---------- 309 skip_deps: bool, default False 310 If `True`, do not install dependencies. 311 312 force: bool, default False 313 If `True`, continue with installation, even if required packages fail to install. 314 315 debug: bool, default False 316 Verbosity toggle. 317 318 Returns 319 ------- 320 A `SuccessTuple` of success (bool) and a message (str). 321 322 """ 323 installation_key = self.full_name 324 with _ongoing_installations_lock: 325 if installation_key in _ongoing_installations: 326 return True, f"Already installing plugin '{self}'." 327 _ongoing_installations.add(installation_key) 328 329 try: 330 from meerschaum.utils.packages import _pip_install_lock 331 with _pip_install_lock(self.name): 332 return self._install_with_rollback( 333 skip_deps=skip_deps, 334 force=force, 335 debug=debug, 336 ) 337 finally: 338 with _ongoing_installations_lock: 339 _ongoing_installations.discard(installation_key) 340 341 342 def _install_with_rollback( 343 self, 344 skip_deps: bool = False, 345 force: bool = False, 346 debug: bool = False, 347 ) -> SuccessTuple: 348 """Restore the previous plugin source and environment after a failed install.""" 349 import tempfile 350 import meerschaum.config.paths as paths 351 from meerschaum.plugins import sync_plugins_symlinks 352 from meerschaum.utils.packages import reload_meerschaum 353 354 if not self.archive_path.exists(): 355 return False, f"Missing archive file for plugin '{self}'." 356 357 paths.PLUGINS_TEMP_RESOURCES_PATH.mkdir(parents=True, exist_ok=True) 358 source_paths = [ 359 plugins_dir_path / filename 360 for plugins_dir_path in paths.PLUGINS_DIR_PATHS 361 for filename in (self.name, self.name + '.py') 362 ] 363 tracked_paths = source_paths + [paths.VIRTENV_RESOURCES_PATH / self.name] 364 365 with tempfile.TemporaryDirectory( 366 prefix=f'.{self.name}-rollback-', 367 dir=paths.PLUGINS_TEMP_RESOURCES_PATH, 368 ) as rollback_dir_str: 369 rollback_dir = pathlib.Path(rollback_dir_str) 370 backups = {} 371 rollback_prepared = False 372 373 def prepare_rollback() -> None: 374 nonlocal rollback_prepared 375 for index, path in enumerate(tracked_paths): 376 if not path.exists() and not path.is_symlink(): 377 continue 378 backup_path = rollback_dir / str(index) 379 _copy_path(path, backup_path) 380 backups[path] = backup_path 381 rollback_prepared = True 382 383 def rollback() -> None: 384 if not rollback_prepared: 385 return 386 for path in tracked_paths: 387 _remove_path(path) 388 for path, backup_path in backups.items(): 389 path.parent.mkdir(parents=True, exist_ok=True) 390 shutil.move(str(backup_path), str(path)) 391 self.__dict__.pop('_module', None) 392 self.__dict__.pop('_required', None) 393 sync_plugins_symlinks(debug=debug) 394 reload_meerschaum(debug=debug) 395 396 try: 397 result = self._install( 398 skip_deps=skip_deps, 399 force=force, 400 debug=debug, 401 _prepare_rollback=prepare_rollback, 402 ) 403 except BaseException: 404 rollback() 405 raise 406 if not result[0]: 407 try: 408 rollback() 409 except Exception as e: 410 return False, result[1] + f"\nFailed to roll back plugin '{self}': {e}" 411 return result 412 413 414 def _install( 415 self, 416 skip_deps: bool = False, 417 force: bool = False, 418 debug: bool = False, 419 _prepare_rollback: Optional[Any] = None, 420 ) -> SuccessTuple: 421 422 import meerschaum.config.paths as paths 423 from meerschaum.utils.warnings import warn, error 424 if debug: 425 from meerschaum.utils.debug import dprint 426 import tarfile 427 import re 428 import ast 429 from meerschaum.plugins import sync_plugins_symlinks 430 from meerschaum.utils.packages import attempt_import, reload_meerschaum 431 from meerschaum.utils.venv import init_venv 432 from meerschaum.utils.misc import safely_extract_tar 433 old_cwd = os.getcwd() 434 old_version = '' 435 new_version = '' 436 temp_dir = paths.PLUGINS_TEMP_RESOURCES_PATH / self.name 437 temp_dir.mkdir(exist_ok=True) 438 439 if not self.archive_path.exists(): 440 return False, f"Missing archive file for plugin '{self}'." 441 if self.version is not None: 442 old_version = self.version 443 if debug: 444 dprint(f"Found existing version '{old_version}' for plugin '{self}'.") 445 446 if debug: 447 dprint(f"Extracting '{self.archive_path}' to '{temp_dir}'...") 448 449 try: 450 with tarfile.open(self.archive_path, 'r:gz') as tarf: 451 safely_extract_tar(tarf, temp_dir) 452 except Exception as e: 453 warn(e) 454 return False, f"Failed to extract plugin '{self.name}'." 455 456 ### search for version information 457 files = os.listdir(temp_dir) 458 459 if str(files[0]) == self.name: 460 is_dir = True 461 elif str(files[0]) == self.name + '.py': 462 is_dir = False 463 else: 464 error(f"Unknown format encountered for plugin '{self}'.") 465 466 fpath = temp_dir / files[0] 467 if is_dir: 468 fpath = fpath / '__init__.py' 469 470 with open(fpath, 'r', encoding='utf-8') as f: 471 init_lines = f.readlines() 472 new_version = None 473 for line in init_lines: 474 if '__version__' not in line: 475 continue 476 version_match = re.search(r'__version__(\s?)=', line.lstrip().rstrip()) 477 if not version_match: 478 continue 479 new_version = ast.literal_eval(line.split('=')[1].lstrip().rstrip()) 480 break 481 if not new_version: 482 warn( 483 f"No `__version__` defined for plugin '{self}'. " 484 + "Assuming new version...", 485 stack = False, 486 ) 487 488 packaging_version = attempt_import('packaging.version') 489 try: 490 is_new_version = (not new_version and not old_version) or ( 491 packaging_version.parse(old_version) < packaging_version.parse(new_version) 492 ) 493 is_same_version = new_version and old_version and ( 494 packaging_version.parse(old_version) == packaging_version.parse(new_version) 495 ) 496 except Exception: 497 is_new_version, is_same_version = True, False 498 499 ### Determine where to permanently store the new plugin. 500 plugin_installation_dir_path = paths.PLUGINS_DIR_PATHS[0] 501 for path in paths.PLUGINS_DIR_PATHS: 502 if not path.exists(): 503 warn(f"Plugins path does not exist: {path}", stack=False) 504 continue 505 506 files_in_plugins_dir = os.listdir(path) 507 if ( 508 self.name in files_in_plugins_dir 509 or 510 (self.name + '.py') in files_in_plugins_dir 511 ): 512 plugin_installation_dir_path = path 513 break 514 515 success_msg = ( 516 f"Successfully installed plugin '{self}'" 517 + ("\n (skipped dependencies)" if skip_deps else "") 518 + "." 519 ) 520 success, abort = None, None 521 522 if is_same_version and not force: 523 success, msg = True, ( 524 f"Plugin '{self}' is up-to-date (version {old_version}).\n" + 525 " Install again with `-f` or `--force` to reinstall." 526 ) 527 abort = True 528 elif is_new_version or force: 529 if _prepare_rollback is not None: 530 _prepare_rollback() 531 source_path = temp_dir / files[0] 532 destination_path = plugin_installation_dir_path / files[0] 533 try: 534 plugin_installation_dir_path.mkdir(parents=True, exist_ok=True) 535 for existing_path in ( 536 plugin_installation_dir_path / self.name, 537 plugin_installation_dir_path / (self.name + '.py'), 538 ): 539 _remove_path(existing_path) 540 if debug: 541 dprint(f"Moving '{source_path}' to '{destination_path}'...") 542 shutil.move(str(source_path), str(destination_path)) 543 except Exception as e: 544 success, msg = False, f"Failed to install plugin '{self}': {e}" 545 else: 546 success, msg = True, success_msg 547 else: 548 success, msg = False, ( 549 f"Your installed version of plugin '{self}' ({old_version}) is higher than " 550 + f"attempted version {new_version}." 551 ) 552 553 shutil.rmtree(temp_dir) 554 os.chdir(old_cwd) 555 556 ### Reload the plugin's module. 557 sync_plugins_symlinks(debug=debug) 558 if '_module' in self.__dict__: 559 del self.__dict__['_module'] 560 init_venv(venv=self.name, force=True, debug=debug) 561 reload_meerschaum(debug=debug) 562 563 ### Record the origin only after a successful transaction. Locally-installed 564 ### plugins have no explicit repo and are intentionally skipped. 565 _origin_repo_keys = ( 566 str(self._repo_connector) 567 if self._repo_connector is not None 568 else self._repo_keys or self._repo_in_name 569 ) 570 571 def write_origin() -> None: 572 if not _origin_repo_keys: 573 return 574 try: 575 from meerschaum.plugins._origins import write_plugin_origin 576 write_plugin_origin( 577 self.name, 578 _origin_repo_keys, 579 plugin_installation_dir_path, 580 debug=debug, 581 ) 582 except Exception: 583 pass 584 585 if abort: 586 write_origin() 587 588 ### if we've already failed, return here 589 if not success or abort: 590 return success, msg 591 592 ### attempt to install dependencies 593 dependencies_installed = skip_deps or self.install_dependencies(force=force, debug=debug) 594 if not dependencies_installed: 595 return False, f"Failed to install dependencies for plugin '{self}'." 596 597 ### handling success tuple, bool, or other (typically None) 598 setup_tuple = self.setup(debug=debug) 599 if isinstance(setup_tuple, tuple): 600 if not setup_tuple[0]: 601 success, msg = setup_tuple 602 elif isinstance(setup_tuple, bool): 603 if not setup_tuple: 604 success, msg = False, ( 605 f"Failed to run post-install setup for plugin '{self}'." + '\n' + 606 f"Check `setup()` in '{self.__file__}' for more information " + 607 "(no error message provided)." 608 ) 609 else: 610 success, msg = True, success_msg 611 elif setup_tuple is None: 612 success = True 613 msg = ( 614 f"Post-install for plugin '{self}' returned None. " + 615 "Assuming plugin successfully installed." 616 ) 617 warn(msg) 618 else: 619 success = False 620 msg = ( 621 f"Post-install for plugin '{self}' returned unexpected value " + 622 f"of type '{type(setup_tuple)}': {setup_tuple}" 623 ) 624 625 _ = self.module 626 if success: 627 write_origin() 628 return success, msg 629 630 631 def remove_archive( 632 self, 633 debug: bool = False 634 ) -> SuccessTuple: 635 """Remove a plugin's archive file.""" 636 if not self.archive_path.exists(): 637 return True, f"Archive file for plugin '{self}' does not exist." 638 try: 639 self.archive_path.unlink() 640 except Exception as e: 641 return False, f"Failed to remove archive for plugin '{self}':\n{e}" 642 return True, "Success" 643 644 645 def remove_venv( 646 self, 647 debug: bool = False 648 ) -> SuccessTuple: 649 """Remove a plugin's virtual environment.""" 650 if not self.venv_path.exists(): 651 return True, f"Virtual environment for plugin '{self}' does not exist." 652 try: 653 shutil.rmtree(self.venv_path) 654 except Exception as e: 655 return False, f"Failed to remove virtual environment for plugin '{self}':\n{e}" 656 return True, "Success" 657 658 659 def uninstall(self, debug: bool = False) -> SuccessTuple: 660 """ 661 Remove a plugin, its virtual environment, and archive file. 662 """ 663 from meerschaum.utils.packages import reload_meerschaum 664 from meerschaum.plugins import sync_plugins_symlinks 665 from meerschaum.utils.warnings import warn, info 666 warnings_thrown_count: int = 0 667 max_warnings: int = 3 668 669 if not self.is_installed(): 670 info( 671 f"Plugin '{self.name}' doesn't seem to be installed.\n " 672 + "Checking for artifacts...", 673 stack = False, 674 ) 675 else: 676 real_path = pathlib.Path(os.path.realpath(self.__file__)) 677 try: 678 if real_path.name == '__init__.py': 679 shutil.rmtree(real_path.parent) 680 else: 681 real_path.unlink() 682 except Exception as e: 683 warn(f"Could not remove source files for plugin '{self.name}':\n{e}", stack=False) 684 warnings_thrown_count += 1 685 else: 686 info(f"Removed source files for plugin '{self.name}'.") 687 688 if self.venv_path.exists(): 689 success, msg = self.remove_venv(debug=debug) 690 if not success: 691 warn(msg, stack=False) 692 warnings_thrown_count += 1 693 else: 694 info(f"Removed virtual environment from plugin '{self.name}'.") 695 696 success = warnings_thrown_count < max_warnings 697 try: 698 from meerschaum.plugins._origins import remove_plugin_origin 699 remove_plugin_origin(self.name, debug=debug) 700 except Exception: 701 pass 702 sync_plugins_symlinks(debug=debug) 703 self.deactivate_venv(force=True, debug=debug) 704 reload_meerschaum(debug=debug) 705 return success, ( 706 f"Successfully uninstalled plugin '{self}'." if success 707 else f"Failed to uninstall plugin '{self}'." 708 ) 709 710 711 def setup(self, *args: str, debug: bool = False, **kw: Any) -> Union[SuccessTuple, bool]: 712 """ 713 If exists, run the plugin's `setup()` function. 714 715 Parameters 716 ---------- 717 *args: str 718 The positional arguments passed to the `setup()` function. 719 720 debug: bool, default False 721 Verbosity toggle. 722 723 **kw: Any 724 The keyword arguments passed to the `setup()` function. 725 726 Returns 727 ------- 728 A `SuccessTuple` or `bool` indicating success. 729 730 """ 731 from meerschaum.utils.debug import dprint 732 import inspect 733 _setup = None 734 for name, fp in inspect.getmembers(self.module): 735 if name == 'setup' and inspect.isfunction(fp): 736 _setup = fp 737 break 738 739 ### assume success if no setup() is found (not necessary) 740 if _setup is None: 741 return True 742 743 sig = inspect.signature(_setup) 744 has_debug, has_kw = ('debug' in sig.parameters), False 745 for k, v in sig.parameters.items(): 746 if '**' in str(v): 747 has_kw = True 748 break 749 750 _kw = {} 751 if has_kw: 752 _kw.update(kw) 753 if has_debug: 754 _kw['debug'] = debug 755 756 if debug: 757 dprint(f"Running setup for plugin '{self}'...") 758 from meerschaum.utils.venv import Venv 759 try: 760 with Venv(self, debug=debug): 761 return_tuple = _setup(*args, **_kw) 762 except Exception as e: 763 return False, str(e) 764 765 if isinstance(return_tuple, tuple): 766 return return_tuple 767 if isinstance(return_tuple, bool): 768 return return_tuple, f"Setup for Plugin '{self.name}' did not return a message." 769 if return_tuple is None: 770 return False, f"Setup for Plugin '{self.name}' returned None." 771 return False, f"Unknown return value from setup for Plugin '{self.name}': {return_tuple}" 772 773 774 def get_dependencies( 775 self, 776 debug: bool = False, 777 ) -> List[str]: 778 """ 779 If the Plugin has specified dependencies in a list called `required`, return the list. 780 781 **NOTE:** Dependecies which start with `'plugin:'` are Meerschaum plugins, not pip packages. 782 Meerschaum plugins may also specify connector keys for a repo after `'@'`. 783 784 Parameters 785 ---------- 786 debug: bool, default False 787 Verbosity toggle. 788 789 Returns 790 ------- 791 A list of required packages and plugins (str). 792 793 """ 794 if '_required' in self.__dict__: 795 return self._required 796 797 ### If the plugin has not yet been imported, 798 ### infer the dependencies from the source text. 799 ### This is not super robust, and it doesn't feel right 800 ### having multiple versions of the logic. 801 ### This is necessary when determining the activation order 802 ### without having import the module. 803 ### For consistency's sake, the module-less method does not cache the requirements. 804 if self.__dict__.get('_module', None) is None: 805 file_path = self.__file__ 806 if file_path is None: 807 return [] 808 with open(file_path, 'r', encoding='utf-8') as f: 809 text = f.read() 810 811 if 'required' not in text: 812 return [] 813 814 ### This has some limitations: 815 ### It relies on `required` being manually declared. 816 ### We lose the ability to dynamically alter the `required` list, 817 ### which is why we've kept the module-reliant method below. 818 import ast, re 819 ### NOTE: This technically would break 820 ### if `required` was the very first line of the file. 821 req_start_match = re.search(r'\nrequired(:\s*)?.*=', text) 822 if not req_start_match: 823 return [] 824 req_start = req_start_match.start() 825 equals_sign = req_start + text[req_start:].find('=') 826 827 ### Dependencies may have brackets within the strings, so push back the index. 828 first_opening_brace = equals_sign + 1 + text[equals_sign:].find('[') 829 if first_opening_brace == -1: 830 return [] 831 832 next_closing_brace = equals_sign + 1 + text[equals_sign:].find(']') 833 if next_closing_brace == -1: 834 return [] 835 836 start_ix = first_opening_brace + 1 837 end_ix = next_closing_brace 838 839 num_braces = 0 840 while True: 841 if '[' not in text[start_ix:end_ix]: 842 break 843 num_braces += 1 844 start_ix = end_ix 845 end_ix += text[end_ix + 1:].find(']') + 1 846 847 req_end = end_ix + 1 848 req_text = ( 849 text[(first_opening_brace-1):req_end] 850 .lstrip() 851 .replace('=', '', 1) 852 .lstrip() 853 .rstrip() 854 ) 855 try: 856 required = ast.literal_eval(req_text) 857 except Exception as e: 858 warn( 859 f"Unable to determine requirements for plugin '{self.name}' " 860 + "without importing the module.\n" 861 + " This may be due to dynamically setting the global `required` list.\n" 862 + f" {e}" 863 ) 864 return [] 865 return required 866 867 import inspect 868 self.activate_venv(dependencies=False, debug=debug) 869 required = [] 870 for name, val in inspect.getmembers(self.module): 871 if name == 'required': 872 required = val 873 break 874 self._required = required 875 self.deactivate_venv(dependencies=False, debug=debug) 876 return required 877 878 879 def get_required_plugins(self, debug: bool=False) -> List[mrsm.plugins.Plugin]: 880 """ 881 Return a list of required Plugin objects. 882 """ 883 from meerschaum.utils.warnings import warn 884 from meerschaum.config import get_config 885 from meerschaum._internal.static import STATIC_CONFIG 886 from meerschaum.connectors.parse import is_valid_connector_keys 887 plugins = [] 888 _deps = self.get_dependencies(debug=debug) 889 sep = STATIC_CONFIG['plugins']['repo_separator'] 890 plugin_names = [ 891 _d[len('plugin:'):] for _d in _deps 892 if _d.startswith('plugin:') and len(_d) > len('plugin:') 893 ] 894 default_repo_keys = get_config('meerschaum', 'repository') 895 skipped_repo_keys = set() 896 897 for _plugin_name in plugin_names: 898 if sep in _plugin_name: 899 try: 900 _plugin_name, _repo_keys = _plugin_name.split(sep) 901 except Exception: 902 _repo_keys = default_repo_keys 903 warn( 904 f"Invalid repo keys for required plugin '{_plugin_name}'.\n " 905 + f"Will try to use '{_repo_keys}' instead.", 906 stack = False, 907 ) 908 else: 909 _repo_keys = default_repo_keys 910 911 if _repo_keys in skipped_repo_keys: 912 continue 913 914 if not is_valid_connector_keys(_repo_keys): 915 warn( 916 f"Invalid connector '{_repo_keys}'.\n" 917 f" Skipping required plugins from repository '{_repo_keys}'", 918 stack=False, 919 ) 920 continue 921 922 plugins.append(Plugin(_plugin_name, repo=_repo_keys)) 923 924 return plugins 925 926 927 def get_required_packages(self, debug: bool=False) -> List[str]: 928 """ 929 Return the required package names (excluding plugins). 930 """ 931 _deps = self.get_dependencies(debug=debug) 932 return [_d for _d in _deps if not _d.startswith('plugin:')] 933 934 935 def activate_venv( 936 self, 937 dependencies: bool = True, 938 init_if_not_exists: bool = True, 939 debug: bool = False, 940 **kw 941 ) -> bool: 942 """ 943 Activate the virtual environments for the plugin and its dependencies. 944 945 Parameters 946 ---------- 947 dependencies: bool, default True 948 If `True`, activate the virtual environments for required plugins. 949 950 Returns 951 ------- 952 A bool indicating success. 953 """ 954 import meerschaum.config.paths as paths 955 from meerschaum.utils.venv import venv_target_path 956 from meerschaum.utils.packages import activate_venv 957 from meerschaum.utils.misc import make_symlink, is_symlink 958 959 if dependencies: 960 for plugin in self.get_required_plugins(debug=debug): 961 plugin.activate_venv(debug=debug, init_if_not_exists=init_if_not_exists, **kw) 962 963 vtp = venv_target_path(self.name, debug=debug, allow_nonexistent=True) 964 venv_meerschaum_path = vtp / 'meerschaum' 965 966 try: 967 success, msg = True, "Success" 968 if is_symlink(venv_meerschaum_path): 969 if pathlib.Path(os.path.realpath(venv_meerschaum_path)) != paths.PACKAGE_ROOT_PATH: 970 venv_meerschaum_path.unlink() 971 success, msg = make_symlink(venv_meerschaum_path, paths.PACKAGE_ROOT_PATH) 972 except Exception as e: 973 success, msg = False, str(e) 974 if not success: 975 warn( 976 f"Unable to create symlink {venv_meerschaum_path} to {paths.PACKAGE_ROOT_PATH}:\n" 977 f"{msg}" 978 ) 979 980 return activate_venv(self.name, init_if_not_exists=init_if_not_exists, debug=debug, **kw) 981 982 983 def deactivate_venv(self, dependencies: bool=True, debug: bool = False, **kw) -> bool: 984 """ 985 Deactivate the virtual environments for the plugin and its dependencies. 986 987 Parameters 988 ---------- 989 dependencies: bool, default True 990 If `True`, deactivate the virtual environments for required plugins. 991 992 Returns 993 ------- 994 A bool indicating success. 995 """ 996 from meerschaum.utils.packages import deactivate_venv 997 success = deactivate_venv(self.name, debug=debug, **kw) 998 if dependencies: 999 for plugin in self.get_required_plugins(debug=debug): 1000 plugin.deactivate_venv(debug=debug, **kw) 1001 return success 1002 1003 1004 def install_dependencies( 1005 self, 1006 force: bool = False, 1007 debug: bool = False, 1008 ) -> bool: 1009 """ 1010 If specified, install dependencies. 1011 1012 **NOTE:** Dependencies that start with `'plugin:'` will be installed as 1013 Meerschaum plugins from the same repository as this Plugin. 1014 To install from a different repository, add the repo keys after `'@'` 1015 (e.g. `'plugin:foo@api:bar'`). 1016 1017 Parameters 1018 ---------- 1019 force: bool, default False 1020 If `True`, continue with the installation, even if some 1021 required packages fail to install. 1022 1023 debug: bool, default False 1024 Verbosity toggle. 1025 1026 Returns 1027 ------- 1028 A bool indicating success. 1029 """ 1030 from meerschaum.utils.packages import pip_install, venv_contains_package 1031 from meerschaum.utils.warnings import warn, info 1032 _deps = self.get_dependencies(debug=debug) 1033 if not _deps and self.requirements_file_path is None: 1034 return True 1035 1036 plugins = self.get_required_plugins(debug=debug) 1037 for _plugin in plugins: 1038 if _plugin.name == self.name: 1039 warn(f"Plugin '{self.name}' cannot depend on itself! Skipping...", stack=False) 1040 continue 1041 _success, _msg = _plugin.repo_connector.install_plugin( 1042 _plugin.name, debug=debug, force=force 1043 ) 1044 if not _success: 1045 warn( 1046 f"Failed to install required plugin '{_plugin}' from '{_plugin.repo_connector}'" 1047 + f" for plugin '{self.name}':\n" + _msg, 1048 stack = False, 1049 ) 1050 if not force: 1051 warn( 1052 "Try installing with the `--force` flag to continue anyway.", 1053 stack = False, 1054 ) 1055 return False 1056 info( 1057 "Continuing with installation despite the failure " 1058 + "(careful, things might be broken!)...", 1059 icon = False 1060 ) 1061 1062 1063 ### First step: parse `requirements.txt` if it exists. 1064 if self.requirements_file_path is not None: 1065 if not pip_install( 1066 requirements_file_path=self.requirements_file_path, 1067 venv=self.name, debug=debug 1068 ): 1069 warn( 1070 f"Failed to resolve 'requirements.txt' for plugin '{self.name}'.", 1071 stack = False, 1072 ) 1073 if not force: 1074 warn( 1075 "Try installing with `--force` to continue anyway.", 1076 stack = False, 1077 ) 1078 return False 1079 info( 1080 "Continuing with installation despite the failure " 1081 + "(careful, things might be broken!)...", 1082 icon = False 1083 ) 1084 1085 1086 ### Don't reinstall packages that are already included in required plugins. 1087 packages = [] 1088 _packages = self.get_required_packages(debug=debug) 1089 accounted_for_packages = set() 1090 for package_name in _packages: 1091 for plugin in plugins: 1092 if venv_contains_package(package_name, plugin.name): 1093 accounted_for_packages.add(package_name) 1094 break 1095 packages = [pkg for pkg in _packages if pkg not in accounted_for_packages] 1096 1097 ### Attempt pip packages installation. 1098 if packages: 1099 for package in packages: 1100 if not pip_install(package, venv=self.name, debug=debug): 1101 warn( 1102 f"Failed to install required package '{package}'" 1103 + f" for plugin '{self.name}'.", 1104 stack = False, 1105 ) 1106 if not force: 1107 warn( 1108 "Try installing with `--force` to continue anyway.", 1109 stack = False, 1110 ) 1111 return False 1112 info( 1113 "Continuing with installation despite the failure " 1114 + "(careful, things might be broken!)...", 1115 icon = False 1116 ) 1117 return True 1118 1119 1120 @property 1121 def full_name(self) -> str: 1122 """ 1123 Include the repo keys with the plugin's name. 1124 """ 1125 from meerschaum._internal.static import STATIC_CONFIG 1126 sep = STATIC_CONFIG['plugins']['repo_separator'] 1127 return self.name + sep + str(self.repo_connector) 1128 1129 1130 def __str__(self): 1131 return self.name 1132 1133 1134 def __repr__(self): 1135 return f"Plugin('{self.name}', repo='{self.repo_connector}')" 1136 1137 1138 def __del__(self): 1139 pass
Handle packaging of Meerschaum plugins.
53 def __init__( 54 self, 55 name: str, 56 version: Optional[str] = None, 57 user_id: Optional[int] = None, 58 required: Optional[List[str]] = None, 59 attributes: Optional[Dict[str, Any]] = None, 60 archive_path: Optional[pathlib.Path] = None, 61 venv_path: Optional[pathlib.Path] = None, 62 repo_connector: Optional['mrsm.connectors.api.APIConnector'] = None, 63 repo: Union['mrsm.connectors.api.APIConnector', str, None] = None, 64 ): 65 import meerschaum.config.paths as paths 66 from meerschaum._internal.static import STATIC_CONFIG 67 sep = STATIC_CONFIG['plugins']['repo_separator'] 68 _repo = None 69 if sep in name: 70 try: 71 name, _repo = name.split(sep) 72 except Exception as e: 73 error(f"Invalid plugin name: '{name}'") 74 self._repo_in_name = _repo 75 76 if attributes is None: 77 attributes = {} 78 self.name = name 79 self.attributes = attributes 80 self.user_id = user_id 81 self._version = version 82 if required: 83 self._required = required 84 self.archive_path = ( 85 archive_path if archive_path is not None 86 else paths.PLUGINS_ARCHIVES_RESOURCES_PATH / f"{self.name}.tar.gz" 87 ) 88 self.venv_path = ( 89 venv_path if venv_path is not None 90 else paths.VIRTENV_RESOURCES_PATH / self.name 91 ) 92 self._repo_connector = repo_connector 93 self._repo_keys = repo
96 @property 97 def repo_connector(self): 98 """ 99 Return the repository connector for this plugin. 100 NOTE: This imports the `connectors` module, which imports certain plugin modules. 101 """ 102 if self._repo_connector is None: 103 from meerschaum.connectors.parse import parse_repo_keys 104 105 repo_keys = self._repo_keys or self._repo_in_name 106 if self._repo_in_name and self._repo_keys and self._repo_keys != self._repo_in_name: 107 error( 108 f"Received inconsistent repos: '{self._repo_in_name}' and '{self._repo_keys}'." 109 ) 110 repo_connector = parse_repo_keys(repo_keys) 111 self._repo_connector = repo_connector 112 return self._repo_connector
Return the repository connector for this plugin.
NOTE: This imports the connectors module, which imports certain plugin modules.
115 @property 116 def version(self): 117 """ 118 Return the plugin's module version is defined (`__version__`) if it's defined. 119 """ 120 if self._version is None: 121 try: 122 self._version = self.module.__version__ 123 except Exception as e: 124 self._version = None 125 return self._version
Return the plugin's module version is defined (__version__) if it's defined.
128 @property 129 def module(self): 130 """ 131 Return the Python module of the underlying plugin, 132 or `None` if the plugin is not installed or its import failed 133 (check `Plugin.import_error` for the swallowed exception). 134 """ 135 if '_module' not in self.__dict__ or self.__dict__.get('_module', None) is None: 136 from meerschaum.plugins import _get_loaded_module, _get_plugins_package_name 137 loaded_module = _get_loaded_module(f'{_get_plugins_package_name()}.{self.name}') 138 if loaded_module is not None: 139 self._module = loaded_module 140 return self._module 141 142 if self.__file__ is None: 143 return None 144 145 from meerschaum.plugins import import_plugins 146 self._module = import_plugins(str(self), warn=False) 147 148 return self._module
Return the Python module of the underlying plugin,
or None if the plugin is not installed or its import failed
(check Plugin.import_error for the swallowed exception).
150 @property 151 def import_error(self) -> Union[Exception, None]: 152 """ 153 Return the exception raised by this plugin's most recent failed import 154 (i.e. when `Plugin.module` is `None` even though the plugin is installed), 155 otherwise `None`. 156 """ 157 from meerschaum.plugins import _plugins_import_errors 158 return _plugins_import_errors.get(self.name, None)
Return the exception raised by this plugin's most recent failed import
(i.e. when Plugin.module is None even though the plugin is installed),
otherwise None.
186 @property 187 def requirements_file_path(self) -> Union[pathlib.Path, None]: 188 """ 189 If a file named `requirements.txt` exists, return its path. 190 """ 191 if self.__file__ is None: 192 return None 193 path = pathlib.Path(self.__file__).parent / 'requirements.txt' 194 if not path.exists(): 195 return None 196 return path
If a file named requirements.txt exists, return its path.
199 def is_installed(self, **kw) -> bool: 200 """ 201 Check whether a plugin is correctly installed. 202 203 Returns 204 ------- 205 A `bool` indicating whether a plugin exists and is successfully imported. 206 """ 207 return self.__file__ is not None
Check whether a plugin is correctly installed.
Returns
- A
boolindicating whether a plugin exists and is successfully imported.
210 def make_tar(self, debug: bool = False) -> pathlib.Path: 211 """ 212 Compress the plugin's source files into a `.tar.gz` archive and return the archive's path. 213 214 Parameters 215 ---------- 216 debug: bool, default False 217 Verbosity toggle. 218 219 Returns 220 ------- 221 A `pathlib.Path` to the archive file's path. 222 223 """ 224 import tarfile, pathlib, subprocess, fnmatch 225 from meerschaum.utils.debug import dprint 226 from meerschaum.utils.packages import attempt_import 227 pathspec = attempt_import('pathspec', debug=debug) 228 229 if not self.__file__: 230 from meerschaum.utils.warnings import error 231 error(f"Could not find file for plugin '{self}'.") 232 if '__init__.py' in self.__file__ or os.path.isdir(self.__file__): 233 path = self.__file__.replace('__init__.py', '') 234 is_dir = True 235 else: 236 path = self.__file__ 237 is_dir = False 238 239 old_cwd = os.getcwd() 240 real_parent_path = pathlib.Path(os.path.realpath(path)).parent 241 os.chdir(real_parent_path) 242 243 default_patterns_to_ignore = [ 244 '.pyc', 245 '__pycache__/', 246 'eggs/', 247 '__pypackages__/', 248 '.git', 249 ] 250 251 def parse_gitignore() -> 'Set[str]': 252 gitignore_path = pathlib.Path(path) / '.gitignore' 253 if not gitignore_path.exists(): 254 return set(default_patterns_to_ignore) 255 with open(gitignore_path, 'r', encoding='utf-8') as f: 256 gitignore_text = f.read() 257 return set(pathspec.PathSpec.from_lines( 258 pathspec.patterns.GitWildMatchPattern, 259 default_patterns_to_ignore + gitignore_text.splitlines() 260 ).match_tree(path)) 261 262 patterns_to_ignore = parse_gitignore() if is_dir else set() 263 264 if debug: 265 dprint(f"Patterns to ignore:\n{patterns_to_ignore}") 266 267 with tarfile.open(self.archive_path, 'w:gz') as tarf: 268 if not is_dir: 269 tarf.add(f"{self.name}.py") 270 else: 271 for root, dirs, files in os.walk(self.name): 272 for f in files: 273 good_file = True 274 fp = os.path.join(root, f) 275 for pattern in patterns_to_ignore: 276 if pattern in str(fp) or f.startswith('.'): 277 good_file = False 278 break 279 if good_file: 280 if debug: 281 dprint(f"Adding '{fp}'...") 282 tarf.add(fp) 283 284 ### clean up and change back to old directory 285 os.chdir(old_cwd) 286 287 ### change to 775 to avoid permissions issues with the API in a Docker container 288 self.archive_path.chmod(0o775) 289 290 if debug: 291 dprint(f"Created archive '{self.archive_path}'.") 292 return self.archive_path
Compress the plugin's source files into a .tar.gz archive and return the archive's path.
Parameters
- debug (bool, default False): Verbosity toggle.
Returns
- A
pathlib.Pathto the archive file's path.
295 def install( 296 self, 297 skip_deps: bool = False, 298 force: bool = False, 299 debug: bool = False, 300 ) -> SuccessTuple: 301 """ 302 Extract a plugin's tar archive to the plugins directory. 303 304 This function checks if the plugin is already installed and if the version is equal or 305 greater than the existing installation. 306 307 Parameters 308 ---------- 309 skip_deps: bool, default False 310 If `True`, do not install dependencies. 311 312 force: bool, default False 313 If `True`, continue with installation, even if required packages fail to install. 314 315 debug: bool, default False 316 Verbosity toggle. 317 318 Returns 319 ------- 320 A `SuccessTuple` of success (bool) and a message (str). 321 322 """ 323 installation_key = self.full_name 324 with _ongoing_installations_lock: 325 if installation_key in _ongoing_installations: 326 return True, f"Already installing plugin '{self}'." 327 _ongoing_installations.add(installation_key) 328 329 try: 330 from meerschaum.utils.packages import _pip_install_lock 331 with _pip_install_lock(self.name): 332 return self._install_with_rollback( 333 skip_deps=skip_deps, 334 force=force, 335 debug=debug, 336 ) 337 finally: 338 with _ongoing_installations_lock: 339 _ongoing_installations.discard(installation_key)
Extract a plugin's tar archive to the plugins directory.
This function checks if the plugin is already installed and if the version is equal or greater than the existing installation.
Parameters
- skip_deps (bool, default False):
If
True, do not install dependencies. - force (bool, default False):
If
True, continue with installation, even if required packages fail to install. - debug (bool, default False): Verbosity toggle.
Returns
- A
SuccessTupleof success (bool) and a message (str).
631 def remove_archive( 632 self, 633 debug: bool = False 634 ) -> SuccessTuple: 635 """Remove a plugin's archive file.""" 636 if not self.archive_path.exists(): 637 return True, f"Archive file for plugin '{self}' does not exist." 638 try: 639 self.archive_path.unlink() 640 except Exception as e: 641 return False, f"Failed to remove archive for plugin '{self}':\n{e}" 642 return True, "Success"
Remove a plugin's archive file.
645 def remove_venv( 646 self, 647 debug: bool = False 648 ) -> SuccessTuple: 649 """Remove a plugin's virtual environment.""" 650 if not self.venv_path.exists(): 651 return True, f"Virtual environment for plugin '{self}' does not exist." 652 try: 653 shutil.rmtree(self.venv_path) 654 except Exception as e: 655 return False, f"Failed to remove virtual environment for plugin '{self}':\n{e}" 656 return True, "Success"
Remove a plugin's virtual environment.
659 def uninstall(self, debug: bool = False) -> SuccessTuple: 660 """ 661 Remove a plugin, its virtual environment, and archive file. 662 """ 663 from meerschaum.utils.packages import reload_meerschaum 664 from meerschaum.plugins import sync_plugins_symlinks 665 from meerschaum.utils.warnings import warn, info 666 warnings_thrown_count: int = 0 667 max_warnings: int = 3 668 669 if not self.is_installed(): 670 info( 671 f"Plugin '{self.name}' doesn't seem to be installed.\n " 672 + "Checking for artifacts...", 673 stack = False, 674 ) 675 else: 676 real_path = pathlib.Path(os.path.realpath(self.__file__)) 677 try: 678 if real_path.name == '__init__.py': 679 shutil.rmtree(real_path.parent) 680 else: 681 real_path.unlink() 682 except Exception as e: 683 warn(f"Could not remove source files for plugin '{self.name}':\n{e}", stack=False) 684 warnings_thrown_count += 1 685 else: 686 info(f"Removed source files for plugin '{self.name}'.") 687 688 if self.venv_path.exists(): 689 success, msg = self.remove_venv(debug=debug) 690 if not success: 691 warn(msg, stack=False) 692 warnings_thrown_count += 1 693 else: 694 info(f"Removed virtual environment from plugin '{self.name}'.") 695 696 success = warnings_thrown_count < max_warnings 697 try: 698 from meerschaum.plugins._origins import remove_plugin_origin 699 remove_plugin_origin(self.name, debug=debug) 700 except Exception: 701 pass 702 sync_plugins_symlinks(debug=debug) 703 self.deactivate_venv(force=True, debug=debug) 704 reload_meerschaum(debug=debug) 705 return success, ( 706 f"Successfully uninstalled plugin '{self}'." if success 707 else f"Failed to uninstall plugin '{self}'." 708 )
Remove a plugin, its virtual environment, and archive file.
711 def setup(self, *args: str, debug: bool = False, **kw: Any) -> Union[SuccessTuple, bool]: 712 """ 713 If exists, run the plugin's `setup()` function. 714 715 Parameters 716 ---------- 717 *args: str 718 The positional arguments passed to the `setup()` function. 719 720 debug: bool, default False 721 Verbosity toggle. 722 723 **kw: Any 724 The keyword arguments passed to the `setup()` function. 725 726 Returns 727 ------- 728 A `SuccessTuple` or `bool` indicating success. 729 730 """ 731 from meerschaum.utils.debug import dprint 732 import inspect 733 _setup = None 734 for name, fp in inspect.getmembers(self.module): 735 if name == 'setup' and inspect.isfunction(fp): 736 _setup = fp 737 break 738 739 ### assume success if no setup() is found (not necessary) 740 if _setup is None: 741 return True 742 743 sig = inspect.signature(_setup) 744 has_debug, has_kw = ('debug' in sig.parameters), False 745 for k, v in sig.parameters.items(): 746 if '**' in str(v): 747 has_kw = True 748 break 749 750 _kw = {} 751 if has_kw: 752 _kw.update(kw) 753 if has_debug: 754 _kw['debug'] = debug 755 756 if debug: 757 dprint(f"Running setup for plugin '{self}'...") 758 from meerschaum.utils.venv import Venv 759 try: 760 with Venv(self, debug=debug): 761 return_tuple = _setup(*args, **_kw) 762 except Exception as e: 763 return False, str(e) 764 765 if isinstance(return_tuple, tuple): 766 return return_tuple 767 if isinstance(return_tuple, bool): 768 return return_tuple, f"Setup for Plugin '{self.name}' did not return a message." 769 if return_tuple is None: 770 return False, f"Setup for Plugin '{self.name}' returned None." 771 return False, f"Unknown return value from setup for Plugin '{self.name}': {return_tuple}"
If exists, run the plugin's setup() function.
Parameters
- *args (str):
The positional arguments passed to the
setup()function. - debug (bool, default False): Verbosity toggle.
- **kw (Any):
The keyword arguments passed to the
setup()function.
Returns
- A
SuccessTupleorboolindicating success.
774 def get_dependencies( 775 self, 776 debug: bool = False, 777 ) -> List[str]: 778 """ 779 If the Plugin has specified dependencies in a list called `required`, return the list. 780 781 **NOTE:** Dependecies which start with `'plugin:'` are Meerschaum plugins, not pip packages. 782 Meerschaum plugins may also specify connector keys for a repo after `'@'`. 783 784 Parameters 785 ---------- 786 debug: bool, default False 787 Verbosity toggle. 788 789 Returns 790 ------- 791 A list of required packages and plugins (str). 792 793 """ 794 if '_required' in self.__dict__: 795 return self._required 796 797 ### If the plugin has not yet been imported, 798 ### infer the dependencies from the source text. 799 ### This is not super robust, and it doesn't feel right 800 ### having multiple versions of the logic. 801 ### This is necessary when determining the activation order 802 ### without having import the module. 803 ### For consistency's sake, the module-less method does not cache the requirements. 804 if self.__dict__.get('_module', None) is None: 805 file_path = self.__file__ 806 if file_path is None: 807 return [] 808 with open(file_path, 'r', encoding='utf-8') as f: 809 text = f.read() 810 811 if 'required' not in text: 812 return [] 813 814 ### This has some limitations: 815 ### It relies on `required` being manually declared. 816 ### We lose the ability to dynamically alter the `required` list, 817 ### which is why we've kept the module-reliant method below. 818 import ast, re 819 ### NOTE: This technically would break 820 ### if `required` was the very first line of the file. 821 req_start_match = re.search(r'\nrequired(:\s*)?.*=', text) 822 if not req_start_match: 823 return [] 824 req_start = req_start_match.start() 825 equals_sign = req_start + text[req_start:].find('=') 826 827 ### Dependencies may have brackets within the strings, so push back the index. 828 first_opening_brace = equals_sign + 1 + text[equals_sign:].find('[') 829 if first_opening_brace == -1: 830 return [] 831 832 next_closing_brace = equals_sign + 1 + text[equals_sign:].find(']') 833 if next_closing_brace == -1: 834 return [] 835 836 start_ix = first_opening_brace + 1 837 end_ix = next_closing_brace 838 839 num_braces = 0 840 while True: 841 if '[' not in text[start_ix:end_ix]: 842 break 843 num_braces += 1 844 start_ix = end_ix 845 end_ix += text[end_ix + 1:].find(']') + 1 846 847 req_end = end_ix + 1 848 req_text = ( 849 text[(first_opening_brace-1):req_end] 850 .lstrip() 851 .replace('=', '', 1) 852 .lstrip() 853 .rstrip() 854 ) 855 try: 856 required = ast.literal_eval(req_text) 857 except Exception as e: 858 warn( 859 f"Unable to determine requirements for plugin '{self.name}' " 860 + "without importing the module.\n" 861 + " This may be due to dynamically setting the global `required` list.\n" 862 + f" {e}" 863 ) 864 return [] 865 return required 866 867 import inspect 868 self.activate_venv(dependencies=False, debug=debug) 869 required = [] 870 for name, val in inspect.getmembers(self.module): 871 if name == 'required': 872 required = val 873 break 874 self._required = required 875 self.deactivate_venv(dependencies=False, debug=debug) 876 return required
If the Plugin has specified dependencies in a list called required, return the list.
NOTE: Dependecies which start with 'plugin:' are Meerschaum plugins, not pip packages.
Meerschaum plugins may also specify connector keys for a repo after '@'.
Parameters
- debug (bool, default False): Verbosity toggle.
Returns
- A list of required packages and plugins (str).
879 def get_required_plugins(self, debug: bool=False) -> List[mrsm.plugins.Plugin]: 880 """ 881 Return a list of required Plugin objects. 882 """ 883 from meerschaum.utils.warnings import warn 884 from meerschaum.config import get_config 885 from meerschaum._internal.static import STATIC_CONFIG 886 from meerschaum.connectors.parse import is_valid_connector_keys 887 plugins = [] 888 _deps = self.get_dependencies(debug=debug) 889 sep = STATIC_CONFIG['plugins']['repo_separator'] 890 plugin_names = [ 891 _d[len('plugin:'):] for _d in _deps 892 if _d.startswith('plugin:') and len(_d) > len('plugin:') 893 ] 894 default_repo_keys = get_config('meerschaum', 'repository') 895 skipped_repo_keys = set() 896 897 for _plugin_name in plugin_names: 898 if sep in _plugin_name: 899 try: 900 _plugin_name, _repo_keys = _plugin_name.split(sep) 901 except Exception: 902 _repo_keys = default_repo_keys 903 warn( 904 f"Invalid repo keys for required plugin '{_plugin_name}'.\n " 905 + f"Will try to use '{_repo_keys}' instead.", 906 stack = False, 907 ) 908 else: 909 _repo_keys = default_repo_keys 910 911 if _repo_keys in skipped_repo_keys: 912 continue 913 914 if not is_valid_connector_keys(_repo_keys): 915 warn( 916 f"Invalid connector '{_repo_keys}'.\n" 917 f" Skipping required plugins from repository '{_repo_keys}'", 918 stack=False, 919 ) 920 continue 921 922 plugins.append(Plugin(_plugin_name, repo=_repo_keys)) 923 924 return plugins
Return a list of required Plugin objects.
927 def get_required_packages(self, debug: bool=False) -> List[str]: 928 """ 929 Return the required package names (excluding plugins). 930 """ 931 _deps = self.get_dependencies(debug=debug) 932 return [_d for _d in _deps if not _d.startswith('plugin:')]
Return the required package names (excluding plugins).
935 def activate_venv( 936 self, 937 dependencies: bool = True, 938 init_if_not_exists: bool = True, 939 debug: bool = False, 940 **kw 941 ) -> bool: 942 """ 943 Activate the virtual environments for the plugin and its dependencies. 944 945 Parameters 946 ---------- 947 dependencies: bool, default True 948 If `True`, activate the virtual environments for required plugins. 949 950 Returns 951 ------- 952 A bool indicating success. 953 """ 954 import meerschaum.config.paths as paths 955 from meerschaum.utils.venv import venv_target_path 956 from meerschaum.utils.packages import activate_venv 957 from meerschaum.utils.misc import make_symlink, is_symlink 958 959 if dependencies: 960 for plugin in self.get_required_plugins(debug=debug): 961 plugin.activate_venv(debug=debug, init_if_not_exists=init_if_not_exists, **kw) 962 963 vtp = venv_target_path(self.name, debug=debug, allow_nonexistent=True) 964 venv_meerschaum_path = vtp / 'meerschaum' 965 966 try: 967 success, msg = True, "Success" 968 if is_symlink(venv_meerschaum_path): 969 if pathlib.Path(os.path.realpath(venv_meerschaum_path)) != paths.PACKAGE_ROOT_PATH: 970 venv_meerschaum_path.unlink() 971 success, msg = make_symlink(venv_meerschaum_path, paths.PACKAGE_ROOT_PATH) 972 except Exception as e: 973 success, msg = False, str(e) 974 if not success: 975 warn( 976 f"Unable to create symlink {venv_meerschaum_path} to {paths.PACKAGE_ROOT_PATH}:\n" 977 f"{msg}" 978 ) 979 980 return activate_venv(self.name, init_if_not_exists=init_if_not_exists, debug=debug, **kw)
Activate the virtual environments for the plugin and its dependencies.
Parameters
- dependencies (bool, default True):
If
True, activate the virtual environments for required plugins.
Returns
- A bool indicating success.
983 def deactivate_venv(self, dependencies: bool=True, debug: bool = False, **kw) -> bool: 984 """ 985 Deactivate the virtual environments for the plugin and its dependencies. 986 987 Parameters 988 ---------- 989 dependencies: bool, default True 990 If `True`, deactivate the virtual environments for required plugins. 991 992 Returns 993 ------- 994 A bool indicating success. 995 """ 996 from meerschaum.utils.packages import deactivate_venv 997 success = deactivate_venv(self.name, debug=debug, **kw) 998 if dependencies: 999 for plugin in self.get_required_plugins(debug=debug): 1000 plugin.deactivate_venv(debug=debug, **kw) 1001 return success
Deactivate the virtual environments for the plugin and its dependencies.
Parameters
- dependencies (bool, default True):
If
True, deactivate the virtual environments for required plugins.
Returns
- A bool indicating success.
1004 def install_dependencies( 1005 self, 1006 force: bool = False, 1007 debug: bool = False, 1008 ) -> bool: 1009 """ 1010 If specified, install dependencies. 1011 1012 **NOTE:** Dependencies that start with `'plugin:'` will be installed as 1013 Meerschaum plugins from the same repository as this Plugin. 1014 To install from a different repository, add the repo keys after `'@'` 1015 (e.g. `'plugin:foo@api:bar'`). 1016 1017 Parameters 1018 ---------- 1019 force: bool, default False 1020 If `True`, continue with the installation, even if some 1021 required packages fail to install. 1022 1023 debug: bool, default False 1024 Verbosity toggle. 1025 1026 Returns 1027 ------- 1028 A bool indicating success. 1029 """ 1030 from meerschaum.utils.packages import pip_install, venv_contains_package 1031 from meerschaum.utils.warnings import warn, info 1032 _deps = self.get_dependencies(debug=debug) 1033 if not _deps and self.requirements_file_path is None: 1034 return True 1035 1036 plugins = self.get_required_plugins(debug=debug) 1037 for _plugin in plugins: 1038 if _plugin.name == self.name: 1039 warn(f"Plugin '{self.name}' cannot depend on itself! Skipping...", stack=False) 1040 continue 1041 _success, _msg = _plugin.repo_connector.install_plugin( 1042 _plugin.name, debug=debug, force=force 1043 ) 1044 if not _success: 1045 warn( 1046 f"Failed to install required plugin '{_plugin}' from '{_plugin.repo_connector}'" 1047 + f" for plugin '{self.name}':\n" + _msg, 1048 stack = False, 1049 ) 1050 if not force: 1051 warn( 1052 "Try installing with the `--force` flag to continue anyway.", 1053 stack = False, 1054 ) 1055 return False 1056 info( 1057 "Continuing with installation despite the failure " 1058 + "(careful, things might be broken!)...", 1059 icon = False 1060 ) 1061 1062 1063 ### First step: parse `requirements.txt` if it exists. 1064 if self.requirements_file_path is not None: 1065 if not pip_install( 1066 requirements_file_path=self.requirements_file_path, 1067 venv=self.name, debug=debug 1068 ): 1069 warn( 1070 f"Failed to resolve 'requirements.txt' for plugin '{self.name}'.", 1071 stack = False, 1072 ) 1073 if not force: 1074 warn( 1075 "Try installing with `--force` to continue anyway.", 1076 stack = False, 1077 ) 1078 return False 1079 info( 1080 "Continuing with installation despite the failure " 1081 + "(careful, things might be broken!)...", 1082 icon = False 1083 ) 1084 1085 1086 ### Don't reinstall packages that are already included in required plugins. 1087 packages = [] 1088 _packages = self.get_required_packages(debug=debug) 1089 accounted_for_packages = set() 1090 for package_name in _packages: 1091 for plugin in plugins: 1092 if venv_contains_package(package_name, plugin.name): 1093 accounted_for_packages.add(package_name) 1094 break 1095 packages = [pkg for pkg in _packages if pkg not in accounted_for_packages] 1096 1097 ### Attempt pip packages installation. 1098 if packages: 1099 for package in packages: 1100 if not pip_install(package, venv=self.name, debug=debug): 1101 warn( 1102 f"Failed to install required package '{package}'" 1103 + f" for plugin '{self.name}'.", 1104 stack = False, 1105 ) 1106 if not force: 1107 warn( 1108 "Try installing with `--force` to continue anyway.", 1109 stack = False, 1110 ) 1111 return False 1112 info( 1113 "Continuing with installation despite the failure " 1114 + "(careful, things might be broken!)...", 1115 icon = False 1116 ) 1117 return True
If specified, install dependencies.
NOTE: Dependencies that start with 'plugin:' will be installed as
Meerschaum plugins from the same repository as this Plugin.
To install from a different repository, add the repo keys after '@'
(e.g. 'plugin:foo@api:bar').
Parameters
- force (bool, default False):
If
True, continue with the installation, even if some required packages fail to install. - debug (bool, default False): Verbosity toggle.
Returns
- A bool indicating success.
1120 @property 1121 def full_name(self) -> str: 1122 """ 1123 Include the repo keys with the plugin's name. 1124 """ 1125 from meerschaum._internal.static import STATIC_CONFIG 1126 sep = STATIC_CONFIG['plugins']['repo_separator'] 1127 return self.name + sep + str(self.repo_connector)
Include the repo keys with the plugin's name.
19class Venv: 20 """ 21 Manage a virtual enviroment's activation status. 22 23 Examples 24 -------- 25 >>> from meerschaum.plugins import Plugin 26 >>> with Venv('mrsm') as venv: 27 ... import pandas 28 >>> with Venv(Plugin('noaa')) as venv: 29 ... import requests 30 >>> venv = Venv('mrsm') 31 >>> venv.activate() 32 True 33 >>> venv.deactivate() 34 True 35 >>> 36 """ 37 38 def __init__( 39 self, 40 venv: Union[str, 'mrsm.core.Plugin', None] = 'mrsm', 41 init_if_not_exists: bool = True, 42 debug: bool = False, 43 ) -> None: 44 from meerschaum.utils.venv import activate_venv, deactivate_venv, active_venvs 45 ### For some weird threading issue, 46 ### we can't use `isinstance` here. 47 if '_Plugin' in str(type(venv)): 48 self._venv = venv.name 49 self._activate = venv.activate_venv 50 self._deactivate = venv.deactivate_venv 51 self._kwargs = {} 52 else: 53 self._venv = venv 54 self._activate = activate_venv 55 self._deactivate = deactivate_venv 56 self._kwargs = {'venv': venv} 57 self._debug = debug 58 self._init_if_not_exists = init_if_not_exists 59 ### In case someone calls `deactivate()` before `activate()`. 60 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs) 61 62 63 def activate(self, debug: bool = False) -> bool: 64 """ 65 Activate this virtual environment. 66 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 67 will also be activated. 68 """ 69 from meerschaum.utils.venv import active_venvs, init_venv 70 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs) 71 try: 72 return self._activate( 73 debug=(debug or self._debug), 74 init_if_not_exists=self._init_if_not_exists, 75 **self._kwargs 76 ) 77 except OSError as e: 78 ### None is the current interpreter, not a named environment to recreate. 79 if self._venv is None: 80 raise 81 if self._init_if_not_exists: 82 if not init_venv(self._venv, force=True): 83 raise e 84 return self._activate( 85 debug=(debug or self._debug), 86 init_if_not_exists=self._init_if_not_exists, 87 **self._kwargs 88 ) 89 90 91 def deactivate(self, debug: bool = False) -> bool: 92 """ 93 Deactivate this virtual environment. 94 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 95 will also be deactivated. 96 """ 97 return self._deactivate(debug=(debug or self._debug), **self._kwargs) 98 99 100 @property 101 def target_path(self) -> pathlib.Path: 102 """ 103 Return the target site-packages path for this virtual environment. 104 A `meerschaum.utils.venv.Venv` may have one virtual environment per minor Python version 105 (e.g. Python 3.10 and Python 3.7). 106 """ 107 from meerschaum.utils.venv import venv_target_path 108 return venv_target_path(venv=self._venv, allow_nonexistent=True, debug=self._debug) 109 110 111 @property 112 def root_path(self) -> pathlib.Path: 113 """ 114 Return the top-level path for this virtual environment. 115 """ 116 import meerschaum.config.paths as paths 117 if self._venv is None: 118 return self.target_path.parent 119 return paths.VIRTENV_RESOURCES_PATH / self._venv 120 121 122 def __enter__(self) -> None: 123 self.activate(debug=self._debug) 124 125 126 def __exit__(self, exc_type, exc_value, exc_traceback) -> None: 127 self.deactivate(debug=self._debug) 128 129 130 def __str__(self) -> str: 131 quote = "'" if self._venv is not None else "" 132 return "Venv(" + quote + str(self._venv) + quote + ")" 133 134 135 def __repr__(self) -> str: 136 return self.__str__()
Manage a virtual enviroment's activation status.
Examples
>>> from meerschaum.plugins import Plugin
>>> with Venv('mrsm') as venv:
... import pandas
>>> with Venv(Plugin('noaa')) as venv:
... import requests
>>> venv = Venv('mrsm')
>>> venv.activate()
True
>>> venv.deactivate()
True
>>>
38 def __init__( 39 self, 40 venv: Union[str, 'mrsm.core.Plugin', None] = 'mrsm', 41 init_if_not_exists: bool = True, 42 debug: bool = False, 43 ) -> None: 44 from meerschaum.utils.venv import activate_venv, deactivate_venv, active_venvs 45 ### For some weird threading issue, 46 ### we can't use `isinstance` here. 47 if '_Plugin' in str(type(venv)): 48 self._venv = venv.name 49 self._activate = venv.activate_venv 50 self._deactivate = venv.deactivate_venv 51 self._kwargs = {} 52 else: 53 self._venv = venv 54 self._activate = activate_venv 55 self._deactivate = deactivate_venv 56 self._kwargs = {'venv': venv} 57 self._debug = debug 58 self._init_if_not_exists = init_if_not_exists 59 ### In case someone calls `deactivate()` before `activate()`. 60 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs)
63 def activate(self, debug: bool = False) -> bool: 64 """ 65 Activate this virtual environment. 66 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 67 will also be activated. 68 """ 69 from meerschaum.utils.venv import active_venvs, init_venv 70 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs) 71 try: 72 return self._activate( 73 debug=(debug or self._debug), 74 init_if_not_exists=self._init_if_not_exists, 75 **self._kwargs 76 ) 77 except OSError as e: 78 ### None is the current interpreter, not a named environment to recreate. 79 if self._venv is None: 80 raise 81 if self._init_if_not_exists: 82 if not init_venv(self._venv, force=True): 83 raise e 84 return self._activate( 85 debug=(debug or self._debug), 86 init_if_not_exists=self._init_if_not_exists, 87 **self._kwargs 88 )
Activate this virtual environment.
If a meerschaum.plugins.Plugin was provided, its dependent virtual environments
will also be activated.
91 def deactivate(self, debug: bool = False) -> bool: 92 """ 93 Deactivate this virtual environment. 94 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 95 will also be deactivated. 96 """ 97 return self._deactivate(debug=(debug or self._debug), **self._kwargs)
Deactivate this virtual environment.
If a meerschaum.plugins.Plugin was provided, its dependent virtual environments
will also be deactivated.
100 @property 101 def target_path(self) -> pathlib.Path: 102 """ 103 Return the target site-packages path for this virtual environment. 104 A `meerschaum.utils.venv.Venv` may have one virtual environment per minor Python version 105 (e.g. Python 3.10 and Python 3.7). 106 """ 107 from meerschaum.utils.venv import venv_target_path 108 return venv_target_path(venv=self._venv, allow_nonexistent=True, debug=self._debug)
Return the target site-packages path for this virtual environment.
A meerschaum.utils.venv.Venv may have one virtual environment per minor Python version
(e.g. Python 3.10 and Python 3.7).
111 @property 112 def root_path(self) -> pathlib.Path: 113 """ 114 Return the top-level path for this virtual environment. 115 """ 116 import meerschaum.config.paths as paths 117 if self._venv is None: 118 return self.target_path.parent 119 return paths.VIRTENV_RESOURCES_PATH / self._venv
Return the top-level path for this virtual environment.
90class Job: 91 """ 92 Manage a job, locally, with `systemd`, or remotely via the API. 93 """ 94 95 def __init__( 96 self, 97 name: str, 98 sysargs: Union[List[str], str, None] = None, 99 env: Optional[Dict[str, str]] = None, 100 executor_keys: Optional[str] = None, 101 delete_after_completion: bool = False, 102 refresh_seconds: Union[int, float, None] = None, 103 _properties: Optional[Dict[str, Any]] = None, 104 _status_hook: Optional[Callable[[], str]] = None, 105 _result_hook: Optional[Callable[[], SuccessTuple]] = None, 106 _externally_managed: bool = False, 107 ): 108 """ 109 Create a new job. 110 111 Parameters 112 ---------- 113 name: str 114 The name of the job to be created. 115 This will also be used as the Daemon ID. 116 117 sysargs: Union[List[str], str, None], default None 118 The sysargs of the command to be executed, e.g. 'start api'. 119 120 env: Optional[Dict[str, str]], default None 121 If provided, set these environment variables in the job's process. 122 123 executor_keys: Optional[str], default None 124 If provided, execute the job remotely on an API instance, e.g. 'api:main' 125 (or with `'systemd'`). Defaults to `'local'`, or the context's executor. 126 127 delete_after_completion: bool, default False 128 If `True`, delete this job when it has finished executing. 129 130 refresh_seconds: Union[int, float, None], default None 131 The number of seconds to sleep between refreshes. 132 Defaults to the configured value `system.cli.refresh_seconds`. 133 134 _properties: Optional[Dict[str, Any]], default None 135 If provided, use this to patch the daemon's properties. 136 """ 137 native = _native.core() 138 self._core = native.Job( 139 name, 140 sysargs, 141 env=env, 142 executor_keys=executor_keys, 143 delete_after_completion=delete_after_completion, 144 refresh_seconds=refresh_seconds, 145 properties=_properties, 146 externally_managed=_externally_managed, 147 api_attributes=_native.api_attributes(executor_keys), 148 ) 149 _native.emit_warnings(self._core.take_warnings()) 150 self.name = name 151 self.executor_keys = self._core.executor_keys 152 self.refresh_seconds = ( 153 refresh_seconds 154 if refresh_seconds is not None 155 else mrsm.get_config('system', 'cli', 'refresh_seconds') 156 ) 157 if _status_hook is not None: 158 self._status_hook = _status_hook 159 160 if _result_hook is not None: 161 self._result_hook = _result_hook 162 163 self._externally_managed = _externally_managed 164 165 def _warn(self) -> None: 166 """Show the warnings the last native operation produced.""" 167 _native.emit_warnings(self._core.take_warnings()) 168 169 @staticmethod 170 def from_pid(pid: int, executor_keys: Optional[str] = None) -> Job: 171 """ 172 Build a `Job` from the PID of a running Meerschaum process. 173 174 Parameters 175 ---------- 176 pid: int 177 The PID of the process. 178 179 executor_keys: Optional[str], default None 180 The executor keys to assign to the job. 181 """ 182 import os 183 try: 184 os.kill(pid, 0) 185 except ProcessLookupError as e: 186 warn(f"Process with PID {pid} does not exist.", stack=False) 187 raise e 188 except PermissionError: 189 pass 190 191 native = _native.core() 192 daemon_ids = native.jobs_daemon_ids() 193 for daemon_id in daemon_ids: 194 job = Job(daemon_id, executor_keys=executor_keys) 195 if job.pid == pid: 196 return job 197 198 cmdline_path = pathlib.Path('/proc') / str(pid) / 'cmdline' 199 if not cmdline_path.exists(): 200 raise EnvironmentError(f"Is process {pid} a Meerschaum job?") 201 202 command_args = [ 203 arg 204 for arg in cmdline_path.read_bytes().decode('utf-8', errors='replace').split('\0') 205 if arg 206 ] 207 # During startup the PID may change from the supervisor to its worker between 208 # the caller's PID read and the registry lookup above. Scope its persistent marker 209 # to this registry: another root may have a different Job with the same name. 210 if '--jobs-dir' in command_args: 211 import meerschaum.config.paths as paths 212 index = command_args.index('--jobs-dir') + 1 213 if ( 214 index >= len(command_args) 215 or pathlib.Path(command_args[index]).resolve() != paths.DAEMON_RESOURCES_PATH.resolve() 216 ): 217 raise EnvironmentError(f"Process {pid} belongs to another Meerschaum jobs directory.") 218 for arg in command_args: 219 if arg.startswith("daemon_id='") and arg.endswith("'"): 220 daemon_id = arg[len("daemon_id='"):-1] 221 if daemon_id in daemon_ids: 222 return Job(daemon_id, executor_keys=executor_keys) 223 if '__mrsm_job_supervisor__' in command_args or '__mrsm_run_daemon__' in command_args: 224 if '--daemon-id' in command_args: 225 index = command_args.index('--daemon-id') + 1 226 if index < len(command_args) and command_args[index] in daemon_ids: 227 return Job(command_args[index], executor_keys=executor_keys) 228 229 if len(command_args) > 1 and command_args[1] == '-c': 230 raise EnvironmentError(f"Is process {pid} a running job?") 231 232 mrsm_ix = 0 233 for i, arg in enumerate(command_args): 234 if 'mrsm' in arg or 'meerschaum' in arg.lower(): 235 mrsm_ix = i 236 break 237 238 sysargs = command_args[mrsm_ix + 1:] 239 name = None 240 for flag in ('--name', '--job-name'): 241 if flag in sysargs and sysargs.index(flag) + 1 < len(sysargs): 242 name = sysargs[sysargs.index(flag) + 1] 243 break 244 if name is None: 245 name = native.jobs_new_daemon_name() 246 return Job(name, sysargs, executor_keys=executor_keys) 247 248 def start(self, debug: bool = False) -> SuccessTuple: 249 """ 250 Start the job's daemon. 251 """ 252 result = self._core.start() 253 self._warn() 254 return result 255 256 def stop( 257 self, 258 timeout_seconds: Union[int, float, None] = None, 259 debug: bool = False, 260 ) -> SuccessTuple: 261 """ 262 Stop the job's daemon. 263 """ 264 result = self._core.stop(timeout_seconds) 265 self._warn() 266 return result 267 268 def pause( 269 self, 270 timeout_seconds: Union[int, float, None] = None, 271 debug: bool = False, 272 ) -> SuccessTuple: 273 """ 274 Pause the job's daemon. 275 """ 276 result = self._core.pause(timeout_seconds) 277 self._warn() 278 return result 279 280 def resume( 281 self, 282 timeout_seconds: Union[int, float, None] = None, 283 debug: bool = False, 284 ) -> SuccessTuple: 285 """ 286 Resume a paused job. 287 """ 288 result = self._core.resume(timeout_seconds) 289 self._warn() 290 return result 291 292 def delete(self, debug: bool = False) -> SuccessTuple: 293 """ 294 Delete the job and its daemon. 295 """ 296 result = self._core.delete() 297 self._warn() 298 return result 299 300 def is_running(self) -> bool: 301 """ 302 Determine whether the job's daemon is running. 303 """ 304 return self.status == 'running' 305 306 def exists(self, debug: bool = False) -> bool: 307 """ 308 Determine whether the job exists. 309 """ 310 return self._core.exists() 311 312 def get_logs(self) -> Union[str, None]: 313 """ 314 Return the output text of the job's daemon. 315 """ 316 return self._core.get_logs() 317 318 def monitor_logs( 319 self, 320 callback_function: Callable[[str], None] = _default_stdout_callback, 321 input_callback_function: Optional[Callable[[], str]] = None, 322 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 323 stop_event: Optional[asyncio.Event] = None, 324 stop_on_exit: bool = False, 325 strip_timestamps: bool = False, 326 accept_input: bool = True, 327 debug: bool = False, 328 _wait_if_stopped: bool = True, 329 ): 330 """ 331 Monitor the job's log files and execute a callback on new lines. 332 333 Parameters 334 ---------- 335 callback_function: Callable[[str], None], default partial(print, end='') 336 The callback to execute as new data comes in. 337 Defaults to printing the output directly to `stdout`. 338 339 input_callback_function: Optional[Callable[[], str]], default None 340 If provided, execute this callback when the daemon is blocking on stdin. 341 Defaults to `sys.stdin.readline()`. 342 343 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 344 If provided, execute this callback when the daemon stops. 345 The job's SuccessTuple will be passed to the callback. 346 347 stop_event: Optional[asyncio.Event], default None 348 If provided, stop monitoring when this event is set. 349 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 350 from within `callback_function` to stop monitoring. 351 352 stop_on_exit: bool, default False 353 If `True`, stop monitoring when the job stops. 354 355 strip_timestamps: bool, default False 356 If `True`, remove leading timestamps from lines. 357 358 accept_input: bool, default True 359 If `True`, accept input when the daemon blocks on stdin. 360 """ 361 return self._monitor( 362 callback_function, 363 input_callback_function, 364 stop_callback_function, 365 stop_event, 366 stop_on_exit, 367 strip_timestamps, 368 accept_input, 369 _wait_if_stopped, 370 loop=None, 371 stop_flag=None, 372 ) 373 374 async def monitor_logs_async( 375 self, 376 callback_function: Callable[[str], None] = _default_stdout_callback, 377 input_callback_function: Optional[Callable[[], str]] = None, 378 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 379 stop_event: Optional[asyncio.Event] = None, 380 stop_on_exit: bool = False, 381 strip_timestamps: bool = False, 382 accept_input: bool = True, 383 debug: bool = False, 384 _wait_if_stopped: bool = True, 385 ): 386 """ 387 Monitor the job's log files and await a callback on new lines. 388 389 Parameters 390 ---------- 391 callback_function: Callable[[str], None], default _default_stdout_callback 392 The callback to execute as new data comes in. 393 Defaults to printing the output directly to `stdout`. 394 395 input_callback_function: Optional[Callable[[], str]], default None 396 If provided, execute this callback when the daemon is blocking on stdin. 397 Defaults to `sys.stdin.readline()`. 398 399 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 400 If provided, execute this callback when the daemon stops. 401 The job's SuccessTuple will be passed to the callback. 402 403 stop_event: Optional[asyncio.Event], default None 404 If provided, stop monitoring when this event is set. 405 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 406 from within `callback_function` to stop monitoring. 407 408 stop_on_exit: bool, default False 409 If `True`, stop monitoring when the job stops. 410 411 strip_timestamps: bool, default False 412 If `True`, remove leading timestamps from lines. 413 414 accept_input: bool, default True 415 If `True`, accept input when the daemon blocks on stdin. 416 """ 417 loop = asyncio.get_running_loop() 418 stop_flag = _native.core().StopFlag() 419 run = functools.partial( 420 self._monitor, 421 callback_function, 422 input_callback_function, 423 stop_callback_function, 424 stop_event, 425 stop_on_exit, 426 strip_timestamps, 427 accept_input, 428 _wait_if_stopped, 429 loop=loop, 430 stop_flag=stop_flag, 431 ) 432 try: 433 await loop.run_in_executor(None, run) 434 except asyncio.CancelledError: 435 stop_flag.set() 436 raise 437 finally: 438 stop_flag.set() 439 440 def _monitor( 441 self, 442 callback_function, 443 input_callback_function, 444 stop_callback_function, 445 stop_event, 446 stop_on_exit, 447 strip_timestamps, 448 accept_input, 449 wait_if_stopped, 450 loop, 451 stop_flag, 452 ): 453 """ 454 Follow the logs with the native monitor, running the callbacks on this thread's helper 455 (coroutine functions are awaited on `loop`, or on a private loop when there is none). 456 """ 457 def sync(function): 458 if function is None or not inspect.iscoroutinefunction(function): 459 return function 460 461 def call(*args): 462 coroutine = function(*args) 463 if loop is not None: 464 return asyncio.run_coroutine_threadsafe(coroutine, loop).result() 465 return asyncio.run(coroutine) 466 467 return call 468 469 def default_input_callback_function(): 470 from meerschaum.utils.prompt import prompt 471 prompt_kwargs = self.get_prompt_kwargs() 472 if prompt_kwargs: 473 answer = prompt(**prompt_kwargs) 474 return answer + '\n' 475 return sys.stdin.readline() 476 477 input_callback = sync(input_callback_function) or default_input_callback_function 478 self._core.monitor_logs( 479 sync(callback_function), 480 input_callback=input_callback if accept_input else None, 481 stop_callback=sync(stop_callback_function), 482 stop_event=stop_event, 483 stop_flag=stop_flag, 484 stop_on_exit=stop_on_exit, 485 strip_timestamps=strip_timestamps, 486 accept_input=accept_input, 487 wait_if_stopped=wait_if_stopped, 488 stop_exception=StopMonitoringLogs, 489 clear_callback=_clear_screen, 490 warn=lambda message: warn(message), 491 ) 492 493 def is_blocking_on_stdin(self, debug: bool = False) -> bool: 494 """ 495 Return whether a job's daemon is blocking on stdin. 496 """ 497 return self._core.is_blocking_on_stdin() 498 499 def get_prompt_kwargs(self, debug: bool = False) -> Dict[str, Any]: 500 """ 501 Return the kwargs to the blocking `prompt()`, if available. 502 """ 503 return self._core.get_prompt_kwargs() 504 505 def write_stdin(self, data): 506 """ 507 Write to a job's daemon's `stdin`. 508 """ 509 self._core.write_stdin(data) 510 511 @property 512 def executor(self) -> Union[Executor, None]: 513 """ 514 If the job is not local, return the connector to its executor 515 (the remote API instance, `systemd`, a plugin's executor). 516 """ 517 if self.executor_keys == 'local': 518 return None 519 from meerschaum.jobs._native import is_python_executor_type 520 if is_python_executor_type(self.executor_keys.split(':', maxsplit=1)[0]): 521 return mrsm.get_connector(self.executor_keys) 522 ### An executor type of a native (Rust) plugin: no Python connector exists. 523 from meerschaum.jobs._NativeExecutor import NativeExecutor 524 return NativeExecutor(self.executor_keys) 525 526 @property 527 def status(self) -> str: 528 """ 529 Return the running status of the job's daemon. 530 """ 531 if '_status_hook' in self.__dict__: 532 return self._status_hook() 533 534 return self._core.status 535 536 @property 537 def pid(self) -> Union[int, None]: 538 """ 539 Return the PID of the job's dameon. 540 """ 541 return self._core.pid 542 543 @property 544 def restart(self) -> bool: 545 """ 546 Return whether to restart a stopped job. 547 """ 548 return self._core.restarts 549 550 @property 551 def result(self) -> SuccessTuple: 552 """ 553 Return the `SuccessTuple` when the job has terminated. 554 """ 555 if '_result_hook' in self.__dict__: 556 if self.is_running(): 557 return True, f"{self} is running." 558 return self._result_hook() 559 560 return self._core.result 561 562 @property 563 def sysargs(self) -> List[str]: 564 """ 565 Return the sysargs to use for the Daemon. 566 """ 567 return self._core.sysargs 568 569 @property 570 def health(self) -> Dict[str, Any]: 571 """ 572 Return how the job's laps go (a job that repeats its work: `--loop`, `-s`, `--listen`). 573 574 Returns 575 ------- 576 A dictionary with the keys: 577 578 - `state`: `'healthy'` (the last lap succeeded), `'failing'` (the last lap failed), 579 `'stalled'` (no lap for three times the expected interval, and at least a minute; 580 an hour without a sync for `--listen`; at least an hour for a first lap), 581 `'stopped'` (not running, or paused) or `'unknown'` (nothing recorded yet, e.g. 582 `'first sync in progress'`). 583 - `last_success`, `last_lap`: ISO-8601 UTC timestamps (or `None`). 584 - `consecutive_failures`: laps that failed in a row. 585 - `expected_interval_seconds`: how often laps are expected (`--min-seconds`, the 586 schedule's period, an hour for `--listen`), or `None`. 587 - `next_run`: while a running job waits for its next lap (`--min-seconds`, a cooldown 588 after a failure, a schedule's next fire), when that lap is due (ISO-8601 UTC); 589 otherwise `None`. 590 - `message`: what to show next to the state, e.g. `'last good sync 2 min ago'`. 591 592 Examples 593 -------- 594 >>> job = Job('weather', 'sync pipes -c plugin:noaa --loop') 595 >>> success, msg = job.start() 596 >>> job.health['state'] # Before its first lap ends. 597 'unknown' 598 """ 599 return self._core.health() 600 601 @property 602 def owner(self) -> Union[str, None]: 603 """ 604 Return the user who started this job through the API server or the web console 605 (`None` for a job created locally: the CLI, Compose, systemd or Python). A job started 606 by another job has that job's owner. For an `api:` job, the server's record. 607 608 Examples 609 -------- 610 >>> Job('nightly', executor_keys='api:main').owner 611 'alice' 612 """ 613 return self._core.access().get('owner', None) 614 615 @property 616 def team(self) -> Union[str, None]: 617 """ 618 Return the team this job belongs to: the primary team of the user who started it through 619 the API server or the web console (a job started by another job: that job's team). 620 `None` means Everyone (every local job). 621 622 Members of the team (and administrators) see the job and, with the job permissions, 623 control it. Renaming the team renames it here; deleting the team moves the job to 624 Everyone. 625 """ 626 return self._core.access().get('team', None) 627 628 def get_daemon_properties(self) -> Dict[str, Any]: 629 """ 630 Return the `properties` dictionary for the job's daemon. 631 """ 632 return self._core.get_daemon_properties() 633 634 @property 635 def daemon(self) -> 'Daemon': 636 """ 637 Return the daemon which this job manages. 638 """ 639 from meerschaum.utils.daemon import Daemon 640 if self.executor_keys == 'local' and self.exists(): 641 return Daemon(daemon_id=self.name) 642 643 return Daemon( 644 daemon_id=self.name, 645 sysargs=self.sysargs, 646 label=shlex.join(self.sysargs), 647 properties=self.get_daemon_properties(), 648 ) 649 650 @property 651 def began(self) -> Union[datetime, None]: 652 """ 653 The datetime when the job began running. 654 """ 655 return _parse_dt(self._core.began) 656 657 @property 658 def ended(self) -> Union[datetime, None]: 659 """ 660 The datetime when the job stopped running. 661 """ 662 return _parse_dt(self._core.ended) 663 664 @property 665 def paused(self) -> Union[datetime, None]: 666 """ 667 The datetime when the job was suspended while running. 668 """ 669 return _parse_dt(self._core.paused) 670 671 @property 672 def stop_time(self) -> Union[datetime, None]: 673 """ 674 Return the timestamp when the job was manually stopped. 675 """ 676 return _parse_dt(self._core.stop_time) 677 678 @property 679 def hidden(self) -> bool: 680 """ 681 Return a bool indicating whether this job should be displayed. 682 """ 683 return self._core.hidden 684 685 def check_restart(self) -> SuccessTuple: 686 """ 687 If `restart` is `True` and the daemon is not running, 688 restart the job. 689 Do not restart if the job was manually stopped. 690 """ 691 result = self._core.check_restart() 692 self._warn() 693 return result 694 695 @property 696 def label(self) -> str: 697 """ 698 Return the job's Daemon label (joined sysargs). 699 """ 700 return self._core.label 701 702 @property 703 def _externally_managed_file(self) -> pathlib.Path: 704 """ 705 Return the path to the externally managed file. 706 """ 707 return pathlib.Path(self._core.paths()['externally_managed']) 708 709 def _set_externally_managed(self): 710 """ 711 Set this job as externally managed. 712 """ 713 self._externally_managed = True 714 self._core.set_externally_managed() 715 716 @property 717 def _is_externally_managed(self) -> bool: 718 """ 719 Return whether this job is externally managed. 720 """ 721 return self._core.is_externally_managed 722 723 @property 724 def env(self) -> Dict[str, str]: 725 """ 726 Return the environment variables to set for the job's process. 727 """ 728 return self._core.env 729 730 @property 731 def delete_after_completion(self) -> bool: 732 """ 733 Return whether this job is configured to delete itself after completion. 734 """ 735 return self._core.delete_after_completion 736 737 def __str__(self) -> str: 738 return self._core.describe() 739 740 def __repr__(self) -> str: 741 return str(self) 742 743 def __hash__(self) -> int: 744 return hash(self.name)
Manage a job, locally, with systemd, or remotely via the API.
95 def __init__( 96 self, 97 name: str, 98 sysargs: Union[List[str], str, None] = None, 99 env: Optional[Dict[str, str]] = None, 100 executor_keys: Optional[str] = None, 101 delete_after_completion: bool = False, 102 refresh_seconds: Union[int, float, None] = None, 103 _properties: Optional[Dict[str, Any]] = None, 104 _status_hook: Optional[Callable[[], str]] = None, 105 _result_hook: Optional[Callable[[], SuccessTuple]] = None, 106 _externally_managed: bool = False, 107 ): 108 """ 109 Create a new job. 110 111 Parameters 112 ---------- 113 name: str 114 The name of the job to be created. 115 This will also be used as the Daemon ID. 116 117 sysargs: Union[List[str], str, None], default None 118 The sysargs of the command to be executed, e.g. 'start api'. 119 120 env: Optional[Dict[str, str]], default None 121 If provided, set these environment variables in the job's process. 122 123 executor_keys: Optional[str], default None 124 If provided, execute the job remotely on an API instance, e.g. 'api:main' 125 (or with `'systemd'`). Defaults to `'local'`, or the context's executor. 126 127 delete_after_completion: bool, default False 128 If `True`, delete this job when it has finished executing. 129 130 refresh_seconds: Union[int, float, None], default None 131 The number of seconds to sleep between refreshes. 132 Defaults to the configured value `system.cli.refresh_seconds`. 133 134 _properties: Optional[Dict[str, Any]], default None 135 If provided, use this to patch the daemon's properties. 136 """ 137 native = _native.core() 138 self._core = native.Job( 139 name, 140 sysargs, 141 env=env, 142 executor_keys=executor_keys, 143 delete_after_completion=delete_after_completion, 144 refresh_seconds=refresh_seconds, 145 properties=_properties, 146 externally_managed=_externally_managed, 147 api_attributes=_native.api_attributes(executor_keys), 148 ) 149 _native.emit_warnings(self._core.take_warnings()) 150 self.name = name 151 self.executor_keys = self._core.executor_keys 152 self.refresh_seconds = ( 153 refresh_seconds 154 if refresh_seconds is not None 155 else mrsm.get_config('system', 'cli', 'refresh_seconds') 156 ) 157 if _status_hook is not None: 158 self._status_hook = _status_hook 159 160 if _result_hook is not None: 161 self._result_hook = _result_hook 162 163 self._externally_managed = _externally_managed
Create a new job.
Parameters
- name (str): The name of the job to be created. This will also be used as the Daemon ID.
- sysargs (Union[List[str], str, None], default None): The sysargs of the command to be executed, e.g. 'start api'.
- env (Optional[Dict[str, str]], default None): If provided, set these environment variables in the job's process.
- executor_keys (Optional[str], default None):
If provided, execute the job remotely on an API instance, e.g. 'api:main'
(or with
'systemd'). Defaults to'local', or the context's executor. - delete_after_completion (bool, default False):
If
True, delete this job when it has finished executing. - refresh_seconds (Union[int, float, None], default None):
The number of seconds to sleep between refreshes.
Defaults to the configured value
system.cli.refresh_seconds. - _properties (Optional[Dict[str, Any]], default None): If provided, use this to patch the daemon's properties.
169 @staticmethod 170 def from_pid(pid: int, executor_keys: Optional[str] = None) -> Job: 171 """ 172 Build a `Job` from the PID of a running Meerschaum process. 173 174 Parameters 175 ---------- 176 pid: int 177 The PID of the process. 178 179 executor_keys: Optional[str], default None 180 The executor keys to assign to the job. 181 """ 182 import os 183 try: 184 os.kill(pid, 0) 185 except ProcessLookupError as e: 186 warn(f"Process with PID {pid} does not exist.", stack=False) 187 raise e 188 except PermissionError: 189 pass 190 191 native = _native.core() 192 daemon_ids = native.jobs_daemon_ids() 193 for daemon_id in daemon_ids: 194 job = Job(daemon_id, executor_keys=executor_keys) 195 if job.pid == pid: 196 return job 197 198 cmdline_path = pathlib.Path('/proc') / str(pid) / 'cmdline' 199 if not cmdline_path.exists(): 200 raise EnvironmentError(f"Is process {pid} a Meerschaum job?") 201 202 command_args = [ 203 arg 204 for arg in cmdline_path.read_bytes().decode('utf-8', errors='replace').split('\0') 205 if arg 206 ] 207 # During startup the PID may change from the supervisor to its worker between 208 # the caller's PID read and the registry lookup above. Scope its persistent marker 209 # to this registry: another root may have a different Job with the same name. 210 if '--jobs-dir' in command_args: 211 import meerschaum.config.paths as paths 212 index = command_args.index('--jobs-dir') + 1 213 if ( 214 index >= len(command_args) 215 or pathlib.Path(command_args[index]).resolve() != paths.DAEMON_RESOURCES_PATH.resolve() 216 ): 217 raise EnvironmentError(f"Process {pid} belongs to another Meerschaum jobs directory.") 218 for arg in command_args: 219 if arg.startswith("daemon_id='") and arg.endswith("'"): 220 daemon_id = arg[len("daemon_id='"):-1] 221 if daemon_id in daemon_ids: 222 return Job(daemon_id, executor_keys=executor_keys) 223 if '__mrsm_job_supervisor__' in command_args or '__mrsm_run_daemon__' in command_args: 224 if '--daemon-id' in command_args: 225 index = command_args.index('--daemon-id') + 1 226 if index < len(command_args) and command_args[index] in daemon_ids: 227 return Job(command_args[index], executor_keys=executor_keys) 228 229 if len(command_args) > 1 and command_args[1] == '-c': 230 raise EnvironmentError(f"Is process {pid} a running job?") 231 232 mrsm_ix = 0 233 for i, arg in enumerate(command_args): 234 if 'mrsm' in arg or 'meerschaum' in arg.lower(): 235 mrsm_ix = i 236 break 237 238 sysargs = command_args[mrsm_ix + 1:] 239 name = None 240 for flag in ('--name', '--job-name'): 241 if flag in sysargs and sysargs.index(flag) + 1 < len(sysargs): 242 name = sysargs[sysargs.index(flag) + 1] 243 break 244 if name is None: 245 name = native.jobs_new_daemon_name() 246 return Job(name, sysargs, executor_keys=executor_keys)
Build a Job from the PID of a running Meerschaum process.
Parameters
- pid (int): The PID of the process.
- executor_keys (Optional[str], default None): The executor keys to assign to the job.
248 def start(self, debug: bool = False) -> SuccessTuple: 249 """ 250 Start the job's daemon. 251 """ 252 result = self._core.start() 253 self._warn() 254 return result
Start the job's daemon.
256 def stop( 257 self, 258 timeout_seconds: Union[int, float, None] = None, 259 debug: bool = False, 260 ) -> SuccessTuple: 261 """ 262 Stop the job's daemon. 263 """ 264 result = self._core.stop(timeout_seconds) 265 self._warn() 266 return result
Stop the job's daemon.
268 def pause( 269 self, 270 timeout_seconds: Union[int, float, None] = None, 271 debug: bool = False, 272 ) -> SuccessTuple: 273 """ 274 Pause the job's daemon. 275 """ 276 result = self._core.pause(timeout_seconds) 277 self._warn() 278 return result
Pause the job's daemon.
280 def resume( 281 self, 282 timeout_seconds: Union[int, float, None] = None, 283 debug: bool = False, 284 ) -> SuccessTuple: 285 """ 286 Resume a paused job. 287 """ 288 result = self._core.resume(timeout_seconds) 289 self._warn() 290 return result
Resume a paused job.
292 def delete(self, debug: bool = False) -> SuccessTuple: 293 """ 294 Delete the job and its daemon. 295 """ 296 result = self._core.delete() 297 self._warn() 298 return result
Delete the job and its daemon.
300 def is_running(self) -> bool: 301 """ 302 Determine whether the job's daemon is running. 303 """ 304 return self.status == 'running'
Determine whether the job's daemon is running.
306 def exists(self, debug: bool = False) -> bool: 307 """ 308 Determine whether the job exists. 309 """ 310 return self._core.exists()
Determine whether the job exists.
312 def get_logs(self) -> Union[str, None]: 313 """ 314 Return the output text of the job's daemon. 315 """ 316 return self._core.get_logs()
Return the output text of the job's daemon.
318 def monitor_logs( 319 self, 320 callback_function: Callable[[str], None] = _default_stdout_callback, 321 input_callback_function: Optional[Callable[[], str]] = None, 322 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 323 stop_event: Optional[asyncio.Event] = None, 324 stop_on_exit: bool = False, 325 strip_timestamps: bool = False, 326 accept_input: bool = True, 327 debug: bool = False, 328 _wait_if_stopped: bool = True, 329 ): 330 """ 331 Monitor the job's log files and execute a callback on new lines. 332 333 Parameters 334 ---------- 335 callback_function: Callable[[str], None], default partial(print, end='') 336 The callback to execute as new data comes in. 337 Defaults to printing the output directly to `stdout`. 338 339 input_callback_function: Optional[Callable[[], str]], default None 340 If provided, execute this callback when the daemon is blocking on stdin. 341 Defaults to `sys.stdin.readline()`. 342 343 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 344 If provided, execute this callback when the daemon stops. 345 The job's SuccessTuple will be passed to the callback. 346 347 stop_event: Optional[asyncio.Event], default None 348 If provided, stop monitoring when this event is set. 349 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 350 from within `callback_function` to stop monitoring. 351 352 stop_on_exit: bool, default False 353 If `True`, stop monitoring when the job stops. 354 355 strip_timestamps: bool, default False 356 If `True`, remove leading timestamps from lines. 357 358 accept_input: bool, default True 359 If `True`, accept input when the daemon blocks on stdin. 360 """ 361 return self._monitor( 362 callback_function, 363 input_callback_function, 364 stop_callback_function, 365 stop_event, 366 stop_on_exit, 367 strip_timestamps, 368 accept_input, 369 _wait_if_stopped, 370 loop=None, 371 stop_flag=None, 372 )
Monitor the job's log files and execute a callback on new lines.
Parameters
- callback_function (Callable[[str], None], default partial(print, end='')):
The callback to execute as new data comes in.
Defaults to printing the output directly to
stdout. - input_callback_function (Optional[Callable[[], str]], default None):
If provided, execute this callback when the daemon is blocking on stdin.
Defaults to
sys.stdin.readline(). - stop_callback_function (Optional[Callable[[SuccessTuple]], str], default None): If provided, execute this callback when the daemon stops. The job's SuccessTuple will be passed to the callback.
- stop_event (Optional[asyncio.Event], default None):
If provided, stop monitoring when this event is set.
You may instead raise
meerschaum.jobs.StopMonitoringLogsfrom withincallback_functionto stop monitoring. - stop_on_exit (bool, default False):
If
True, stop monitoring when the job stops. - strip_timestamps (bool, default False):
If
True, remove leading timestamps from lines. - accept_input (bool, default True):
If
True, accept input when the daemon blocks on stdin.
374 async def monitor_logs_async( 375 self, 376 callback_function: Callable[[str], None] = _default_stdout_callback, 377 input_callback_function: Optional[Callable[[], str]] = None, 378 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 379 stop_event: Optional[asyncio.Event] = None, 380 stop_on_exit: bool = False, 381 strip_timestamps: bool = False, 382 accept_input: bool = True, 383 debug: bool = False, 384 _wait_if_stopped: bool = True, 385 ): 386 """ 387 Monitor the job's log files and await a callback on new lines. 388 389 Parameters 390 ---------- 391 callback_function: Callable[[str], None], default _default_stdout_callback 392 The callback to execute as new data comes in. 393 Defaults to printing the output directly to `stdout`. 394 395 input_callback_function: Optional[Callable[[], str]], default None 396 If provided, execute this callback when the daemon is blocking on stdin. 397 Defaults to `sys.stdin.readline()`. 398 399 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 400 If provided, execute this callback when the daemon stops. 401 The job's SuccessTuple will be passed to the callback. 402 403 stop_event: Optional[asyncio.Event], default None 404 If provided, stop monitoring when this event is set. 405 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 406 from within `callback_function` to stop monitoring. 407 408 stop_on_exit: bool, default False 409 If `True`, stop monitoring when the job stops. 410 411 strip_timestamps: bool, default False 412 If `True`, remove leading timestamps from lines. 413 414 accept_input: bool, default True 415 If `True`, accept input when the daemon blocks on stdin. 416 """ 417 loop = asyncio.get_running_loop() 418 stop_flag = _native.core().StopFlag() 419 run = functools.partial( 420 self._monitor, 421 callback_function, 422 input_callback_function, 423 stop_callback_function, 424 stop_event, 425 stop_on_exit, 426 strip_timestamps, 427 accept_input, 428 _wait_if_stopped, 429 loop=loop, 430 stop_flag=stop_flag, 431 ) 432 try: 433 await loop.run_in_executor(None, run) 434 except asyncio.CancelledError: 435 stop_flag.set() 436 raise 437 finally: 438 stop_flag.set()
Monitor the job's log files and await a callback on new lines.
Parameters
- callback_function (Callable[[str], None], default _default_stdout_callback):
The callback to execute as new data comes in.
Defaults to printing the output directly to
stdout. - input_callback_function (Optional[Callable[[], str]], default None):
If provided, execute this callback when the daemon is blocking on stdin.
Defaults to
sys.stdin.readline(). - stop_callback_function (Optional[Callable[[SuccessTuple]], str], default None): If provided, execute this callback when the daemon stops. The job's SuccessTuple will be passed to the callback.
- stop_event (Optional[asyncio.Event], default None):
If provided, stop monitoring when this event is set.
You may instead raise
meerschaum.jobs.StopMonitoringLogsfrom withincallback_functionto stop monitoring. - stop_on_exit (bool, default False):
If
True, stop monitoring when the job stops. - strip_timestamps (bool, default False):
If
True, remove leading timestamps from lines. - accept_input (bool, default True):
If
True, accept input when the daemon blocks on stdin.
493 def is_blocking_on_stdin(self, debug: bool = False) -> bool: 494 """ 495 Return whether a job's daemon is blocking on stdin. 496 """ 497 return self._core.is_blocking_on_stdin()
Return whether a job's daemon is blocking on stdin.
499 def get_prompt_kwargs(self, debug: bool = False) -> Dict[str, Any]: 500 """ 501 Return the kwargs to the blocking `prompt()`, if available. 502 """ 503 return self._core.get_prompt_kwargs()
Return the kwargs to the blocking prompt(), if available.
505 def write_stdin(self, data): 506 """ 507 Write to a job's daemon's `stdin`. 508 """ 509 self._core.write_stdin(data)
Write to a job's daemon's stdin.
511 @property 512 def executor(self) -> Union[Executor, None]: 513 """ 514 If the job is not local, return the connector to its executor 515 (the remote API instance, `systemd`, a plugin's executor). 516 """ 517 if self.executor_keys == 'local': 518 return None 519 from meerschaum.jobs._native import is_python_executor_type 520 if is_python_executor_type(self.executor_keys.split(':', maxsplit=1)[0]): 521 return mrsm.get_connector(self.executor_keys) 522 ### An executor type of a native (Rust) plugin: no Python connector exists. 523 from meerschaum.jobs._NativeExecutor import NativeExecutor 524 return NativeExecutor(self.executor_keys)
If the job is not local, return the connector to its executor
(the remote API instance, systemd, a plugin's executor).
526 @property 527 def status(self) -> str: 528 """ 529 Return the running status of the job's daemon. 530 """ 531 if '_status_hook' in self.__dict__: 532 return self._status_hook() 533 534 return self._core.status
Return the running status of the job's daemon.
536 @property 537 def pid(self) -> Union[int, None]: 538 """ 539 Return the PID of the job's dameon. 540 """ 541 return self._core.pid
Return the PID of the job's dameon.
543 @property 544 def restart(self) -> bool: 545 """ 546 Return whether to restart a stopped job. 547 """ 548 return self._core.restarts
Return whether to restart a stopped job.
550 @property 551 def result(self) -> SuccessTuple: 552 """ 553 Return the `SuccessTuple` when the job has terminated. 554 """ 555 if '_result_hook' in self.__dict__: 556 if self.is_running(): 557 return True, f"{self} is running." 558 return self._result_hook() 559 560 return self._core.result
Return the SuccessTuple when the job has terminated.
562 @property 563 def sysargs(self) -> List[str]: 564 """ 565 Return the sysargs to use for the Daemon. 566 """ 567 return self._core.sysargs
Return the sysargs to use for the Daemon.
569 @property 570 def health(self) -> Dict[str, Any]: 571 """ 572 Return how the job's laps go (a job that repeats its work: `--loop`, `-s`, `--listen`). 573 574 Returns 575 ------- 576 A dictionary with the keys: 577 578 - `state`: `'healthy'` (the last lap succeeded), `'failing'` (the last lap failed), 579 `'stalled'` (no lap for three times the expected interval, and at least a minute; 580 an hour without a sync for `--listen`; at least an hour for a first lap), 581 `'stopped'` (not running, or paused) or `'unknown'` (nothing recorded yet, e.g. 582 `'first sync in progress'`). 583 - `last_success`, `last_lap`: ISO-8601 UTC timestamps (or `None`). 584 - `consecutive_failures`: laps that failed in a row. 585 - `expected_interval_seconds`: how often laps are expected (`--min-seconds`, the 586 schedule's period, an hour for `--listen`), or `None`. 587 - `next_run`: while a running job waits for its next lap (`--min-seconds`, a cooldown 588 after a failure, a schedule's next fire), when that lap is due (ISO-8601 UTC); 589 otherwise `None`. 590 - `message`: what to show next to the state, e.g. `'last good sync 2 min ago'`. 591 592 Examples 593 -------- 594 >>> job = Job('weather', 'sync pipes -c plugin:noaa --loop') 595 >>> success, msg = job.start() 596 >>> job.health['state'] # Before its first lap ends. 597 'unknown' 598 """ 599 return self._core.health()
Return how the job's laps go (a job that repeats its work: --loop, -s, --listen).
Returns
- A dictionary with the keys:
- -
state('healthy'(the last lap succeeded),'failing'(the last lap failed),):'stalled'(no lap for three times the expected interval, and at least a minute; an hour without a sync for--listen; at least an hour for a first lap),'stopped'(not running, or paused) or'unknown'(nothing recorded yet, e.g.'first sync in progress'). -
last_success,last_lap(ISO-8601 UTC timestamps (orNone).):-
consecutive_failures(laps that failed in a row.):-
expected_interval_seconds(how often laps are expected (--min-seconds, the): schedule's period, an hour for--listen), orNone.- -
next_run(while a running job waits for its next lap (--min-seconds, a cooldown): after a failure, a schedule's next fire), when that lap is due (ISO-8601 UTC); otherwiseNone. - -
message(what to show next to the state, e.g.'last good sync 2 min ago'.):
Examples
>>> job = Job('weather', 'sync pipes -c plugin:noaa --loop')
>>> success, msg = job.start()
>>> job.health['state'] # Before its first lap ends.
'unknown'
601 @property 602 def owner(self) -> Union[str, None]: 603 """ 604 Return the user who started this job through the API server or the web console 605 (`None` for a job created locally: the CLI, Compose, systemd or Python). A job started 606 by another job has that job's owner. For an `api:` job, the server's record. 607 608 Examples 609 -------- 610 >>> Job('nightly', executor_keys='api:main').owner 611 'alice' 612 """ 613 return self._core.access().get('owner', None)
Return the user who started this job through the API server or the web console
(None for a job created locally: the CLI, Compose, systemd or Python). A job started
by another job has that job's owner. For an api: job, the server's record.
Examples
>>> Job('nightly', executor_keys='api:main').owner
'alice'
615 @property 616 def team(self) -> Union[str, None]: 617 """ 618 Return the team this job belongs to: the primary team of the user who started it through 619 the API server or the web console (a job started by another job: that job's team). 620 `None` means Everyone (every local job). 621 622 Members of the team (and administrators) see the job and, with the job permissions, 623 control it. Renaming the team renames it here; deleting the team moves the job to 624 Everyone. 625 """ 626 return self._core.access().get('team', None)
Return the team this job belongs to: the primary team of the user who started it through
the API server or the web console (a job started by another job: that job's team).
None means Everyone (every local job).
Members of the team (and administrators) see the job and, with the job permissions, control it. Renaming the team renames it here; deleting the team moves the job to Everyone.
628 def get_daemon_properties(self) -> Dict[str, Any]: 629 """ 630 Return the `properties` dictionary for the job's daemon. 631 """ 632 return self._core.get_daemon_properties()
Return the properties dictionary for the job's daemon.
634 @property 635 def daemon(self) -> 'Daemon': 636 """ 637 Return the daemon which this job manages. 638 """ 639 from meerschaum.utils.daemon import Daemon 640 if self.executor_keys == 'local' and self.exists(): 641 return Daemon(daemon_id=self.name) 642 643 return Daemon( 644 daemon_id=self.name, 645 sysargs=self.sysargs, 646 label=shlex.join(self.sysargs), 647 properties=self.get_daemon_properties(), 648 )
Return the daemon which this job manages.
650 @property 651 def began(self) -> Union[datetime, None]: 652 """ 653 The datetime when the job began running. 654 """ 655 return _parse_dt(self._core.began)
The datetime when the job began running.
657 @property 658 def ended(self) -> Union[datetime, None]: 659 """ 660 The datetime when the job stopped running. 661 """ 662 return _parse_dt(self._core.ended)
The datetime when the job stopped running.
664 @property 665 def paused(self) -> Union[datetime, None]: 666 """ 667 The datetime when the job was suspended while running. 668 """ 669 return _parse_dt(self._core.paused)
The datetime when the job was suspended while running.
671 @property 672 def stop_time(self) -> Union[datetime, None]: 673 """ 674 Return the timestamp when the job was manually stopped. 675 """ 676 return _parse_dt(self._core.stop_time)
Return the timestamp when the job was manually stopped.
685 def check_restart(self) -> SuccessTuple: 686 """ 687 If `restart` is `True` and the daemon is not running, 688 restart the job. 689 Do not restart if the job was manually stopped. 690 """ 691 result = self._core.check_restart() 692 self._warn() 693 return result
If restart is True and the daemon is not running,
restart the job.
Do not restart if the job was manually stopped.
695 @property 696 def label(self) -> str: 697 """ 698 Return the job's Daemon label (joined sysargs). 699 """ 700 return self._core.label
Return the job's Daemon label (joined sysargs).
723 @property 724 def env(self) -> Dict[str, str]: 725 """ 726 Return the environment variables to set for the job's process. 727 """ 728 return self._core.env
Return the environment variables to set for the job's process.
730 @property 731 def delete_after_completion(self) -> bool: 732 """ 733 Return whether this job is configured to delete itself after completion. 734 """ 735 return self._core.delete_after_completion
Return whether this job is configured to delete itself after completion.
296def make_executor(cls): 297 """ 298 Register a class as an `Executor`. 299 """ 300 import re 301 from meerschaum.connectors import make_connector 302 suffix_regex = r'executor$' 303 typ = re.sub(suffix_regex, '', cls.__name__.lower()) 304 if typ not in executor_types: 305 executor_types.append(typ) 306 return make_connector(cls, _is_executor=True)
Register a class as an Executor.
10def pprint( 11 *args, 12 detect_password: bool = True, 13 nopretty: bool = False, 14 **kw 15) -> None: 16 """Pretty print an object according to the configured ANSI and UNICODE settings. 17 If detect_password is True (default), search and replace passwords with '*' characters. 18 Does not mutate objects. 19 """ 20 import copy 21 import json 22 from meerschaum.utils.packages import attempt_import, import_rich 23 from meerschaum.utils.formatting import ANSI, get_console, print_tuple 24 from meerschaum.utils.warnings import error 25 from meerschaum.utils.misc import replace_password, dict_from_od, filter_keywords 26 from collections import OrderedDict 27 28 if ( 29 len(args) == 1 30 and 31 isinstance(args[0], tuple) 32 and 33 len(args[0]) == 2 34 and 35 isinstance(args[0][0], bool) 36 and 37 isinstance(args[0][1], str) 38 ): 39 return print_tuple(args[0], **filter_keywords(print_tuple, **kw)) 40 41 modify = True 42 rich_pprint = None 43 if ANSI and not nopretty: 44 ### `rich` is not a dependency of the base wheel: without it, output is not styled. 45 rich = import_rich(lazy=False) 46 rich_pretty = attempt_import('rich.pretty', lazy=False) if rich is not None else None 47 if rich_pretty is not None and get_console() is not None: 48 def _rich_pprint(*args, **kw): 49 _console = get_console() 50 _kw = filter_keywords(_console.print, **kw) 51 _console.print(*args, **_kw) 52 rich_pprint = _rich_pprint 53 if rich_pprint is None and not nopretty: 54 pprintpp = attempt_import('pprintpp', warn=False) 55 try: 56 _pprint = pprintpp.pprint 57 except Exception : 58 import pprint as _pprint_module 59 _pprint = _pprint_module.pprint 60 61 func = ( 62 _pprint if rich_pprint is None else rich_pprint 63 ) if not nopretty else print 64 65 try: 66 args_copy = copy.deepcopy(args) 67 except Exception: 68 args_copy = args 69 modify = False 70 71 _args = [] 72 for a in args: 73 c = a 74 ### convert OrderedDict into dict 75 if isinstance(a, OrderedDict) or issubclass(type(a), OrderedDict): 76 c = dict_from_od(copy.deepcopy(c)) 77 _args.append(c) 78 args = _args 79 80 _args = list(args) 81 if detect_password and modify: 82 _args = [] 83 for a in args: 84 c = a 85 if isinstance(c, dict): 86 c = replace_password(copy.deepcopy(c)) 87 if nopretty: 88 try: 89 c = json.dumps(c) 90 is_json = True 91 except Exception: 92 is_json = False 93 if not is_json: 94 try: 95 c = str(c) 96 except Exception: 97 pass 98 _args.append(c) 99 100 ### filter out unsupported keywords 101 func_kw = filter_keywords(func, **kw) if not nopretty else {} 102 error_msg = None 103 try: 104 func(*_args, **func_kw) 105 except Exception as e: 106 error_msg = e 107 if error_msg is not None: 108 error(error_msg)
Pretty print an object according to the configured ANSI and UNICODE settings. If detect_password is True (default), search and replace passwords with '*' characters. Does not mutate objects.
1458def attempt_import( 1459 *names: str, 1460 lazy: bool = True, 1461 warn: bool = True, 1462 install: bool = True, 1463 venv: Optional[str] = 'mrsm', 1464 precheck: bool = True, 1465 split: bool = True, 1466 check_update: bool = False, 1467 check_pypi: bool = False, 1468 check_is_installed: bool = True, 1469 allow_outside_venv: bool = True, 1470 color: bool = True, 1471 debug: bool = False 1472) -> Any: 1473 """ 1474 Raise a warning if packages are not installed; otherwise import and return modules. 1475 If `lazy` is `True`, return lazy-imported modules. 1476 1477 Returns tuple of modules if multiple names are provided, else returns one module. 1478 1479 Parameters 1480 ---------- 1481 names: List[str] 1482 The packages to be imported. 1483 1484 lazy: bool, default True 1485 If `True`, lazily load packages. 1486 1487 warn: bool, default True 1488 If `True`, raise a warning if a package cannot be imported. 1489 1490 install: bool, default True 1491 If `True`, attempt to install a missing package into the designated virtual environment. 1492 If `check_update` is True, install updates if available. 1493 1494 venv: Optional[str], default 'mrsm' 1495 The virtual environment in which to search for packages and to install packages into. 1496 1497 precheck: bool, default True 1498 If `True`, attempt to find module before importing (necessary for checking if modules exist 1499 and retaining lazy imports), otherwise assume lazy is `False`. 1500 1501 split: bool, default True 1502 If `True`, split packages' names on `'.'`. 1503 1504 check_update: bool, default False 1505 If `True` and `install` is `True`, install updates if the required minimum version 1506 does not match. 1507 1508 check_pypi: bool, default False 1509 If `True` and `check_update` is `True`, check PyPI when determining whether 1510 an update is required. 1511 1512 check_is_installed: bool, default True 1513 If `True`, check if the package is contained in the virtual environment. 1514 1515 allow_outside_venv: bool, default True 1516 If `True`, search outside of the specified virtual environment 1517 if the package cannot be found. 1518 Setting to `False` will reinstall the package into a virtual environment, even if it 1519 is installed outside. 1520 1521 color: bool, default True 1522 If `False`, do not print ANSI colors. 1523 1524 Returns 1525 ------- 1526 The specified modules. If they're not available and `install` is `True`, it will first 1527 download them into a virtual environment and return the modules. 1528 1529 Examples 1530 -------- 1531 >>> pandas, sqlalchemy = attempt_import('pandas', 'sqlalchemy') 1532 >>> pandas = attempt_import('pandas') 1533 1534 """ 1535 1536 import importlib.util 1537 1538 global emitted_auto_install_warning 1539 no_auto_install_env_var = NO_AUTO_INSTALL_ENV_VAR 1540 if not _auto_install_allowed(): 1541 install = False 1542 1543 ### Cosmetic packages are never installed on import: without them output is plain text. 1544 if any(name.split('.')[0] in _NEVER_AUTO_INSTALLED for name in names): 1545 install = False 1546 warn = False 1547 1548 ### to prevent recursion, check if parent Meerschaum package is being imported 1549 if names == ('meerschaum',): 1550 return _import_module('meerschaum') 1551 1552 if venv == 'mrsm' and _import_hook_venv is not None: 1553 if debug: 1554 print(f"Import hook for virtual environment '{_import_hook_venv}' is active.") 1555 venv = _import_hook_venv 1556 1557 _warnings = _import_module('meerschaum.utils.warnings') 1558 warn_function = _warnings.warn 1559 1560 def do_import(_name: str, **kw) -> Union['ModuleType', None]: 1561 ### A fully-imported module resolves from `sys.modules` regardless of the active venv, 1562 ### so skip activating the venv (which resolves and creates its directories). 1563 import sys 1564 loaded_module = sys.modules.get(_name, None) 1565 if ( 1566 loaded_module is not None 1567 and not getattr(getattr(loaded_module, '__spec__', None), '_initializing', False) 1568 ): 1569 return loaded_module 1570 1571 with Venv(venv=venv, debug=debug): 1572 ### determine the import method (lazy vs normal) 1573 from meerschaum.utils.misc import filter_keywords 1574 import_method = ( 1575 _import_module if not lazy 1576 else lazy_import 1577 ) 1578 try: 1579 mod = import_method(_name, **(filter_keywords(import_method, **kw))) 1580 except Exception as e: 1581 if warn: 1582 import traceback 1583 traceback.print_exception(type(e), e, e.__traceback__) 1584 warn_function( 1585 f"Failed to import module '{_name}'.\nException:\n{e}", 1586 ImportWarning, 1587 stacklevel = (5 if lazy else 4), 1588 color = False, 1589 ) 1590 mod = None 1591 return mod 1592 1593 modules = [] 1594 for name in names: 1595 ### Check if package is a declared dependency. 1596 root_name = name.split('.')[0] if split else name 1597 install_name = _import_to_install_name(root_name) 1598 1599 if install_name is None: 1600 install_name = root_name 1601 if warn and root_name != 'plugins': 1602 warn_function( 1603 f"Package '{root_name}' is not declared in meerschaum.utils.packages.", 1604 ImportWarning, 1605 stacklevel = 3, 1606 color = False 1607 ) 1608 1609 ### Determine if the package exists. 1610 if precheck is False: 1611 found_module = ( 1612 do_import( 1613 name, debug=debug, warn=False, venv=venv, color=color, 1614 check_update=False, check_pypi=False, split=split, 1615 ) is not None 1616 ) 1617 else: 1618 installed_cache_key = (name, venv, split, allow_outside_venv) 1619 if check_is_installed: 1620 with _locks['_is_installed_first_check']: 1621 if not _is_installed_first_check.get(installed_cache_key, False): 1622 package_is_installed = is_installed( 1623 name, 1624 venv = venv, 1625 split = split, 1626 allow_outside_venv = allow_outside_venv, 1627 debug = debug, 1628 ) 1629 if package_is_installed: 1630 _is_installed_first_check[installed_cache_key] = True 1631 else: 1632 package_is_installed = True 1633 else: 1634 package_is_installed = venv_contains_package( 1635 name, 1636 venv=venv, 1637 split=split, 1638 debug=debug, 1639 ) 1640 found_module = package_is_installed 1641 1642 if not found_module: 1643 if install: 1644 with _locks['emitted_auto_install_warning']: 1645 if warn and not emitted_auto_install_warning: 1646 emitted_auto_install_warning = True 1647 warn_function( 1648 "Meerschaum is installing a missing runtime dependency. " 1649 + f"Set {no_auto_install_env_var}=1 to disable automatic downloads.", 1650 ImportWarning, 1651 stacklevel=3, 1652 color=False, 1653 ) 1654 if not pip_install( 1655 install_name, 1656 venv = venv, 1657 split = False, 1658 check_update = check_update, 1659 color = color, 1660 debug = debug, 1661 _implicit = True, 1662 ): 1663 if warn: 1664 warn_function( 1665 f"Failed to install '{install_name}'.", 1666 ImportWarning, 1667 stacklevel = 3, 1668 color = False, 1669 ) 1670 else: 1671 _record_import_request(install_name, venv) 1672 elif warn: 1673 ### Raise a warning if we can't find the package and install = False. 1674 warn_function( 1675 (f"\n\nMissing package '{name}' from virtual environment '{venv}'; " 1676 + "some features will not work correctly." 1677 + ( 1678 f"\n\nUnset {no_auto_install_env_var} to allow package installation.\n" 1679 if no_auto_install_env_var in os.environ 1680 else "\n\nSet install=True when calling attempt_import.\n" 1681 )), 1682 ImportWarning, 1683 stacklevel = 3, 1684 color = False, 1685 ) 1686 1687 elif found_module: 1688 _record_import_request(install_name, venv, only_plugins=True) 1689 1690 ### Do the import. Will be lazy if lazy=True. 1691 m = do_import( 1692 name, debug=debug, warn=warn, venv=venv, color=color, 1693 check_update=check_update, check_pypi=check_pypi, install=install, split=split, 1694 ) 1695 modules.append(m) 1696 1697 modules = tuple(modules) 1698 if len(modules) == 1: 1699 return modules[0] 1700 return modules
Raise a warning if packages are not installed; otherwise import and return modules.
If lazy is True, return lazy-imported modules.
Returns tuple of modules if multiple names are provided, else returns one module.
Parameters
- names (List[str]): The packages to be imported.
- lazy (bool, default True):
If
True, lazily load packages. - warn (bool, default True):
If
True, raise a warning if a package cannot be imported. - install (bool, default True):
If
True, attempt to install a missing package into the designated virtual environment. Ifcheck_updateis True, install updates if available. - venv (Optional[str], default 'mrsm'): The virtual environment in which to search for packages and to install packages into.
- precheck (bool, default True):
If
True, attempt to find module before importing (necessary for checking if modules exist and retaining lazy imports), otherwise assume lazy isFalse. - split (bool, default True):
If
True, split packages' names on'.'. - check_update (bool, default False):
If
TrueandinstallisTrue, install updates if the required minimum version does not match. - check_pypi (bool, default False):
If
Trueandcheck_updateisTrue, check PyPI when determining whether an update is required. - check_is_installed (bool, default True):
If
True, check if the package is contained in the virtual environment. - allow_outside_venv (bool, default True):
If
True, search outside of the specified virtual environment if the package cannot be found. Setting toFalsewill reinstall the package into a virtual environment, even if it is installed outside. - color (bool, default True):
If
False, do not print ANSI colors.
Returns
- The specified modules. If they're not available and
installisTrue, it will first - download them into a virtual environment and return the modules.
Examples
>>> pandas, sqlalchemy = attempt_import('pandas', 'sqlalchemy')
>>> pandas = attempt_import('pandas')
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']).
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
>>>
114def entry( 115 sysargs: Union[List[str], str, None] = None, 116 _patch_args: Optional[Dict[str, Any]] = None, 117 _use_cli_daemon: bool = True, 118 _session_id: Optional[str] = None, 119) -> SuccessTuple: 120 """ 121 Parse arguments and launch a Meerschaum action. 122 123 The action runs natively, in this process. Ctrl-C asks a looping action to stop and the call 124 then raises `KeyboardInterrupt`; Python delivers signals to the main thread only, so a call 125 made from another thread cannot be interrupted. Inside a plugin worker, a call made while a plugin module is being imported (at the top 126 level of the module) runs without plugins, so the import cannot recurse. 127 128 Returns 129 ------- 130 A `SuccessTuple` indicating success. 131 """ 132 return entry_without_daemon(sysargs, _patch_args=_patch_args)
Parse arguments and launch a Meerschaum action.
The action runs natively, in this process. Ctrl-C asks a looping action to stop and the call
then raises KeyboardInterrupt; Python delivers signals to the main thread only, so a call
made from another thread cannot be interrupted. Inside a plugin worker, a call made while a plugin module is being imported (at the top
level of the module) runs without plugins, so the import cannot recurse.
Returns
- A
SuccessTupleindicating success.