✍️ Writing Your Own Plugins¶
This tutorial explains how to extend Meerschaum with plugins. For general information, consult the Types of Plugins reference page.
Meerschaum's plugin system is designed to be simple so you can get your plugins working quickly. Plugins are Python packages defined in the Meerschaum configuration plugins
directory and are imported at startup under the global namespace plugins
.
Looking to make your existing package a plugin? See Package Management below.
To create your plugin, follow these steps:
-
Navigate to your Meerschaum
plugins
directory.~/.config/meerschaum/plugins
%APPDATA%\Meerschaum\plugins
plugins/
If you have set
MRSM_ROOT_DIR
orMRSM_PLUGINS_DIR
, navigate to your designated plugins directory. -
Create your Python module.
Create either<name>.py
or<name>/__init__.py
.Plugins may have hyphenated names, e.g.
mongodb-connector
: -
(Optional) Define your plugin's
__version__
string.Plugin repositories need
__version__
To publish changes to a repository with
register plugin
, you must increment__version__
(according to SemVer). You may omit__version__
for the initial release, but subsequent releases need the version defined.1 2
# ~/.config/meerschaum/plugins/example.py __version__ = '0.0.1'
-
(Optional) Define your required dependencies.
Your plugin will be run from a virtual environment and therefore may not be able to import packages that aren't declared.
These packagess will be installed into the plugin's virtual environment at installation or with
mrsm install required <plugins>
.Depend on other plugins
Dependencies that start with
plugin:
are Meerschaum plugins. To require a plugin from another repository, add add the repository's keys after the plugin name (e.g.plugin:foo@api:bar
).1
required = ['rich>=9.8.0', 'pandas', 'plugin:foo']
-
Write your functions.
Special functions are
fetch()
,sync()
,register()
,setup()
, and<package_name>()
, and you can use the@make_action
decorator to designate additional functions as actions. Below is more information on these functions.Don't forget keyword arguments!
You must include a
**kwargs
argument to capture optional arguments. The functionsfetch()
,sync()
, andregister()
also require apipe
positional argument for themeerschaum.Pipe
object being synced.You may find the supplied arguments useful for your implementation (e.g.
begin
andend
datetime.datetime
objects). Runmrsm -h
ormrsm show help
to see the available keyword arguments.
Imports impact performance
For optimal performance, keep module-level code to a minimum ― especially heavy imports.
1 2 3 4 |
|
1 2 3 4 |
|
Functions¶
Plugins are just modules with functions. This section explains the roles of the following special functions:
register(pipe: mrsm.Pipe, **kwargs)
Return a pipe's initial parameters dictionary.fetch(pipe: mrsm.Pipe, **kwargs)
Return a DataFrame, list of dictionaries, or generator of DataFrame-like chunks.sync(pipe: mrsm.Pipe, **kwargs)
Override a pipe'ssync
process for finer-grained control.@make_connector
Create new connector types.@make_action
Create new commands.@api_plugin
Create new FastAPI endpoints.@dash_plugin
and@web_page
Create new Dash pages on the Web Console.@pre_sync_hook
and@post_sync_hook
Inject callbacks when pipes are synced by thesync pipes
action.setup(**kwargs)
Executed during plugin installation or withmrsm setup plugins <plugin>
.
The register()
Function¶
The register()
function returns a new pipe's parameters dictionary.
If you are using Meerschaum Compose, then register()
is overriden by mrsm-compose.yaml
and may be skipped.
The below example is register()
from the noaa
plugin:
Register function example
1 2 3 4 5 6 7 8 9 |
|
The fetch()
Function¶
The fastest way to leverage Meerschaum's syncing engine is with the fetch()
function. Simply return a DataFrame (or list of dictionaries) or a chunk generator.
Tip
Just like when writing sync plugins, there are additional keyword arguments available that you might find useful. Go ahead and inspect **kw
and see if anything is helpful (e.g. begin
, end
, blocking
, etc.). Check out Keyword Arguments for further information.
Below is an example of a simple fetch plugin:
Fetch Plugin Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
If your plugin will be fetching large amounts of data, you may return a generator of DataFrame-like chunks:
1 2 3 4 5 6 7 |
|
Chunking handles any iterable, so you may return a simple generator or yield
the chunks yourself.
1 2 3 |
|
1 2 3 4 |
|
The sync()
Function¶
The sync()
function makes sync pipes
override the built-in syncing process and behaves more like an action, returning only a SuccessTuple
(e.g. True, "Success"
).
Sync plugins allow for much more flexibility than fetch plugins, so what you come up with may differ from the following example. In any case, below is a simple sync plugin.
Note
The only required argument is the positional pipe
argument. The following example demonstrates one use of the begin
and end
arguments. Check out Keyword Arguments for further information.
Sync Plugin Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
The @make_connector
Decorator¶
Defining a new type of connector is easy:
- Create a new class that inherits from
meerschaum.connectors.Connector
. - Decorate the class with
@make_connector
. - Define the class-level list
REQUIRED_ATTRIBUTES
. - Add the method
fetch(pipe, **kwargs)
that returns data.
For example, the following creates a new connector of type foo
:
FooConnector
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
You can register new foo
connectors via mrsm bootstrap connector
, which would prompt the user for each attribute in REQUIRED_ATTRIBUTES
(username, password).
You may also define your new foo
connectors as environment variables, e.g.:
1 2 3 4 5 6 7 8 |
|
Instance Connectors
You may designate your connector as an instance connector by adding IS_INSTANCE = True
.
If you are creating an instance connector and want to enable multithreading, add IS_THREAD_SAFE = True
.
The @make_action
Decorator¶
Your plugin may extend Meerschaum by providing additional actions. Actions are regular Python functions but with perks:
- Actions are integrated into the larger Meerschaum system.
Actions from plugins may be executed as background jobs, via the web console, or via the API, just like standard actions. - Actions inherit the standard Meerschaum keyword arguments.
You can use flags such as--begin
,--end
, etc., or even add your own custom flags.
Sub-Actions
Actions with underscores are a good way to add sub-actions (e.g. foo_bar
is the same as foo bar
).
Action Plugin Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Suggest Auto-Completions¶
Return a list of options from a function complete_<action>()
, and these options will be suggested in the Meerschaum shell. The keyword arguments passed to complete_<action>()
are line
, sysargs
, action
, and the currently parsed flags.
1 2 3 4 5 6 |
|
The @api_plugin
Decorator¶
Meerschaum plugins may also extend the web API by accessing the FastAPI
app. Use the @api_plugin
decorator to define an initialization function that will be executed on the command mrsm start api
.
The only argument for the initalization function should be app
for the FastAPI application.
For your endpoints, arguments will be used as HTTP parameters, and to require that the user must be logged in, import manager
from meerschaum.api
and add the argument curr_user = fastapi.Depends(manager)
:
Swagger Endpoint Tester
Navigate to https://localhost:8000/docs to test your endpoint in the browser.
API Plugin Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
The @dash_plugin
and @web_page
Decorators¶
Add new Plotly Dash pages to the Web Console with @dash_plugin
and @web_page
.
Like @api_plugin
, @dash_app
designates an initialization function which accepts the Dash app. Within this function, you can create callbacks with @dash_app.callback()
.
@web_page
may be used with or without arguments. When invoked without arguments, it will use the name of the function as the path string.
Functions decorated with @web_page
return a Dash layout. These don't need to reside within the @dash_app
-decorated function, but this is recommended to improve lazy-loading performance.
Dash Plugin Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Dynamic Routing
Sub-paths will be routed to your base callback, e.g. /items/apple
will be accepted by @web_page('/items')
. You can then parse the path string with dcc.Location
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
The @pre_sync_hook
and @post_sync_hook
Decorators¶
You can tap into the built-in syncing engine via the sync pipes
action by decorating callback functions with @pre_sync_hook
and/or @post_sync_hook
. Both callbacks accept a positional pipe
argument, and other useful contextual arguments as passed as keyword arguments (if your function accepts them).
Add **kwargs
(optional) for additional context of sync, e.g.:
sync_method
(Callable[[], mrsm.SuccessTuple]
)
A bound function of eitherpipe.sync()
,pipe.verify()
, orpipe.deduplicate()
.sync_timestamp
(datetime
)
The UTC datetime captured right before the sync began. This value is the same for pre- and post-syncs.
The following keywords arguments are available only for @post_sync_hook
callbacks:
success_tuple
(mrsm.SuccessTuple
)
The return value ofsync_method
(SuccessTuple
i.e.Tuple[bool, str]
) to indicate whether the sync completed successfully.sync_complete_timestamp
(datetime
)
The UTC datetime captured right after the sync completed.sync_duration
(float
)
The duration of the sync in seconds.
All other keyword arguments correspond to the flags used to initiate the sync. See the following useful arguments:
name
(str
)
If the sync was spawned by a background job with a--name
argument, you may track the job's syncs withname
.schedule
(str
)
If you spawn your syncing jobs with-s
, you may see the job's cadence withschedule
.min_seconds
(float
)
If you run continous syncs (i.e. with--loop
), the value of--min-seconds
will be available asmin_seconds
.begin
andend
(datetime
|int
)
The values for--begin
and--end
.params
(dict[str, Any]
)
The value for--params
.
Sync hooks don't need to return anything, but if a SuccessTuple
is returned from your callback function, it will be pretty-printed alongside the normal success messages.
@pre_sync_hook
and @post_sync_hook
example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
The setup()
Function¶
When your plugin is first installed, its required
list will be installed, but in case you need to do any extra setup upon installation, you can write a setup()
function which returns a tuple of a boolean and a message.
Below is a snippet from the apex
plugin which initializes a Selenium WebDriver.
Setup Function Example
1 2 3 4 5 6 7 8 |
|
Keyword Arguments¶
There are a many useful command line arguments provided to plugins as keyword arguments. To add customizability to your plugins, consider inspecting the keys in the **kwargs
dictionary.
Tip
You can see the value of **kwargs
with the command mrsm show arguments
.
Inspecting **kwargs
1 2 3 4 5 6 |
|
Useful Command Line and Keyword Arguments¶
You can see the available command line options with the command mrsm show help
or mrsm --help
. Expand the table below to see more information about usefult builtin arguments.
Useful Keyword Arguments
Keyword Argument | Command Line Flags | Type | Examples | Description |
---|---|---|---|---|
action |
Positional arguments | Optional[List[str]] |
['foo', 'bar'] |
The positional action arguments following the action are passed into the action list. For example, the command mrsm example foo bar will be parsed into the list ['foo', 'bar'] for the action function example(action : Optional[List[str]] = None) . |
yes |
-y , --yes |
Bool = False |
False (default), True |
When a user provides the -y flag, yes/no prompts should default to the yes option. You can easily use this functionality with the builtin yes_no() function. |
force |
-f , --force |
Bool = False |
False (default), True |
The -f flag implies -y . Generally, when -f is passed, you may skip asking for confirmation altogether. |
loop |
--loop |
Bool = False |
False (default), True |
The --loop flag implies that the action should be continuously executed. The exact looping logic is left up to the action developer and therefore is only used in certain contexts (such as in sync pipes ). |
begin |
--begin |
Optional[datetime.datetime] = None |
datetime.datetime(2021, 1, 1, 0, 0) |
A user may provide a begin datetime. Consult sync pipes or show data for examples. |
end |
--end |
Optional[datetime.datetime] = None |
datetime.datetime(2021, 1, 1, 0, 0) |
A user may provide an end datetime. Consult sync pipes or show data for examples. |
connector_keys |
-c , -C , --connector-keys |
Optional[List[str]] = None |
['sql:main', 'sql:remote'] |
The list of connectors is used to filter pipes. This filtering is done with the get_pipes() function. |
metric_keys |
-m , -M , --metric-keys |
Optional[List[str]] = None |
['weather', 'power'] |
The list of metrics is used to filter pipes. This filtering is done with the get_pipes() function. |
location_keys |
-l , -L , --location-keys |
Optional[List[str]] = None |
[None, 'clemson'] |
The list of metrics is used to filter pipes. This filtering is done with the get_pipes() function. The location None is preserved and always parsed into None (NULL ). |
mrsm_instance , instance |
-i , -I , --instance , --mrsm_instance |
Optional[str] = None |
'sql:main' |
The connector keys of the corresponding Meerschaum instance. This filtering is done with the get_pipes() function. If mrsm_instance is None (default), the configured instance will be used ('sql:main' by default). When actions are launched from within the Meerschaum shell, the current instance is passed. |
repository |
-r , --repository , --repo |
Optional[str] = None |
'api:mrsm' |
The connector keys of the corresponding Meerschaum repository. If repository is None (default), the configured repository will be used ('api:mrsm' by default). When actions are launched from within the Meerschaum shell, the current repository is passed. |
Custom Command Line Options¶
In case the built-in command line options are not sufficient, you can add arguments with add_plugin_argument()
. This function takes the same arguments as the parser.add_argument()
function from argparse
and will include your plugin's arguments in the --help
text.
1 2 3 4 |
|
The above code snippet will produce append the following text to the --help
or show help
text:
1 2 |
|
sysargs
, shell
, and Other Edge Cases¶
Generally, using built-in or custom arguments mentioned above should cover almost every use case. In case you have specific needs, the arguments sysargs
, sub_args
, filtered_sysargs
, shell
, and line
are provided.
Edge Case Keyword Arguments
Keyword Argument | Type | Example | Description |
---|---|---|---|
sysargs |
List[str] |
['ls', '[-l]', '[-a]', '[-h]'] |
The sysargs keyword corresponds to sys.argv[1:] . |
line |
Optional[List[str]] = None |
'ls [-l] [-a] [-h]' |
The line keyword is a string which corresponds to sysargs joined by spaces. line is only provided when the Meerschaum shell is used. |
sub_args |
Optional[List[str]] = None |
['-l', '-a', -h'] |
The sub_args keyword corresponds to items in sysargs enclosed in square brackets ([] ). |
filtered_sysargs |
List[str] |
['ls'] |
filtered_sysargs contains the values of sysargs without sub_args . |
shell |
Bool |
True , False |
The shell boolean indicates whether or not an action was executed from within the Meerschaum shell. |
Working With Plugins¶
Plugins are just Python modules, so you can write custom code and share it amongst other plugins (i.e. a library plugin).
At run time, plugins are imported under the global plugins
namespace, but you'll probably be testing plugins directly when the plugins
namespace isn't created. That's where Plugin
objects come in handy: they contain a number of convenience functions so you can cross-pollinate between plugins.
Package Management¶
Meerschaum plugins only need to be modules ― no package metadata required. But if you plan on managing your plugins as proper packages (e.g. to publish to repositories like PyPI), simply add the entry point meerschaum.plugins
to your package metadata:
Creating Plugins from a Package Entry Point
1 2 3 4 5 6 7 8 9 10 |
|
1 2 |
|
Import Another Plugin¶
Accessing the member module
of the Plugin
object will import its module:
1 2 3 |
|
Plugins in a REPL¶
Plugins run from virtual environments, so to import your plugin in a REPL, you'll need to activate its environment before importing:
1 2 3 4 |
|
Plugins in Scripts¶
You can also pass a plugin to the Venv
virtual environment manager, which handles activating and deactivating environments.
1 2 3 4 5 |
|