meerschaum.connectors.api

Interact with the Meerschaum API (send commands, pull data, etc.)

 1#! /usr/bin/env python
 2# -*- coding: utf-8 -*-
 3# vim:fenc=utf-8
 4
 5"""
 6Interact with the Meerschaum API (send commands, pull data, etc.)
 7"""
 8
 9from meerschaum.connectors.api._APIConnector import APIConnector
10
11__all__ = ('APIConnector',)
class APIConnector(meerschaum.connectors.instance._InstanceConnector.InstanceConnector):
 22class APIConnector(InstanceConnector):
 23    """
 24    Connect to a Meerschaum API instance.
 25    """
 26
 27    IS_THREAD_SAFE: bool = False
 28    OPTIONAL_ATTRIBUTES: List[str] = ['port', 'client_secret', 'client_id', 'api_key']
 29
 30    from ._request import (
 31        make_request,
 32        get,
 33        post,
 34        put,
 35        patch,
 36        delete,
 37        wget,
 38    )
 39    from ._actions import (
 40        get_actions,
 41        do_action,
 42        do_action_async,
 43        do_action_legacy,
 44    )
 45    from ._misc import get_mrsm_version, get_chaining_status
 46    from ._pipes import (
 47        get_pipe_instance_keys,
 48        register_pipe,
 49        fetch_pipes_keys,
 50        edit_pipe,
 51        sync_pipe,
 52        delete_pipe,
 53        delete_pipe_cache,
 54        get_pipe_data,
 55        get_pipe_id,
 56        get_pipe_attributes,
 57        get_sync_time,
 58        pipe_exists,
 59        create_metadata,
 60        get_pipe_rowcount,
 61        drop_pipe,
 62        clear_pipe,
 63        get_pipe_columns_types,
 64        get_pipe_columns_indices,
 65        get_pipe_docs,
 66        get_pipe_size,
 67        compress_pipe,
 68        decompress_pipe,
 69        vacuum_pipe,
 70        analyze_pipe,
 71        partition_pipe,
 72    )
 73    from ._fetch import fetch
 74    from ._plugins import (
 75        register_plugin,
 76        install_plugin,
 77        delete_plugin,
 78        get_plugins,
 79        get_plugin_attributes,
 80        get_plugin_version,
 81    )
 82    from ._login import login, test_connection
 83    from ._users import (
 84        register_user,
 85        get_user_id,
 86        get_users,
 87        edit_user,
 88        delete_user,
 89        get_user_password_hash,
 90        get_user_type,
 91        get_user_attributes,
 92    )
 93    from ._tokens import (
 94        register_token,
 95        get_token_model,
 96        get_tokens,
 97        edit_token,
 98        invalidate_token,
 99        get_token_scopes,
100        token_exists,
101        delete_token,
102    )
103    from ._uri import from_uri
104    from ._jobs import (
105        get_jobs,
106        get_job,
107        get_job_metadata,
108        get_job_properties,
109        get_job_exists,
110        delete_job,
111        start_job,
112        create_job,
113        stop_job,
114        pause_job,
115        get_logs,
116        get_job_stop_time,
117        monitor_logs,
118        monitor_logs_async,
119        get_job_is_blocking_on_stdin,
120        get_job_began,
121        get_job_ended,
122        get_job_paused,
123        get_job_status,
124    )
125
126    def __init__(
127        self,
128        label: Optional[str] = None,
129        wait: bool = False,
130        debug: bool = False,
131        **kw
132    ):
133        if 'uri' in kw:
134            from_uri_params = self.from_uri(kw['uri'], as_dict=True)
135            label = label or from_uri_params.get('label', None)
136            _ = from_uri_params.pop('label', None)
137            kw.update(from_uri_params)
138
139        super().__init__('api', label=label, **kw)
140        if 'protocol' not in self.__dict__:
141            self.protocol = (
142                'https' if self.__dict__.get('uri', '').startswith('https')
143                else 'http'
144            )
145
146        if 'uri' not in self.__dict__:
147            self.verify_attributes(required_attributes)
148        else:
149            from meerschaum.connectors.sql import SQLConnector
150            conn_attrs = SQLConnector.parse_uri(self.__dict__['uri'])
151            if 'host' not in conn_attrs:
152                raise Exception(f"Invalid URI for '{self}'.")
153            self.__dict__.update(conn_attrs)
154
155        self.url = (
156            self.protocol + '://' +
157            self.host
158            + (
159                (':' + str(self.port))
160                if self.__dict__.get('port', None)
161                else ''
162            )
163        )
164        self._token = None
165        self._expires = None
166        self._session = None
167        self._instance_keys = self.__dict__.get('instance_keys', None)
168
169
170    @property
171    def URI(self) -> str:
172        """
173        Return the fully qualified URI.
174        """
175        import urllib.parse
176        username = self.__dict__.get('username', None)
177        password = self.__dict__.get('password', None)
178        client_id = self.__dict__.get('client_id', None)
179        client_secret = self.__dict__.get('client_secret', None)
180        api_key = self.__dict__.get('api_key', None)
181        creds = (username + ':' + password + '@') if username and password else ''
182        params = {}
183        params_str = ('?' + urllib.parse.urlencode(params)) if params else ''
184        return (
185            self.protocol
186            + '://'
187            + creds
188            + self.host
189            + (
190                (':' + str(self.port))
191                if self.__dict__.get('port', None)
192                else ''
193            )
194            + params_str
195        )
196
197    @property
198    def session(self):
199        if self._session is None:
200            _ = attempt_import('certifi', lazy=False)
201            requests = attempt_import('requests', lazy=False)
202            if requests:
203                self._session = requests.Session()
204            if self._session is None:
205                error("Failed to import requests. Is requests installed?")
206        return self._session
207
208    @property
209    def token(self):
210        if self.login_scheme == 'api_key':
211            return self.api_key
212
213        expired = (
214            True if self._expires is None else (
215                (
216                    self._expires
217                    <
218                    datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(minutes=1)
219                )
220            )
221        )
222
223        if self._token is None or expired:
224            success, msg = self.login()
225            if not success and not self.__dict__.get('_emitted_warning'):
226                warn(msg, stack=False)
227                self._emitted_warning = True
228        return self._token
229
230    @property
231    def instance_keys(self) -> Union[str, None]:
232        """
233        Return the instance keys to be sent alongside pipe requests.
234        """
235        return self._instance_keys
236
237    @property
238    def login_scheme(self) -> str:
239        """
240        Return the login scheme to use based on the configured credentials.
241        """
242        if 'username' in self.__dict__:
243            return 'password'
244        if 'client_id' in self.__dict__:
245            return 'client_credentials'
246        elif 'api_key' in self.__dict__:
247            return 'api_key'
248
249        return 'password'

Connect to a Meerschaum API instance.

APIConnector( label: Optional[str] = None, wait: bool = False, debug: bool = False, **kw)
126    def __init__(
127        self,
128        label: Optional[str] = None,
129        wait: bool = False,
130        debug: bool = False,
131        **kw
132    ):
133        if 'uri' in kw:
134            from_uri_params = self.from_uri(kw['uri'], as_dict=True)
135            label = label or from_uri_params.get('label', None)
136            _ = from_uri_params.pop('label', None)
137            kw.update(from_uri_params)
138
139        super().__init__('api', label=label, **kw)
140        if 'protocol' not in self.__dict__:
141            self.protocol = (
142                'https' if self.__dict__.get('uri', '').startswith('https')
143                else 'http'
144            )
145
146        if 'uri' not in self.__dict__:
147            self.verify_attributes(required_attributes)
148        else:
149            from meerschaum.connectors.sql import SQLConnector
150            conn_attrs = SQLConnector.parse_uri(self.__dict__['uri'])
151            if 'host' not in conn_attrs:
152                raise Exception(f"Invalid URI for '{self}'.")
153            self.__dict__.update(conn_attrs)
154
155        self.url = (
156            self.protocol + '://' +
157            self.host
158            + (
159                (':' + str(self.port))
160                if self.__dict__.get('port', None)
161                else ''
162            )
163        )
164        self._token = None
165        self._expires = None
166        self._session = None
167        self._instance_keys = self.__dict__.get('instance_keys', None)

Set the given keyword arguments as attributes.

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

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

meerschaum:
    connections:
        {type}:
            {label}:
                ### attributes go here
IS_THREAD_SAFE: bool = False
OPTIONAL_ATTRIBUTES: List[str] = ['port', 'client_secret', 'client_id', 'api_key']
url
URI: str
170    @property
171    def URI(self) -> str:
172        """
173        Return the fully qualified URI.
174        """
175        import urllib.parse
176        username = self.__dict__.get('username', None)
177        password = self.__dict__.get('password', None)
178        client_id = self.__dict__.get('client_id', None)
179        client_secret = self.__dict__.get('client_secret', None)
180        api_key = self.__dict__.get('api_key', None)
181        creds = (username + ':' + password + '@') if username and password else ''
182        params = {}
183        params_str = ('?' + urllib.parse.urlencode(params)) if params else ''
184        return (
185            self.protocol
186            + '://'
187            + creds
188            + self.host
189            + (
190                (':' + str(self.port))
191                if self.__dict__.get('port', None)
192                else ''
193            )
194            + params_str
195        )

Return the fully qualified URI.

session
197    @property
198    def session(self):
199        if self._session is None:
200            _ = attempt_import('certifi', lazy=False)
201            requests = attempt_import('requests', lazy=False)
202            if requests:
203                self._session = requests.Session()
204            if self._session is None:
205                error("Failed to import requests. Is requests installed?")
206        return self._session
token
208    @property
209    def token(self):
210        if self.login_scheme == 'api_key':
211            return self.api_key
212
213        expired = (
214            True if self._expires is None else (
215                (
216                    self._expires
217                    <
218                    datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(minutes=1)
219                )
220            )
221        )
222
223        if self._token is None or expired:
224            success, msg = self.login()
225            if not success and not self.__dict__.get('_emitted_warning'):
226                warn(msg, stack=False)
227                self._emitted_warning = True
228        return self._token
instance_keys: Optional[str]
230    @property
231    def instance_keys(self) -> Union[str, None]:
232        """
233        Return the instance keys to be sent alongside pipe requests.
234        """
235        return self._instance_keys

Return the instance keys to be sent alongside pipe requests.

login_scheme: str
237    @property
238    def login_scheme(self) -> str:
239        """
240        Return the login scheme to use based on the configured credentials.
241        """
242        if 'username' in self.__dict__:
243            return 'password'
244        if 'client_id' in self.__dict__:
245            return 'client_credentials'
246        elif 'api_key' in self.__dict__:
247            return 'api_key'
248
249        return 'password'

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

def make_request( self, method: str, r_url: str, headers: Optional[Dict[str, Any]] = None, use_token: bool = True, debug: bool = False, **kwargs: Any) -> requests.models.Response:
 28def make_request(
 29    self,
 30    method: str,
 31    r_url: str,
 32    headers: Optional[Dict[str, Any]] = None,
 33    use_token: bool = True,
 34    debug: bool = False,
 35    **kwargs: Any
 36) -> 'requests.Response':
 37    """
 38    Make a request to this APIConnector's endpoint using the in-memory session.
 39
 40    Parameters
 41    ----------
 42    method: str
 43        The kind of request to make.
 44        Accepted values:
 45        - `'GET'`
 46        - `'OPTIONS'`
 47        - `'HEAD'`
 48        - `'POST'`
 49        - `'PUT'`
 50        - `'PATCH'`
 51        - `'DELETE'`
 52
 53    r_url: str
 54        The relative URL for the endpoint (e.g. `'/pipes'`).
 55
 56    headers: Optional[Dict[str, Any]], default None
 57        The headers to use for the request.
 58        If `use_token` is `True`, the authorization token will be added to a copy of these headers.
 59
 60    use_token: bool, default True
 61        If `True`, add the authorization token to the headers.
 62
 63    debug: bool, default False
 64        Verbosity toggle.
 65
 66    kwargs: Any
 67        All other keyword arguments are passed to `requests.request`.
 68
 69    Returns
 70    -------
 71    A `requests.Reponse` object.
 72    """
 73    if method.upper() not in METHODS:
 74        raise ValueError(f"Method '{method}' is not supported.")
 75
 76    verify = self.__dict__.get('verify', None)
 77    if 'verify' not in kwargs and isinstance(verify, bool):
 78        kwargs['verify'] = verify
 79
 80    headers = (
 81        copy.deepcopy(headers)
 82        if isinstance(headers, dict)
 83        else {}
 84    )
 85
 86    if use_token:
 87        headers.update({'Authorization': f'Bearer {self.token}'})
 88
 89    if 'timeout' not in kwargs:
 90        kwargs['timeout'] = STATIC_CONFIG['api']['default_timeout']
 91
 92    request_url = urllib.parse.urljoin(self.url, r_url)
 93    if debug:
 94        dprint(f"[{self}] Sending a '{method.upper()}' request to {request_url}")
 95
 96    return self.session.request(
 97        method.upper(),
 98        request_url,
 99        headers=headers,
100        **kwargs
101    )

Make a request to this APIConnector's endpoint using the in-memory session.

Parameters
  • method (str): The kind of request to make. Accepted values:
    • 'GET'
    • 'OPTIONS'
    • 'HEAD'
    • 'POST'
    • 'PUT'
    • 'PATCH'
    • 'DELETE'
  • r_url (str): The relative URL for the endpoint (e.g. '/pipes').
  • headers (Optional[Dict[str, Any]], default None): The headers to use for the request. If use_token is True, the authorization token will be added to a copy of these headers.
  • use_token (bool, default True): If True, add the authorization token to the headers.
  • debug (bool, default False): Verbosity toggle.
  • kwargs (Any): All other keyword arguments are passed to requests.request.
Returns
  • A requests.Reponse object.
def get(self, r_url: str, **kwargs: Any) -> requests.models.Response:
104def get(self, r_url: str, **kwargs: Any) -> 'requests.Response':
105    """
106    Wrapper for `requests.get`.
107
108    Parameters
109    ----------
110    r_url: str
111        The relative URL for the endpoint (e.g. `'/pipes'`).
112
113    headers: Optional[Dict[str, Any]], default None
114        The headers to use for the request.
115        If `use_token` is `True`, the authorization token will be added to a copy of these headers.
116
117    use_token: bool, default True
118        If `True`, add the authorization token to the headers.
119
120    debug: bool, default False
121        Verbosity toggle.
122
123    kwargs: Any
124        All other keyword arguments are passed to `requests.request`.
125
126    Returns
127    -------
128    A `requests.Reponse` object.
129
130    """
131    return self.make_request('GET', r_url, **kwargs)

Wrapper for requests.get.

Parameters
  • r_url (str): The relative URL for the endpoint (e.g. '/pipes').
  • headers (Optional[Dict[str, Any]], default None): The headers to use for the request. If use_token is True, the authorization token will be added to a copy of these headers.
  • use_token (bool, default True): If True, add the authorization token to the headers.
  • debug (bool, default False): Verbosity toggle.
  • kwargs (Any): All other keyword arguments are passed to requests.request.
Returns
  • A requests.Reponse object.
def post(self, r_url: str, **kwargs: Any) -> requests.models.Response:
134def post(self, r_url: str, **kwargs: Any) -> 'requests.Response':
135    """
136    Wrapper for `requests.post`.
137
138    Parameters
139    ----------
140    r_url: str
141        The relative URL for the endpoint (e.g. `'/pipes'`).
142
143    headers: Optional[Dict[str, Any]], default None
144        The headers to use for the request.
145        If `use_token` is `True`, the authorization token will be added to a copy of these headers.
146
147    use_token: bool, default True
148        If `True`, add the authorization token to the headers.
149
150    debug: bool, default False
151        Verbosity toggle.
152
153    kwargs: Any
154        All other keyword arguments are passed to `requests.request`.
155
156    Returns
157    -------
158    A `requests.Reponse` object.
159
160    """
161    return self.make_request('POST', r_url, **kwargs)

Wrapper for requests.post.

Parameters
  • r_url (str): The relative URL for the endpoint (e.g. '/pipes').
  • headers (Optional[Dict[str, Any]], default None): The headers to use for the request. If use_token is True, the authorization token will be added to a copy of these headers.
  • use_token (bool, default True): If True, add the authorization token to the headers.
  • debug (bool, default False): Verbosity toggle.
  • kwargs (Any): All other keyword arguments are passed to requests.request.
Returns
  • A requests.Reponse object.
def put(self, r_url: str, **kwargs: Any) -> requests.models.Response:
193def put(self, r_url: str, **kwargs: Any) -> 'requests.Response':
194    """
195    Wrapper for `requests.put`.
196
197    Parameters
198    ----------
199    r_url: str
200        The relative URL for the endpoint (e.g. `'/pipes'`).
201
202    headers: Optional[Dict[str, Any]], default None
203        The headers to use for the request.
204        If `use_token` is `True`, the authorization token will be added to a copy of these headers.
205
206    use_token: bool, default True
207        If `True`, add the authorization token to the headers.
208
209    debug: bool, default False
210        Verbosity toggle.
211
212    kwargs: Any
213        All other keyword arguments are passed to `requests.request`.
214
215    Returns
216    -------
217    A `requests.Reponse` object.
218    """
219    return self.make_request('PUT', r_url, **kwargs)

Wrapper for requests.put.

Parameters
  • r_url (str): The relative URL for the endpoint (e.g. '/pipes').
  • headers (Optional[Dict[str, Any]], default None): The headers to use for the request. If use_token is True, the authorization token will be added to a copy of these headers.
  • use_token (bool, default True): If True, add the authorization token to the headers.
  • debug (bool, default False): Verbosity toggle.
  • kwargs (Any): All other keyword arguments are passed to requests.request.
Returns
  • A requests.Reponse object.
def patch(self, r_url: str, **kwargs: Any) -> requests.models.Response:
164def patch(self, r_url: str, **kwargs: Any) -> 'requests.Response':
165    """
166    Wrapper for `requests.patch`.
167
168    Parameters
169    ----------
170    r_url: str
171        The relative URL for the endpoint (e.g. `'/pipes'`).
172
173    headers: Optional[Dict[str, Any]], default None
174        The headers to use for the request.
175        If `use_token` is `True`, the authorization token will be added to a copy of these headers.
176
177    use_token: bool, default True
178        If `True`, add the authorization token to the headers.
179
180    debug: bool, default False
181        Verbosity toggle.
182
183    kwargs: Any
184        All other keyword arguments are passed to `requests.request`.
185
186    Returns
187    -------
188    A `requests.Reponse` object.
189    """
190    return self.make_request('PATCH', r_url, **kwargs)

Wrapper for requests.patch.

Parameters
  • r_url (str): The relative URL for the endpoint (e.g. '/pipes').
  • headers (Optional[Dict[str, Any]], default None): The headers to use for the request. If use_token is True, the authorization token will be added to a copy of these headers.
  • use_token (bool, default True): If True, add the authorization token to the headers.
  • debug (bool, default False): Verbosity toggle.
  • kwargs (Any): All other keyword arguments are passed to requests.request.
Returns
  • A requests.Reponse object.
def delete(self, r_url: str, **kwargs: Any) -> requests.models.Response:
222def delete(self, r_url: str, **kwargs: Any) -> 'requests.Response':
223    """
224    Wrapper for `requests.delete`.
225
226    Parameters
227    ----------
228    r_url: str
229        The relative URL for the endpoint (e.g. `'/pipes'`).
230
231    headers: Optional[Dict[str, Any]], default None
232        The headers to use for the request.
233        If `use_token` is `True`, the authorization token will be added to a copy of these headers.
234
235    use_token: bool, default True
236        If `True`, add the authorization token to the headers.
237
238    debug: bool, default False
239        Verbosity toggle.
240
241    kwargs: Any
242        All other keyword arguments are passed to `requests.request`.
243
244    Returns
245    -------
246    A `requests.Reponse` object.
247    """
248    return self.make_request('DELETE', r_url, **kwargs)

Wrapper for requests.delete.

Parameters
  • r_url (str): The relative URL for the endpoint (e.g. '/pipes').
  • headers (Optional[Dict[str, Any]], default None): The headers to use for the request. If use_token is True, the authorization token will be added to a copy of these headers.
  • use_token (bool, default True): If True, add the authorization token to the headers.
  • debug (bool, default False): Verbosity toggle.
  • kwargs (Any): All other keyword arguments are passed to requests.request.
Returns
  • A requests.Reponse object.
def wget( self, r_url: str, dest: Union[str, pathlib.Path, NoneType] = None, headers: Optional[Dict[str, Any]] = None, use_token: bool = True, debug: bool = False, **kw: Any) -> pathlib.Path:
251def wget(
252    self,
253    r_url: str,
254    dest: Optional[Union[str, pathlib.Path]] = None,
255    headers: Optional[Dict[str, Any]] = None,
256    use_token: bool = True,
257    debug: bool = False,
258    **kw: Any
259) -> pathlib.Path:
260    """Mimic wget with requests."""
261    from meerschaum.utils.misc import wget
262    if headers is None:
263        headers = {}
264    if use_token:
265        headers.update({'Authorization': f'Bearer {self.token}'})
266    request_url = urllib.parse.urljoin(self.url, r_url)
267    if debug:
268        dprint(
269            f"[{self}] Downloading {request_url}"
270            + (f' to {dest}' if dest is not None else '')
271            + "..."
272        )
273    return wget(request_url, dest=dest, headers=headers, **kw)

Mimic wget with requests.

def get_actions(self):
24def get_actions(self):
25    """Get available actions from the API instance."""
26    return self.get(ACTIONS_ENDPOINT)

Get available actions from the API instance.

def do_action(self, sysargs: List[str]) -> Tuple[bool, str]:
29def do_action(self, sysargs: List[str]) -> SuccessTuple:
30    """
31    Execute a Meerschaum action remotely.
32    """
33    return asyncio.run(self.do_action_async(sysargs))

Execute a Meerschaum action remotely.

async def do_action_async( self, sysargs: List[str], callback_function: Callable[[str], NoneType] = functools.partial(<built-in function print>, end='')) -> Tuple[bool, str]:
36async def do_action_async(
37    self,
38    sysargs: List[str],
39    callback_function: Callable[[str], None] = partial(print, end=''),
40) -> SuccessTuple:
41    """
42    Execute an action as a temporary remote job.
43    """
44    from meerschaum._internal.arguments import remove_api_executor_keys
45    from meerschaum.utils.misc import generate_password
46    sysargs = remove_api_executor_keys(sysargs)
47
48    job_name = TEMP_PREFIX + generate_password(12)
49    job = mrsm.Job(
50        job_name,
51        sysargs,
52        executor_keys=str(self),
53        ### Emit the entire output, not just the last `lines_to_show` (default 30) lines.
54        _properties={'logs': {'lines_to_show': 0}},
55    )
56
57    start_success, start_msg = job.start()
58    if not start_success:
59        return start_success, start_msg
60
61    await job.monitor_logs_async(
62        callback_function=callback_function,
63        stop_on_exit=True,
64        strip_timestamps=True,
65    )
66
67    success, msg = job.result
68    job.delete()
69    return success, msg

Execute an action as a temporary remote job.

def do_action_legacy( self, action: Optional[List[str]] = None, sysargs: Optional[List[str]] = None, debug: bool = False, **kw) -> Tuple[bool, str]:
 72def do_action_legacy(
 73    self,
 74    action: Optional[List[str]] = None,
 75    sysargs: Optional[List[str]] = None,
 76    debug: bool = False,
 77    **kw
 78) -> SuccessTuple:
 79    """
 80    NOTE: This method is deprecated.
 81    Please use `do_action()` or `do_action_async()`.
 82
 83    Execute a Meerschaum action remotely.
 84
 85    If `sysargs` are provided, parse those instead.
 86    Otherwise infer everything from keyword arguments.
 87
 88    Examples
 89    --------
 90    >>> conn = mrsm.get_connector('api:main')
 91    >>> conn.do_action(['show', 'pipes'])
 92    (True, "Success")
 93    >>> conn.do_action(['show', 'arguments'], name='test')
 94    (True, "Success")
 95    """
 96    import sys, json
 97    from meerschaum.utils.debug import dprint
 98    from meerschaum._internal.static import STATIC_CONFIG
 99    from meerschaum.utils.misc import json_serialize_datetime
100    if action is None:
101        action = []
102
103    if sysargs is not None and action and action[0] == '':
104        from meerschaum._internal.arguments import parse_arguments
105        if debug:
106            dprint(f"Parsing sysargs:\n{sysargs}")
107        json_dict = parse_arguments(sysargs)
108    else:
109        json_dict = kw
110        json_dict['action'] = action
111        if 'noask' not in kw:
112            json_dict['noask'] = True
113        if 'yes' not in kw:
114            json_dict['yes'] = True
115        if debug:
116            json_dict['debug'] = debug
117
118    root_action = json_dict['action'][0]
119    del json_dict['action'][0]
120    r_url = f"{STATIC_CONFIG['api']['endpoints']['actions']}/{root_action}"
121    
122    if debug:
123        from meerschaum.utils.formatting import pprint
124        dprint(f"Sending data to '{self.url + r_url}':")
125        pprint(json_dict, stream=sys.stderr)
126
127    response = self.post(
128        r_url,
129        data = json.dumps(json_dict, default=json_serialize_datetime),
130        debug = debug,
131    )
132    try:
133        response_list = json.loads(response.text)
134        if isinstance(response_list, dict) and 'detail' in response_list:
135            return False, response_list['detail']
136    except Exception as e:
137        print(f"Invalid response: {response}")
138        print(e)
139        return False, response.text
140    if debug:
141        dprint(response)
142    try:
143        return response_list[0], response_list[1]
144    except Exception as e:
145        return False, f"Failed to parse result from action '{root_action}'"

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

Execute a Meerschaum action remotely.

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

Examples
>>> conn = mrsm.get_connector('api:main')
>>> conn.do_action(['show', 'pipes'])
(True, "Success")
>>> conn.do_action(['show', 'arguments'], name='test')
(True, "Success")
def get_mrsm_version(self, **kw) -> Optional[str]:
13def get_mrsm_version(self, **kw) -> Optional[str]:
14    """
15    Return the Meerschaum version of the API instance.
16    """
17    from meerschaum._internal.static import STATIC_CONFIG
18    try:
19        j = self.get(
20            STATIC_CONFIG['api']['endpoints']['version'] + '/mrsm',
21            use_token=False,
22            **kw
23        ).json()
24    except Exception:
25        return None
26    if isinstance(j, dict) and 'detail' in j:
27        return None
28    return j

Return the Meerschaum version of the API instance.

def get_chaining_status(self, **kw) -> Optional[bool]:
31def get_chaining_status(self, **kw) -> Optional[bool]:
32    """
33    Fetch the chaining status of the API instance.
34    """
35    from meerschaum._internal.static import STATIC_CONFIG
36    try:
37        response = self.get(
38            STATIC_CONFIG['api']['endpoints']['chaining'],
39            use_token = True,
40            **kw
41        )
42        if not response:
43            return None
44    except Exception:
45        return None
46
47    return response.json()

Fetch the chaining status of the API instance.

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

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

def register_pipe( self, pipe: meerschaum.Pipe, debug: bool = False) -> Tuple[bool, str]:
43def register_pipe(
44    self,
45    pipe: mrsm.Pipe,
46    debug: bool = False
47) -> SuccessTuple:
48    """Submit a POST to the API to register a new Pipe object.
49    Returns a tuple of (success_bool, response_dict).
50    """
51    from meerschaum.utils.debug import dprint
52    r_url = pipe_r_url(pipe)
53    response = self.post(
54        r_url + '/register',
55        json=pipe._attributes.get('parameters', {}),
56        params={'instance_keys': self.get_pipe_instance_keys(pipe)},
57        debug=debug,
58    )
59    if debug:
60        dprint(response.text)
61
62    if not response:
63        return False, response.text
64
65    response_data = response.json()
66    if isinstance(response_data, list):
67        response_tuple = response_data[0], response_data[1]
68    elif 'detail' in response.json():
69        response_tuple = response.__bool__(), response_data['detail']
70    else:
71        response_tuple = response.__bool__(), response.text
72    return response_tuple

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

def fetch_pipes_keys( self, connector_keys: Optional[List[str]] = None, metric_keys: Optional[List[str]] = None, location_keys: Optional[List[str]] = None, tags: Optional[List[str]] = None, params: Optional[Dict[str, Any]] = None, debug: bool = False) -> Union[Dict[Union[int, str], Tuple[str, str, Optional[str], Dict[str, Any]]], List[Tuple[str, str, Optional[str]]]]:
108def fetch_pipes_keys(
109    self,
110    connector_keys: Optional[List[str]] = None,
111    metric_keys: Optional[List[str]] = None,
112    location_keys: Optional[List[str]] = None,
113    tags: Optional[List[str]] = None,
114    params: Optional[Dict[str, Any]] = None,
115    debug: bool = False
116) -> Union[
117    Dict[Union[int, str], Tuple[str, str, Union[str, None], Dict[str, Any]]],
118    List[Tuple[str, str, Union[str, None]]],
119]:
120    """
121    Fetch registered Pipes' keys from the API.
122
123    Parameters
124    ----------
125    connector_keys: Optional[List[str]], default None
126        The connector keys for the query.
127
128    metric_keys: Optional[List[str]], default None
129        The metric keys for the query.
130
131    location_keys: Optional[List[str]], default None
132        The location keys for the query.
133
134    tags: Optional[List[str]], default None
135        A list of tags for the query.
136
137    params: Optional[Dict[str, Any]], default None
138        A parameters dictionary for filtering against the `pipes` table
139        (e.g. `{'connector_keys': 'plugin:foo'}`).
140        Not recommeded to be used.
141
142    debug: bool, default False
143        Verbosity toggle.
144
145    Returns
146    -------
147    A dictionary mapping pipe IDs to key tuples, or a list of key tuples for older servers.
148    """
149    from meerschaum._internal.static import STATIC_CONFIG
150    if connector_keys is None:
151        connector_keys = []
152    if metric_keys is None:
153        metric_keys = []
154    if location_keys is None:
155        location_keys = []
156    if tags is None:
157        tags = []
158
159    r_url = STATIC_CONFIG['api']['endpoints']['pipes'] + '/keys'
160    try:
161        j = self.get(
162            r_url,
163            params={
164                'connector_keys': json.dumps(connector_keys),
165                'metric_keys': json.dumps(metric_keys),
166                'location_keys': json.dumps(location_keys),
167                'tags': json.dumps(tags),
168                'params': json.dumps(params),
169                'instance_keys': self.instance_keys,
170                'as_dict': True,
171            },
172            debug=debug
173        ).json()
174    except Exception as e:
175        import traceback
176        traceback.print_exc()
177        error(str(e))
178
179    if 'detail' in j:
180        error(j['detail'], stack=False)
181
182    if isinstance(j, dict):
183        return {
184            (int(k) if str(k).isdigit() else k): tuple(v)
185            for k, v in j.items()
186        }
187    return [tuple(r) for r in j]

Fetch registered Pipes' keys from the API.

Parameters
  • connector_keys (Optional[List[str]], default None): The connector keys for the query.
  • metric_keys (Optional[List[str]], default None): The metric keys for the query.
  • location_keys (Optional[List[str]], default None): The location keys for the query.
  • tags (Optional[List[str]], default None): A list of tags for the query.
  • params (Optional[Dict[str, Any]], default None): A parameters dictionary for filtering against the pipes table (e.g. {'connector_keys': 'plugin:foo'}). Not recommeded to be used.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A dictionary mapping pipe IDs to key tuples, or a list of key tuples for older servers.
def edit_pipe( self, pipe: meerschaum.Pipe, patch: bool = False, debug: bool = False) -> Tuple[bool, str]:
 75def edit_pipe(
 76    self,
 77    pipe: mrsm.Pipe,
 78    patch: bool = False,
 79    debug: bool = False,
 80) -> SuccessTuple:
 81    """Submit a PATCH to the API to edit an existing Pipe object.
 82    Returns a tuple of (success_bool, response_dict).
 83    """
 84    from meerschaum.utils.debug import dprint
 85    ### NOTE: if `parameters` is supplied in the Pipe constructor,
 86    ###       then `pipe.parameters` will exist and not be fetched from the database.
 87    r_url = pipe_r_url(pipe)
 88    response = self.patch(
 89        r_url + '/edit',
 90        params={'patch': patch, 'instance_keys': self.get_pipe_instance_keys(pipe)},
 91        json=pipe.get_parameters(apply_symlinks=False),
 92        debug=debug,
 93    )
 94    if debug:
 95        dprint(response.text)
 96
 97    response_data = response.json()
 98
 99    if isinstance(response.json(), list):
100        response_tuple = response_data[0], response_data[1]
101    elif 'detail' in response.json():
102        response_tuple = response.__bool__(), response_data['detail']
103    else:
104        response_tuple = response.__bool__(), response.text
105    return response_tuple

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

def sync_pipe( self, pipe: meerschaum.Pipe, df: "Optional[Union['pd.DataFrame', Dict[Any, Any], str]]" = None, chunksize: Optional[int] = -1, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
190def sync_pipe(
191    self,
192    pipe: mrsm.Pipe,
193    df: Optional[Union['pd.DataFrame', Dict[Any, Any], str]] = None,
194    chunksize: Optional[int] = -1,
195    debug: bool = False,
196    **kw: Any
197) -> SuccessTuple:
198    """Sync a DataFrame into a Pipe."""
199    from decimal import Decimal
200    from meerschaum.utils.debug import dprint
201    from meerschaum.utils.dtypes import json_serialize_value
202    from meerschaum.utils.misc import items_str, interval_str
203    from meerschaum.config import get_config
204    from meerschaum.utils.packages import attempt_import
205    from meerschaum.utils.dataframe import get_special_cols, to_json
206    begin = time.perf_counter()
207    more_itertools = attempt_import('more_itertools')
208    if df is None:
209        msg = f"DataFrame is `None`. Cannot sync {pipe}."
210        return False, msg
211
212    def get_json_str(c):
213        if isinstance(c, str):
214            return c
215        if isinstance(c, (dict, list, tuple)):
216            return json.dumps(c, default=json_serialize_value)
217        return to_json(c, orient='columns', geometry_format='wkb_hex')
218
219    df = json.loads(df) if isinstance(df, str) else df
220
221    _chunksize: Optional[int] = (1 if chunksize is None else (
222        get_config('system', 'connectors', 'sql', 'chunksize') if chunksize == -1
223        else chunksize
224    ))
225    keys: List[str] = list(df.columns)
226    chunks = []
227    if hasattr(df, 'index'):
228        df = df.reset_index(drop=True)
229        is_dask = 'dask' in df.__module__
230        chunks = (
231            (df.iloc[i] for i in more_itertools.chunked(df.index, _chunksize))
232            if not is_dask
233            else [partition.compute() for partition in df.partitions]
234        )
235
236    elif isinstance(df, dict):
237        ### `_chunks` is a dict of lists of dicts.
238        ### e.g. {'a' : [ {'a':[1, 2]}, {'a':[3, 4]} ] }
239        _chunks = {k: [] for k in keys}
240        for k in keys:
241            chunk_iter = more_itertools.chunked(df[k], _chunksize)
242            for l in chunk_iter:
243                _chunks[k].append({k: l})
244
245        ### `chunks` is a list of dicts (e.g. orient by rows in pandas JSON).
246        for k, l in _chunks.items():
247            for i, c in enumerate(l):
248                try:
249                    chunks[i].update(c)
250                except IndexError:
251                    chunks.append(c)
252    elif isinstance(df, list):
253        chunks = (df[i] for i in more_itertools.chunked(df, _chunksize))
254
255    ### Send columns in case the user has defined them locally.
256    request_params = kw.copy()
257    if pipe.columns:
258        request_params['columns'] = json.dumps(pipe.columns)
259    request_params['instance_keys'] = self.get_pipe_instance_keys(pipe)
260    r_url = pipe_r_url(pipe) + '/data'
261
262    rowcount = 0
263    num_success_chunks = 0
264    for i, c in enumerate(chunks):
265        if debug:
266            dprint(f"[{self}] Posting chunk {i} to {r_url}...")
267        if len(c) == 0:
268            if debug:
269                dprint(f"[{self}] Skipping empty chunk...")
270            continue
271        json_str = get_json_str(c)
272
273        try:
274            response = self.post(
275                r_url,
276                params=request_params,
277                data=json_str,
278                debug=debug,
279            )
280        except Exception as e:
281            msg = f"Failed to post a chunk to {pipe}:\n{e}"
282            warn(msg)
283            return False, msg
284            
285        if not response:
286            return False, f"Failed to sync a chunk:\n{response.text}"
287
288        try:
289            j = json.loads(response.text)
290        except Exception as e:
291            return False, f"Failed to parse response from syncing {pipe}:\n{e}"
292
293        if isinstance(j, dict) and 'detail' in j:
294            return False, j['detail']
295
296        try:
297            j = tuple(j)
298        except Exception:
299            return False, response.text
300
301        if debug:
302            dprint("Received response: " + str(j))
303        if not j[0]:
304            return j
305
306        rowcount += len(c)
307        num_success_chunks += 1
308
309    self.delete_pipe_cache(pipe, debug=debug)
310    success_tuple = True, (
311        f"It took {interval_str(timedelta(seconds=(time.perf_counter() - begin)))} "
312        + f"to sync {rowcount:,} row"
313        + ('s' if rowcount != 1 else '')
314        + f" across {num_success_chunks:,} chunk" + ('s' if num_success_chunks != 1 else '') +
315        f" to {pipe}."
316    )
317    return success_tuple

Sync a DataFrame into a Pipe.

def delete_pipe( self, pipe: Optional[meerschaum.Pipe] = None, debug: bool = False) -> Tuple[bool, str]:
342def delete_pipe(
343    self,
344    pipe: Optional[mrsm.Pipe] = None,
345    debug: bool = False,
346) -> SuccessTuple:
347    """Delete a Pipe and drop its table."""
348    if pipe is None:
349        error("Pipe cannot be None.")
350    r_url = pipe_r_url(pipe)
351    response = self.delete(
352        r_url + '/delete',
353        params={'instance_keys': self.get_pipe_instance_keys(pipe)},
354        debug=debug,
355    )
356    if debug:
357        dprint(response.text)
358
359    response_data = response.json()
360    if isinstance(response.json(), list):
361        response_tuple = response_data[0], response_data[1]
362    elif 'detail' in response.json():
363        response_tuple = response.__bool__(), response_data['detail']
364    else:
365        response_tuple = response.__bool__(), response.text
366    return response_tuple

Delete a Pipe and drop its table.

def delete_pipe_cache( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
320def delete_pipe_cache(
321    self,
322    pipe: mrsm.Pipe,
323    debug: bool = False,
324    **kw: Any
325) -> SuccessTuple:
326    """Invalidate the server-side cache for a pipe."""
327    r_url = pipe_r_url(pipe)
328    response = self.delete(
329        r_url + '/cache',
330        params={'instance_keys': self.get_pipe_instance_keys(pipe)},
331        debug=debug,
332    )
333    if not response.ok:
334        return False, f"Failed to invalidate cache for {pipe}: {response.text}"
335    try:
336        data = response.json()
337        return tuple(data) if isinstance(data, list) else (response.ok, response.text)
338    except Exception:
339        return response.ok, response.text

Invalidate the server-side cache for a pipe.

def get_pipe_data( self, pipe: meerschaum.Pipe, select_columns: Optional[List[str]] = None, omit_columns: Optional[List[str]] = None, begin: Union[str, datetime.datetime, int, NoneType] = None, end: Union[str, datetime.datetime, int, NoneType] = None, params: Optional[Dict[str, Any]] = None, as_chunks: bool = False, debug: bool = False, **kw: Any) -> Optional[pandas.DataFrame]:
369def get_pipe_data(
370    self,
371    pipe: mrsm.Pipe,
372    select_columns: Optional[List[str]] = None,
373    omit_columns: Optional[List[str]] = None,
374    begin: Union[str, datetime, int, None] = None,
375    end: Union[str, datetime, int, None] = None,
376    params: Optional[Dict[str, Any]] = None,
377    as_chunks: bool = False,
378    debug: bool = False,
379    **kw: Any
380) -> Union[pandas.DataFrame, None]:
381    """Fetch data from the API."""
382    r_url = pipe_r_url(pipe)
383    while True:
384        try:
385            response = self.get(
386                r_url + "/data",
387                params={
388                    'select_columns': json.dumps(select_columns),
389                    'omit_columns': json.dumps(omit_columns),
390                    'begin': begin,
391                    'end': end,
392                    'params': json.dumps(params, default=str),
393                    'instance': self.get_pipe_instance_keys(pipe),
394                    'as_chunks': as_chunks,
395                },
396                debug=debug
397            )
398            if not response.ok:
399                return None
400            j = response.json()
401        except Exception as e:
402            warn(f"Failed to get data for {pipe}:\n{e}")
403            return None
404        if isinstance(j, dict) and 'detail' in j:
405            return False, j['detail']
406        break
407
408    from meerschaum.utils.dataframe import parse_df_datetimes, add_missing_cols_to_df
409    from meerschaum.utils.dtypes import are_dtypes_equal
410    try:
411        df = parse_df_datetimes(
412            j,
413            ignore_cols=[
414                col
415                for col, dtype in pipe.dtypes.items()
416                if not are_dtypes_equal(str(dtype), 'datetime')
417            ],
418            strip_timezone=(pipe.tzinfo is None),
419            debug=debug,
420        )
421    except Exception as e:
422        warn(f"Failed to parse response for {pipe}:\n{e}")
423        return None
424
425    if len(df.columns) == 0:
426        return add_missing_cols_to_df(df, pipe.dtypes)
427
428    return df

Fetch data from the API.

def get_pipe_id( self, pipe: meerschaum.Pipe, debug: bool = False) -> Union[int, str, NoneType]:
431def get_pipe_id(
432    self,
433    pipe: mrsm.Pipe,
434    debug: bool = False,
435) -> Union[int, str, None]:
436    """Get a Pipe's ID from the API."""
437    from meerschaum.utils.misc import is_int
438    r_url = pipe_r_url(pipe)
439    response = self.get(
440        r_url + '/id',
441        params={
442            'instance': self.get_pipe_instance_keys(pipe),
443        },
444        debug=debug,
445    )
446    if debug:
447        dprint(f"Got pipe ID: {response.text}")
448    try:
449        if is_int(response.text):
450            return int(response.text)
451        if response.text and response.text[0] != '{':
452            return response.text
453    except Exception as e:
454        warn(f"Failed to get the ID for {pipe}:\n{e}")
455    return None

Get a Pipe's ID from the API.

def get_pipe_attributes( self, pipe: meerschaum.Pipe, debug: bool = False) -> Dict[str, Any]:
458def get_pipe_attributes(
459    self,
460    pipe: mrsm.Pipe,
461    debug: bool = False,
462) -> Dict[str, Any]:
463    """Get a Pipe's attributes from the API
464
465    Parameters
466    ----------
467    pipe: meerschaum.Pipe
468        The pipe whose attributes we are fetching.
469        
470    Returns
471    -------
472    A dictionary of a pipe's attributes.
473    If the pipe does not exist, return an empty dictionary.
474    """
475    r_url = pipe_r_url(pipe)
476    response = self.get(
477        r_url + '/attributes',
478        params={
479            'instance': self.get_pipe_instance_keys(pipe),
480        },
481        debug=debug
482    )
483    response.raise_for_status()
484    attributes = json.loads(response.text)
485    if not isinstance(attributes, dict) or (
486        attributes and not isinstance(attributes.get('parameters'), dict)
487    ):
488        raise ValueError(f"Invalid attributes response for {pipe}.")
489    return attributes

Get a Pipe's attributes from the API

Parameters
Returns
  • A dictionary of a pipe's attributes.
  • If the pipe does not exist, return an empty dictionary.
def get_sync_time( self, pipe: meerschaum.Pipe, params: Optional[Dict[str, Any]] = None, newest: bool = True, debug: bool = False) -> Union[datetime.datetime, int, NoneType]:
492def get_sync_time(
493    self,
494    pipe: mrsm.Pipe,
495    params: Optional[Dict[str, Any]] = None,
496    newest: bool = True,
497    debug: bool = False,
498) -> Union[datetime, int, None]:
499    """Get a Pipe's most recent datetime value from the API.
500
501    Parameters
502    ----------
503    pipe: meerschaum.Pipe
504        The pipe to select from.
505
506    params: Optional[Dict[str, Any]], default None
507        Optional params dictionary to build the WHERE clause.
508
509    newest: bool, default True
510        If `True`, get the most recent datetime (honoring `params`).
511        If `False`, get the oldest datetime (ASC instead of DESC).
512
513    Returns
514    -------
515    The most recent (or oldest if `newest` is `False`) datetime of a pipe,
516    rounded down to the closest minute.
517    """
518    from meerschaum.utils.misc import is_int
519    from meerschaum.utils.warnings import warn
520    r_url = pipe_r_url(pipe)
521    response = self.get(
522        r_url + '/sync_time',
523        json=params,
524        params={
525            'instance': self.get_pipe_instance_keys(pipe),
526            'newest': newest,
527            'debug': debug,
528        },
529        debug=debug,
530    )
531    if not response:
532        warn(f"Failed to get the sync time for {pipe}:\n" + response.text)
533        return None
534
535    j = response.json()
536    if j is None:
537        dt = None
538    else:
539        try:
540            dt = (
541                datetime.fromisoformat(j)
542                if not is_int(j)
543                else int(j)
544            )
545        except Exception as e:
546            warn(f"Failed to parse the sync time '{j}' for {pipe}:\n{e}")
547            dt = None
548    return dt

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

Parameters
  • pipe (meerschaum.Pipe): The pipe to select from.
  • params (Optional[Dict[str, Any]], default None): Optional params dictionary to build the WHERE clause.
  • newest (bool, default True): If True, get the most recent datetime (honoring params). If False, get the oldest datetime (ASC instead of DESC).
Returns
  • The most recent (or oldest if newest is False) datetime of a pipe,
  • rounded down to the closest minute.
def pipe_exists(self, pipe: meerschaum.Pipe, debug: bool = False) -> bool:
551def pipe_exists(
552    self,
553    pipe: mrsm.Pipe,
554    debug: bool = False
555) -> bool:
556    """Check the API to see if a Pipe exists.
557
558    Parameters
559    ----------
560    pipe: 'meerschaum.Pipe'
561        The pipe which were are querying.
562        
563    Returns
564    -------
565    A bool indicating whether a pipe's underlying table exists.
566    """
567    from meerschaum.utils.debug import dprint
568    from meerschaum.utils.warnings import warn
569    r_url = pipe_r_url(pipe)
570    response = self.get(
571        r_url + '/exists',
572        params={
573            'instance': self.get_pipe_instance_keys(pipe),
574        },
575        debug=debug,
576    )
577    if not response:
578        warn(f"Failed to check if {pipe} exists:\n{response.text}")
579        return False
580    if debug:
581        dprint("Received response: " + str(response.text))
582    j = response.json()
583    if isinstance(j, dict) and 'detail' in j:
584        warn(j['detail'])
585    return j

Check the API to see if a Pipe exists.

Parameters
Returns
  • A bool indicating whether a pipe's underlying table exists.
def create_metadata(self, debug: bool = False) -> bool:
588def create_metadata(
589    self,
590    debug: bool = False
591) -> bool:
592    """Create metadata tables.
593
594    Returns
595    -------
596    A bool indicating success.
597    """
598    from meerschaum.utils.debug import dprint
599    from meerschaum._internal.static import STATIC_CONFIG
600    r_url = STATIC_CONFIG['api']['endpoints']['metadata']
601    response = self.post(r_url, debug=debug)
602    if debug:
603        dprint("Create metadata response: {response.text}")
604    try:
605        _ = json.loads(response.text)
606    except Exception as e:
607        warn(f"Failed to create metadata on {self}:\n{e}")
608    return False

Create metadata tables.

Returns
  • A bool indicating success.
def get_pipe_rowcount( self, pipe: meerschaum.Pipe, begin: Union[str, datetime.datetime, int, NoneType] = None, end: Union[str, datetime.datetime, int, NoneType] = None, params: Optional[Dict[str, Any]] = None, remote: bool = False, debug: bool = False) -> int:
611def get_pipe_rowcount(
612    self,
613    pipe: mrsm.Pipe,
614    begin: Union[str, datetime, int, None] = None,
615    end: Union[str, datetime, int, None] = None,
616    params: Optional[Dict[str, Any]] = None,
617    remote: bool = False,
618    debug: bool = False,
619) -> int:
620    """Get a pipe's row count from the API.
621
622    Parameters
623    ----------
624    pipe: 'meerschaum.Pipe':
625        The pipe whose row count we are counting.
626        
627    begin: Union[str, datetime, int, None], default None
628        If provided, bound the count by this datetime.
629
630    end: Union[str, datetime, int, None], default None
631        If provided, bound the count by this datetime.
632
633    params: Optional[Dict[str, Any]], default None
634        If provided, bound the count by these parameters.
635
636    remote: bool, default False
637        If `True`, return the rowcount for the fetch definition.
638
639    Returns
640    -------
641    The number of rows in the pipe's table, bound the given parameters.
642    If the table does not exist, return 0.
643    """
644    r_url = pipe_r_url(pipe)
645    response = self.get(
646        r_url + "/rowcount",
647        json = params,
648        params = {
649            'begin': begin,
650            'end': end,
651            'remote': remote,
652            'instance': self.get_pipe_instance_keys(pipe),
653        },
654        debug = debug
655    )
656    if not response:
657        warn(f"Failed to get the rowcount for {pipe}:\n{response.text}")
658        return 0
659    try:
660        return int(json.loads(response.text))
661    except Exception as e:
662        warn(f"Failed to get the rowcount for {pipe}:\n{e}")
663    return 0

Get a pipe's row count from the API.

Parameters
  • pipe ('meerschaum.Pipe':): The pipe whose row count we are counting.
  • begin (Union[str, datetime, int, None], default None): If provided, bound the count by this datetime.
  • end (Union[str, datetime, int, None], default None): If provided, bound the count by this datetime.
  • params (Optional[Dict[str, Any]], default None): If provided, bound the count by these parameters.
  • remote (bool, default False): If True, return the rowcount for the fetch definition.
Returns
  • The number of rows in the pipe's table, bound the given parameters.
  • If the table does not exist, return 0.
def drop_pipe( self, pipe: meerschaum.Pipe, debug: bool = False) -> Tuple[bool, str]:
666def drop_pipe(
667    self,
668    pipe: mrsm.Pipe,
669    debug: bool = False
670) -> SuccessTuple:
671    """
672    Drop a pipe's table but maintain its registration.
673
674    Parameters
675    ----------
676    pipe: meerschaum.Pipe:
677        The pipe to be dropped.
678        
679    Returns
680    -------
681    A success tuple (bool, str).
682    """
683    from meerschaum.utils.warnings import error
684    from meerschaum.utils.debug import dprint
685    if pipe is None:
686        error("Pipe cannot be None.")
687    r_url = pipe_r_url(pipe)
688    response = self.delete(
689        r_url + '/drop',
690        params={
691            'instance': self.get_pipe_instance_keys(pipe),
692        },
693        debug=debug,
694    )
695    if debug:
696        dprint(response.text)
697
698    try:
699        data = response.json()
700    except Exception as e:
701        return False, f"Failed to drop {pipe}."
702
703    if isinstance(data, list):
704        response_tuple = data[0], data[1]
705    elif 'detail' in response.json():
706        response_tuple = response.__bool__(), data['detail']
707    else:
708        response_tuple = response.__bool__(), response.text
709
710    return response_tuple

Drop a pipe's table but maintain its registration.

Parameters
Returns
  • A success tuple (bool, str).
def clear_pipe( self, pipe: meerschaum.Pipe, begin: Union[str, datetime.datetime, int, NoneType] = None, end: Union[str, datetime.datetime, int, NoneType] = None, params: Optional[Dict[str, Any]] = None, debug: bool = False, **kw) -> Tuple[bool, str]:
713def clear_pipe(
714    self,
715    pipe: mrsm.Pipe,
716    begin: Union[str, datetime, int, None] = None,
717    end: Union[str, datetime, int, None] = None,
718    params: Optional[Dict[str, Any]] = None,
719    debug: bool = False,
720    **kw
721) -> SuccessTuple:
722    """
723    Delete rows in a pipe's table.
724
725    Parameters
726    ----------
727    pipe: meerschaum.Pipe
728        The pipe with rows to be deleted.
729        
730    Returns
731    -------
732    A success tuple.
733    """
734    r_url = pipe_r_url(pipe)
735    response = self.delete(
736        r_url + '/clear',
737        params={
738            'begin': begin,
739            'end': end,
740            'params': json.dumps(params),
741            'instance': self.get_pipe_instance_keys(pipe),
742        },
743        debug=debug,
744    )
745    if debug:
746        dprint(response.text)
747
748    try:
749        data = response.json()
750    except Exception as e:
751        return False, f"Failed to clear {pipe} with constraints {begin=}, {end=}, {params=}."
752
753    if isinstance(data, list):
754        response_tuple = data[0], data[1]
755    elif 'detail' in response.json():
756        response_tuple = response.__bool__(), data['detail']
757    else:
758        response_tuple = response.__bool__(), response.text
759
760    return response_tuple

Delete rows in a pipe's table.

Parameters
Returns
  • A success tuple.
def get_pipe_columns_types( self, pipe: meerschaum.Pipe, debug: bool = False) -> Optional[Dict[str, str]]:
763def get_pipe_columns_types(
764    self,
765    pipe: mrsm.Pipe,
766    debug: bool = False,
767) -> Union[Dict[str, str], None]:
768    """
769    Fetch the columns and types of the pipe's table.
770
771    Parameters
772    ----------
773    pipe: meerschaum.Pipe
774        The pipe whose columns to be queried.
775
776    Returns
777    -------
778    A dictionary mapping column names to their database types.
779
780    Examples
781    --------
782    >>> {
783    ...   'dt': 'TIMESTAMP WITHOUT TIMEZONE',
784    ...   'id': 'BIGINT',
785    ...   'val': 'DOUBLE PRECISION',
786    ... }
787    >>>
788    """
789    r_url = pipe_r_url(pipe) + '/columns/types'
790    response = self.get(
791        r_url,
792        params={
793            'instance': self.get_pipe_instance_keys(pipe),
794        },
795        debug=debug,
796    )
797    j = response.json()
798    if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
799        warn(j['detail'])
800        return None
801    if not isinstance(j, dict):
802        warn(response.text)
803        return None
804    return j

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

Parameters
Returns
  • A dictionary mapping column names to their database types.
Examples
>>> {
...   'dt': 'TIMESTAMP WITHOUT TIMEZONE',
...   'id': 'BIGINT',
...   'val': 'DOUBLE PRECISION',
... }
>>>
def get_pipe_columns_indices( self, pipe: meerschaum.Pipe, debug: bool = False) -> Optional[Dict[str, str]]:
807def get_pipe_columns_indices(
808    self,
809    pipe: mrsm.Pipe,
810    debug: bool = False,
811) -> Union[Dict[str, str], None]:
812    """
813    Fetch the index information for a pipe.
814
815    Parameters
816    ----------
817    pipe: mrsm.Pipe
818        The pipe whose columns to be queried.
819
820    Returns
821    -------
822    A dictionary mapping column names to a list of associated index information.
823    """
824    r_url = pipe_r_url(pipe) + '/columns/indices'
825    response = self.get(
826        r_url,
827        params={
828            'instance': self.get_pipe_instance_keys(pipe),
829        },
830        debug=debug,
831    )
832    j = response.json()
833    if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
834        warn(j['detail'])
835        return None
836    if not isinstance(j, dict):
837        warn(response.text)
838        return None
839    return j

Fetch the index information for a pipe.

Parameters
  • pipe (mrsm.Pipe): The pipe whose columns to be queried.
Returns
  • A dictionary mapping column names to a list of associated index information.
def get_pipe_docs( self, pipe: meerschaum.Pipe, select_columns: Optional[List[str]] = None, omit_columns: Optional[List[str]] = None, begin: Union[str, datetime.datetime, int, NoneType] = None, end: Union[str, datetime.datetime, int, NoneType] = None, params: Optional[Dict[str, Any]] = None, order: str = 'asc', limit: Optional[int] = None, debug: bool = False, **kw: Any) -> List[Dict[str, Any]]:
842def get_pipe_docs(
843    self,
844    pipe: mrsm.Pipe,
845    select_columns: Optional[List[str]] = None,
846    omit_columns: Optional[List[str]] = None,
847    begin: Union[str, datetime, int, None] = None,
848    end: Union[str, datetime, int, None] = None,
849    params: Optional[Dict[str, Any]] = None,
850    order: str = 'asc',
851    limit: Optional[int] = None,
852    debug: bool = False,
853    **kw: Any
854) -> List[Dict[str, Any]]:
855    """Fetch a pipe's data as a list of documents from the API."""
856    r_url = pipe_r_url(pipe)
857    try:
858        response = self.get(
859            r_url + "/docs",
860            params={
861                'select_columns': json.dumps(select_columns),
862                'omit_columns': json.dumps(omit_columns),
863                'begin': begin,
864                'end': end,
865                'params': json.dumps(params, default=str),
866                'order': order,
867                'limit': limit,
868                'instance_keys': self.get_pipe_instance_keys(pipe),
869            },
870            debug=debug,
871        )
872        if not response.ok:
873            warn(f"Failed to get docs for {pipe}:\n{response.text}")
874            return []
875        j = response.json()
876        if isinstance(j, list):
877            return j
878        return []
879    except Exception as e:
880        warn(f"Failed to get docs for {pipe}:\n{e}")
881        return []

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

def get_pipe_size( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> Optional[int]:
884def get_pipe_size(
885    self,
886    pipe: mrsm.Pipe,
887    debug: bool = False,
888    **kw: Any
889) -> Union[int, None]:
890    """
891    Return the on-disk size of a pipe's target table in bytes via the API.
892
893    Parameters
894    ----------
895    pipe: mrsm.Pipe
896        The pipe whose target table size to measure.
897
898    Returns
899    -------
900    An `int` of the number of bytes occupied by the target table,
901    or `None` if the size could not be determined.
902    """
903    r_url = pipe_r_url(pipe) + '/size'
904    response = self.get(
905        r_url,
906        params={
907            'instance': self.get_pipe_instance_keys(pipe),
908        },
909        debug=debug,
910    )
911    if not response:
912        warn(f"Failed to get the size for {pipe}:\n{response.text}")
913        return None
914    try:
915        j = json.loads(response.text)
916    except Exception as e:
917        warn(f"Failed to parse the size for {pipe}:\n{e}")
918        return None
919    if j is None:
920        return None
921    if isinstance(j, dict) and 'detail' in j:
922        warn(j['detail'])
923        return None
924    try:
925        return int(j)
926    except Exception:
927        return None

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

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table size to measure.
Returns
  • An int of the number of bytes occupied by the target table,
  • or None if the size could not be determined.
def compress_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
930def compress_pipe(
931    self,
932    pipe: mrsm.Pipe,
933    debug: bool = False,
934    **kw: Any
935) -> SuccessTuple:
936    """
937    Compress a pipe's target table via the API.
938
939    Parameters
940    ----------
941    pipe: mrsm.Pipe
942        The pipe whose target table to compress.
943
944    Returns
945    -------
946    A `SuccessTuple` indicating success.
947    """
948    r_url = pipe_r_url(pipe) + '/compress'
949    response = self.post(
950        r_url,
951        params={
952            'instance_keys': self.get_pipe_instance_keys(pipe),
953        },
954        debug=debug,
955    )
956    if debug:
957        dprint(response.text)
958
959    try:
960        data = response.json()
961    except Exception:
962        return False, f"Failed to compress {pipe}."
963
964    if isinstance(data, list):
965        return data[0], data[1]
966    if isinstance(data, dict) and 'detail' in data:
967        return response.__bool__(), data['detail']
968    return response.__bool__(), response.text

Compress a pipe's target table via the API.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to compress.
Returns
  • A SuccessTuple indicating success.
def decompress_pipe( self, pipe: meerschaum.Pipe, no_policy: bool = False, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
 971def decompress_pipe(
 972    self,
 973    pipe: mrsm.Pipe,
 974    no_policy: bool = False,
 975    debug: bool = False,
 976    **kw: Any
 977) -> SuccessTuple:
 978    """
 979    Decompress a pipe's target table via the API, the inverse of `compress_pipe()`.
 980
 981    Parameters
 982    ----------
 983    pipe: mrsm.Pipe
 984        The pipe whose target table to decompress.
 985
 986    no_policy: bool, default False
 987        If `True`, decompress existing data now but leave the compression policy in place.
 988
 989    Returns
 990    -------
 991    A `SuccessTuple` indicating success.
 992    """
 993    r_url = pipe_r_url(pipe) + '/decompress'
 994    response = self.post(
 995        r_url,
 996        params={
 997            'instance_keys': self.get_pipe_instance_keys(pipe),
 998            'no_policy': no_policy,
 999        },
1000        debug=debug,
1001    )
1002    if debug:
1003        dprint(response.text)
1004
1005    try:
1006        data = response.json()
1007    except Exception:
1008        return False, f"Failed to decompress {pipe}."
1009
1010    if isinstance(data, list):
1011        return data[0], data[1]
1012    if isinstance(data, dict) and 'detail' in data:
1013        return response.__bool__(), data['detail']
1014    return response.__bool__(), response.text

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

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to decompress.
  • no_policy (bool, default False): If True, decompress existing data now but leave the compression policy in place.
Returns
  • A SuccessTuple indicating success.
def vacuum_pipe( self, pipe: meerschaum.Pipe, full: bool = False, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
1017def vacuum_pipe(
1018    self,
1019    pipe: mrsm.Pipe,
1020    full: bool = False,
1021    debug: bool = False,
1022    **kw: Any
1023) -> SuccessTuple:
1024    """
1025    Vacuum a pipe's target table via the API.
1026
1027    Parameters
1028    ----------
1029    pipe: mrsm.Pipe
1030        The pipe whose target table to vacuum.
1031
1032    full: bool, default False
1033        If `True`, run `VACUUM FULL` (PostgreSQL family only).
1034
1035    Returns
1036    -------
1037    A `SuccessTuple` indicating success.
1038    """
1039    r_url = pipe_r_url(pipe) + '/vacuum'
1040    response = self.post(
1041        r_url,
1042        params={
1043            'instance_keys': self.get_pipe_instance_keys(pipe),
1044            'full': full,
1045        },
1046        debug=debug,
1047    )
1048    if debug:
1049        dprint(response.text)
1050
1051    try:
1052        data = response.json()
1053    except Exception:
1054        return False, f"Failed to vacuum {pipe}."
1055
1056    if isinstance(data, list):
1057        return data[0], data[1]
1058    if isinstance(data, dict) and 'detail' in data:
1059        return response.__bool__(), data['detail']
1060    return response.__bool__(), response.text

Vacuum a pipe's target table via the API.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to vacuum.
  • full (bool, default False): If True, run VACUUM FULL (PostgreSQL family only).
Returns
  • A SuccessTuple indicating success.
def analyze_pipe( self, pipe: meerschaum.Pipe, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
1109def analyze_pipe(
1110    self,
1111    pipe: mrsm.Pipe,
1112    debug: bool = False,
1113    **kw: Any
1114) -> SuccessTuple:
1115    """
1116    Analyze a pipe's target table via the API.
1117
1118    Parameters
1119    ----------
1120    pipe: mrsm.Pipe
1121        The pipe whose target table to analyze.
1122
1123    Returns
1124    -------
1125    A `SuccessTuple` indicating success.
1126    """
1127    r_url = pipe_r_url(pipe) + '/analyze'
1128    response = self.post(
1129        r_url,
1130        params={
1131            'instance_keys': self.get_pipe_instance_keys(pipe),
1132        },
1133        debug=debug,
1134    )
1135    if debug:
1136        dprint(response.text)
1137
1138    try:
1139        data = response.json()
1140    except Exception:
1141        return False, f"Failed to analyze {pipe}."
1142
1143    if isinstance(data, list):
1144        return data[0], data[1]
1145    if isinstance(data, dict) and 'detail' in data:
1146        return response.__bool__(), data['detail']
1147    return response.__bool__(), response.text

Analyze a pipe's target table via the API.

Parameters
  • pipe (mrsm.Pipe): The pipe whose target table to analyze.
Returns
  • A SuccessTuple indicating success.
def partition_pipe( self, pipe: meerschaum.Pipe, chunk_minutes: Optional[int] = None, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
1063def partition_pipe(
1064    self,
1065    pipe: mrsm.Pipe,
1066    chunk_minutes: Optional[int] = None,
1067    debug: bool = False,
1068    **kw: Any
1069) -> SuccessTuple:
1070    """
1071    Repartition a pipe's target table to a new chunk width via the API.
1072
1073    Parameters
1074    ----------
1075    pipe: mrsm.Pipe
1076        The partitioned pipe whose target table to repartition.
1077
1078    chunk_minutes: Optional[int], default None
1079        The new partition width in minutes. Defaults to the pipe's `verify.chunk_minutes`.
1080
1081    Returns
1082    -------
1083    A `SuccessTuple` indicating success.
1084    """
1085    r_url = pipe_r_url(pipe) + '/partition'
1086    response = self.post(
1087        r_url,
1088        params={
1089            'instance_keys': self.get_pipe_instance_keys(pipe),
1090            **({'chunk_minutes': chunk_minutes} if chunk_minutes is not None else {}),
1091        },
1092        debug=debug,
1093    )
1094    if debug:
1095        dprint(response.text)
1096
1097    try:
1098        data = response.json()
1099    except Exception:
1100        return False, f"Failed to repartition {pipe}."
1101
1102    if isinstance(data, list):
1103        return data[0], data[1]
1104    if isinstance(data, dict) and 'detail' in data:
1105        return response.__bool__(), data['detail']
1106    return response.__bool__(), response.text

Repartition a pipe's target table to a new chunk width via the API.

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

Get the Pipe data from the remote Pipe.

def register_plugin( self, plugin: meerschaum.Plugin, make_archive: bool = True, debug: bool = False) -> Tuple[bool, str]:
24def register_plugin(
25    self,
26    plugin: mrsm.core.Plugin,
27    make_archive: bool = True,
28    debug: bool = False,
29) -> SuccessTuple:
30    """Register a plugin and upload its archive."""
31    import json
32    archive_path = plugin.make_tar(debug=debug) if make_archive else plugin.archive_path
33    file_pointer = open(archive_path, 'rb')
34    files = {'archive': file_pointer}
35    metadata = {
36        'version': plugin.version,
37        'attributes': json.dumps(plugin.attributes),
38    }
39    r_url = plugin_r_url(plugin)
40    try:
41        response = self.post(r_url, files=files, params=metadata, debug=debug)
42    except Exception:
43        return False, f"Failed to register plugin '{plugin}'."
44    finally:
45        file_pointer.close()
46
47    try:
48        success, msg = json.loads(response.text)
49    except Exception:
50        return False, response.text
51
52    return success, msg

Register a plugin and upload its archive.

def install_plugin( self, name: str, skip_deps: bool = False, force: bool = False, debug: bool = False) -> Tuple[bool, str]:
55def install_plugin(
56    self,
57    name: str,
58    skip_deps: bool = False,
59    force: bool = False,
60    debug: bool = False
61) -> SuccessTuple:
62    """Download and attempt to install a plugin from the API."""
63    import os
64    import pathlib
65    import json
66    from meerschaum.core import Plugin
67    import meerschaum.config.paths as paths
68    from meerschaum.utils.debug import dprint
69    from meerschaum.utils.packages import attempt_import
70    binaryornot_check = attempt_import('binaryornot.check', lazy=False)
71    r_url = plugin_r_url(name)
72    dest = pathlib.Path(os.path.join(paths.PLUGINS_TEMP_RESOURCES_PATH, name + '.tar.gz'))
73    if debug:
74        dprint(f"Fetching from '{self.url + r_url}' to '{dest}'...")
75    archive_path = self.wget(r_url, dest, debug=debug) 
76    is_binary = binaryornot_check.is_binary(str(archive_path))
77    if not is_binary:
78        fail_msg = f"Failed to download binary for plugin '{name}'."
79        try:
80            with open(archive_path, 'r') as f:
81                j = json.load(f)
82            if isinstance(j, list):
83                success, msg = tuple(j)
84            elif isinstance(j, dict) and 'detail' in j:
85                success, msg = False, fail_msg
86        except Exception:
87            success, msg = False, fail_msg
88        return success, msg
89    plugin = Plugin(name, archive_path=archive_path, repo_connector=self)
90    return plugin.install(skip_deps=skip_deps, force=force, debug=debug)

Download and attempt to install a plugin from the API.

def delete_plugin( self, plugin: meerschaum.Plugin, debug: bool = False) -> Tuple[bool, str]:
177def delete_plugin(
178    self,
179    plugin: mrsm.core.Plugin,
180    debug: bool = False
181) -> SuccessTuple:
182    """Delete a plugin from an API repository."""
183    import json
184    r_url = plugin_r_url(plugin)
185    try:
186        response = self.delete(r_url, debug=debug)
187    except Exception:
188        return False, f"Failed to delete plugin '{plugin}'."
189
190    try:
191        success, msg = json.loads(response.text)
192    except Exception:
193        return False, response.text
194
195    return success, msg

Delete a plugin from an API repository.

def get_plugins( self, user_id: Optional[int] = None, search_term: Optional[str] = None, debug: bool = False) -> List[str]:
 93def get_plugins(
 94    self,
 95    user_id: Optional[int] = None,
 96    search_term: Optional[str] = None,
 97    debug: bool = False
 98) -> List[str]:
 99    """Return a list of registered plugin names.
100
101    Parameters
102    ----------
103    user_id: Optional[int], default None
104        If specified, return all plugins from a certain user.
105
106    search_term: Optional[str], default None
107        If specified, return plugins beginning with this string.
108
109    Returns
110    -------
111    A list of plugin names.
112    """
113    import json
114    from meerschaum.utils.warnings import error
115    from meerschaum._internal.static import STATIC_CONFIG
116    response = self.get(
117        STATIC_CONFIG['api']['endpoints']['plugins'],
118        params = {'user_id': user_id, 'search_term': search_term},
119        use_token = True,
120        debug = debug
121    )
122    if not response:
123        return []
124    plugins = json.loads(response.text)
125    if not isinstance(plugins, list):
126        error(response.text)
127    return plugins

Return a list of registered plugin names.

Parameters
  • user_id (Optional[int], default None): If specified, return all plugins from a certain user.
  • search_term (Optional[str], default None): If specified, return plugins beginning with this string.
Returns
  • A list of plugin names.
def get_plugin_attributes( self, plugin: meerschaum.Plugin, debug: bool = False) -> Dict[str, Any]:
130def get_plugin_attributes(
131    self,
132    plugin: mrsm.core.Plugin,
133    debug: bool = False
134) -> Dict[str, Any]:
135    """
136    Return a plugin's attributes.
137    """
138    import json
139    from meerschaum.utils.warnings import warn, error
140    r_url = plugin_r_url(plugin) + '/attributes'
141    response = self.get(r_url, use_token=True, debug=debug)
142    attributes = response.json()
143    if isinstance(attributes, str) and attributes and attributes[0] == '{':
144        try:
145            attributes = json.loads(attributes)
146        except Exception:
147            pass
148    if not isinstance(attributes, dict):
149        error(response.text)
150    elif not response and 'detail' in attributes:
151        warn(attributes['detail'])
152        return {}
153    return attributes

Return a plugin's attributes.

def get_plugin_version( self, plugin: meerschaum.Plugin, debug: bool = False) -> Optional[str]:
156def get_plugin_version(
157    self,
158    plugin: mrsm.core.Plugin,
159    debug: bool = False
160) -> Union[str, None]:
161    """
162    Return a plugin's version.
163    """
164    from meerschaum.utils.warnings import warn
165    r_url = plugin_r_url(plugin) + '/version'
166    response = self.get(r_url, use_token=(self.label != 'mrsm'), debug=debug)
167    if not response:
168        return None
169    try:
170        version = response.json()
171    except Exception as e:
172        warn(f"Cannot parse version for {plugin}:\n{e}")
173        version = None
174    return version or None

Return a plugin's version.

def login( self, debug: bool = False, warn: bool = True, **kw: Any) -> Tuple[bool, str]:
19def login(
20    self,
21    debug: bool = False,
22    warn: bool = True,
23    **kw: Any
24) -> SuccessTuple:
25    """Log in and set the session token."""
26    if warn and self.__dict__.get('_warn', None) is False:
27        warn = False
28    if self.login_scheme == 'api_key':
29        validate_response = self.post(
30            STATIC_CONFIG['api']['endpoints']['tokens'] + '/validate',
31            headers={'Authorization': f'Bearer {self.api_key}'},
32            use_token=False,
33            debug=debug,
34        )
35        if not validate_response:
36            return False, "API key is not valid."
37        return True, "API key is valid."
38
39    try:
40        if self.login_scheme == 'password':
41            login_data = {
42                'username': self.username,
43                'password': self.password,
44            }
45        elif self.login_scheme == 'client_credentials':
46            login_data = {
47                'client_id': self.client_id,
48                'client_secret': self.client_secret,
49            }
50    except AttributeError:
51        login_data = {}
52
53    if not login_data:
54        return False, f"Please login with the command `login {self}`."
55
56    login_scheme_msg = (
57        f" as user '{login_data['username']}'"
58        if self.login_scheme == 'username'
59        else ''
60    )
61
62    response = self.post(
63        STATIC_CONFIG['api']['endpoints']['login'],
64        data=login_data,
65        use_token=False,
66        debug=debug,
67    )
68    if response:
69        msg = f"Successfully logged into '{self}'{login_scheme_msg}'."
70        self._token = json.loads(response.text)['access_token']
71        self._expires = datetime.datetime.strptime(
72            json.loads(response.text)['expires'], 
73            '%Y-%m-%dT%H:%M:%S.%f'
74        )
75    else:
76        msg = (
77            f"Failed to log into '{self}'{login_scheme_msg}.\n" +
78            f"    Please verify login details for connector '{self}'."
79        )
80        if warn and not self.__dict__.get('_emitted_warning', False):
81            _warn(msg, stack=False)
82            self._emitted_warning = True
83
84    return response.__bool__(), msg

Log in and set the session token.

def test_connection(self, **kw: Any) -> Optional[bool]:
 87def test_connection(
 88    self,
 89    **kw: Any
 90) -> Union[bool, None]:
 91    """Test if a successful connection to the API may be made."""
 92    from meerschaum.connectors.poll import retry_connect
 93    _default_kw = {
 94        'max_retries': 1, 'retry_wait': 0, 'warn': False,
 95        'connector': self, 'enforce_chaining': False,
 96        'enforce_login': False,
 97    }
 98    _default_kw.update(kw)
 99    try:
100        return retry_connect(**_default_kw)
101    except Exception:
102        return False

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

def register_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
70def register_user(
71    self,
72    user: mrsm.core.User,
73    debug: bool = False,
74    **kw: Any
75) -> SuccessTuple:
76    """Register a new user."""
77    from meerschaum._internal.static import STATIC_CONFIG
78    r_url = f"{STATIC_CONFIG['api']['endpoints']['users']}/register"
79    data = {
80        'username': user.username,
81        'password': user.password,
82        'attributes': json.dumps(user.attributes),
83    }
84    if user.type:
85        data['type'] = user.type
86    if user.email:
87        data['email'] = user.email
88    response = self.post(r_url, data=data, debug=debug)
89    try:
90        _json = json.loads(response.text)
91        if isinstance(_json, dict) and 'detail' in _json:
92            return False, _json['detail']
93        success_tuple = tuple(_json)
94    except Exception:
95        msg = response.text if response else f"Failed to register user '{user}'."
96        return False, msg
97
98    return tuple(success_tuple)

Register a new user.

def get_user_id( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Union[int, str, uuid.UUID, NoneType]:
101def get_user_id(
102    self,
103    user: mrsm.core.User,
104    debug: bool = False,
105    **kw: Any
106) -> Union[int, str, UUID, None]:
107    """Get a user's ID."""
108    from meerschaum._internal.static import STATIC_CONFIG
109    from meerschaum.utils.misc import is_int, is_uuid
110    r_url = f"{STATIC_CONFIG['api']['endpoints']['users']}/{user.username}/id"
111    response = self.get(r_url, debug=debug, **kw)
112    try:
113        id_text = str(json.loads(response.text))
114        if is_int(id_text):
115            user_id = int(id_text)
116        elif is_uuid(id_text):
117            user_id = UUID(id_text)
118        else:
119            user_id = id_text
120    except Exception as e:
121        user_id = None
122    return user_id

Get a user's ID.

def get_users(self, debug: bool = False, **kw: Any) -> List[str]:
19def get_users(
20    self,
21    debug: bool = False,
22    **kw: Any
23) -> List[str]:
24    """
25    Return a list of registered usernames.
26    """
27    from meerschaum._internal.static import STATIC_CONFIG
28    response = self.get(
29        f"{STATIC_CONFIG['api']['endpoints']['users']}",
30        debug = debug,
31        use_token = True,
32    )
33    if not response:
34        return []
35    try:
36        return response.json()
37    except Exception as e:
38        return []

Return a list of registered usernames.

def edit_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
41def edit_user(
42    self,
43    user: mrsm.core.User,
44    debug: bool = False,
45    **kw: Any
46) -> SuccessTuple:
47    """Edit an existing user."""
48    from meerschaum._internal.static import STATIC_CONFIG
49    r_url = f"{STATIC_CONFIG['api']['endpoints']['users']}/edit"
50    data = {
51        'username': user.username,
52        'password': user.password,
53        'type': user.type,
54        'email': user.email,
55        'attributes': json.dumps(user.attributes),
56    }
57    response = self.post(r_url, data=data, debug=debug)
58    try:
59        _json = json.loads(response.text)
60        if isinstance(_json, dict) and 'detail' in _json:
61            return False, _json['detail']
62        success_tuple = tuple(_json)
63    except Exception:
64        msg = response.text if response else f"Failed to edit user '{user}'."
65        return False, msg
66
67    return tuple(success_tuple)

Edit an existing user.

def delete_user( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Tuple[bool, str]:
125def delete_user(
126    self,
127    user: mrsm.core.User,
128    debug: bool = False,
129    **kw: Any
130) -> SuccessTuple:
131    """Delete a user."""
132    from meerschaum._internal.static import STATIC_CONFIG
133    r_url = f"{STATIC_CONFIG['api']['endpoints']['users']}/{user.username}"
134    response = self.delete(r_url, debug=debug)
135    try:
136        _json = json.loads(response.text)
137        if isinstance(_json, dict) and 'detail' in _json:
138            return False, _json['detail']
139        success_tuple = tuple(_json)
140    except Exception:
141        success_tuple = False, f"Failed to delete user '{user.username}'."
142    return success_tuple

Delete a user.

def get_user_password_hash( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Optional[str]:
166def get_user_password_hash(
167    self,
168    user: mrsm.core.User,
169    debug: bool = False,
170    **kw: Any
171) -> Optional[str]:
172    """If configured, get a user's password hash."""
173    from meerschaum._internal.static import STATIC_CONFIG
174    r_url = STATIC_CONFIG['api']['endpoints']['users'] + '/' + user.username + '/password_hash'
175    response = self.get(r_url, debug=debug, **kw)
176    if not response:
177        return None
178    return response.json()

If configured, get a user's password hash.

def get_user_type( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw: Any) -> Optional[str]:
181def get_user_type(
182    self,
183    user: mrsm.core.User,
184    debug: bool = False,
185    **kw: Any
186) -> Optional[str]:
187    """If configured, get a user's type."""
188    from meerschaum._internal.static import STATIC_CONFIG
189    r_url = STATIC_CONFIG['api']['endpoints']['users'] + '/' + user.username + '/type'
190    response = self.get(r_url, debug=debug, **kw)
191    if not response:
192        return None
193    return response.json()

If configured, get a user's type.

def get_user_attributes( self, user: meerschaum.core.User._User.User, debug: bool = False, **kw) -> int:
145def get_user_attributes(
146    self,
147    user: mrsm.core.User,
148    debug: bool = False,
149    **kw
150) -> int:
151    """Get a user's attributes."""
152    from meerschaum._internal.static import STATIC_CONFIG
153    r_url = f"{STATIC_CONFIG['api']['endpoints']['users']}/{user.username}/attributes"
154    response = self.get(r_url, debug=debug, **kw)
155    try:
156        attributes = json.loads(response.text)
157    except Exception:
158        attributes = None
159    return attributes

Get a user's attributes.

def register_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
20def register_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
21    """
22    Register the provided token to the API.
23    """
24    from meerschaum.utils.dtypes import json_serialize_value
25    r_url = tokens_endpoint + '/register'
26    response = self.post(
27        r_url,
28        data=json.dumps({
29            'label': token.label,
30            'scopes': token.scopes,
31            'expiration': token.expiration,
32        }, default=json_serialize_value),
33        debug=debug,
34    )
35    if not response:
36        return False, f"Failed to register token:\n{response.text}"
37
38    data = response.json()
39    token.label = data['label']
40    token.secret = data['secret']
41    token.id = uuid.UUID(data['id'])
42    if data.get('expiration', None):
43        token.expiration = datetime.fromisoformat(data['expiration'])
44
45    return True, f"Registered token '{token.label}'."

Register the provided token to the API.

def get_token_model( self, token_id: uuid.UUID, debug: bool = False) -> 'Union[TokenModel, None]':
48def get_token_model(self, token_id: uuid.UUID, debug: bool = False) -> 'Union[TokenModel, None]':
49    """
50    Return a token's model from the API instance.
51    """
52    from meerschaum.models import TokenModel
53    r_url = tokens_endpoint + f'/{token_id}'
54    response = self.get(r_url, debug=debug)
55    if not response:
56        return None
57    data = response.json()
58    return TokenModel(**data)

Return a token's model from the API instance.

def get_tokens( self, labels: Optional[List[str]] = None, debug: bool = False) -> List[meerschaum.core.Token._Token.Token]:
61def get_tokens(self, labels: Optional[List[str]] = None, debug: bool = False) -> List[Token]:
62    """
63    Return the tokens registered to the current user.
64    """
65    from meerschaum.utils.warnings import warn
66    r_url = tokens_endpoint
67    params = {}
68    if labels:
69        params['labels'] = ','.join(labels)
70    response = self.get(r_url, params={'labels': labels}, debug=debug)
71    if not response:
72        warn(f"Could not get tokens from '{self}':\n{response.text}")
73        return []
74
75    tokens = [
76        Token(instance=self, **payload)
77        for payload in response.json()
78    ]
79    return tokens

Return the tokens registered to the current user.

def edit_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
 82def edit_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
 83    """
 84    Persist the token's in-memory state to the API.
 85    """
 86    r_url = tokens_endpoint + f"/{token.id}/edit"
 87    response = self.post(
 88        r_url,
 89        json={
 90            'creation': token.creation.isoformat() if token.creation else None,
 91            'expiration': token.expiration.isoformat() if token.expiration else None,
 92            'label': token.label,
 93            'is_valid': token.is_valid,
 94            'scopes': token.scopes,
 95        },
 96    )
 97    if not response:
 98        return False, f"Failed to edit token:\n{response.text}"
 99
100    success, msg = response.json()
101    return success, msg

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

def invalidate_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
104def invalidate_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
105    """
106    Invalidate the token, disabling it for future requests.
107    """
108    r_url = tokens_endpoint + f"/{token.id}/invalidate"
109    response = self.post(r_url)
110    if not response:
111        return False, f"Failed to invalidate token:\n{response.text}"
112
113    success, msg = response.json()
114    return success, msg

Invalidate the token, disabling it for future requests.

def get_token_scopes( self, token_id: Union[uuid.UUID, meerschaum.core.Token._Token.Token], debug: bool = False) -> List[str]:
117def get_token_scopes(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> List[str]:
118    """
119    Return the scopes for a token.
120    """
121    _token_id = (token_id.id if isinstance(token_id, Token) else token_id)
122    model = self.get_token_model(_token_id, debug=debug).scopes
123    return getattr(model, 'scopes', [])

Return the scopes for a token.

def token_exists( self, token_id: Union[uuid.UUID, meerschaum.core.Token._Token.Token], debug: bool = False) -> bool:
126def token_exists(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> bool:
127    """
128    Return `True` if a token exists.
129    """
130    _token_id = (token_id.id if isinstance(token_id, Token) else token_id)
131    model = self.get_token_model(_token_id, debug=debug)
132    if model is None:
133        return False
134    return model.creation is not None

Return True if a token exists.

def delete_token( self, token: meerschaum.core.Token._Token.Token, debug: bool = False) -> Tuple[bool, str]:
137def delete_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
138    """
139    Delete the token from the API.
140    """
141    r_url = tokens_endpoint + f"/{token.id}"
142    response = self.delete(r_url, debug=debug)
143    if not response:
144        return False, f"Failed to delete token:\n{response.text}"
145    
146    success, msg = response.json()
147    return success, msg

Delete the token from the API.

@classmethod
def from_uri( cls, uri: str, label: Optional[str] = None, as_dict: bool = False) -> Union[APIConnector, Dict[str, Union[str, int]]]:
13@classmethod
14def from_uri(
15    cls,
16    uri: str,
17    label: Optional[str] = None,
18    as_dict: bool = False,
19) -> Union[
20        'meerschaum.connectors.APIConnector',
21        Dict[str, Union[str, int]],
22    ]:
23    """
24    Create a new APIConnector from a URI string.
25
26    Parameters
27    ----------
28    uri: str
29        The URI connection string.
30
31    label: Optional[str], default None
32        If provided, use this as the connector label.
33        Otherwise use the determined database name.
34
35    as_dict: bool, default False
36        If `True`, return a dictionary of the keyword arguments
37        necessary to create a new `APIConnector`, otherwise create a new object.
38
39    Returns
40    -------
41    A new APIConnector object or a dictionary of attributes (if `as_dict` is `True`).
42    """
43    from meerschaum.connectors.sql import SQLConnector
44    params = SQLConnector.parse_uri(uri)
45    if 'host' not in params:
46        error("No host was found in the provided URI.")
47    params['protocol'] = params.pop('flavor')
48    params['label'] = label or (
49        (
50            (params['username'] + '@' if 'username' in params else '')
51            + params['host']
52        ).lower()
53    )
54
55    return cls(**params) if not as_dict else params

Create a new APIConnector from a URI string.

Parameters
  • uri (str): The URI connection string.
  • label (Optional[str], default None): If provided, use this as the connector label. Otherwise use the determined database name.
  • as_dict (bool, default False): If True, return a dictionary of the keyword arguments necessary to create a new APIConnector, otherwise create a new object.
Returns
  • A new APIConnector object or a dictionary of attributes (if as_dict is True).
def get_jobs(self, debug: bool = False) -> Dict[str, meerschaum.Job]:
28def get_jobs(self, debug: bool = False) -> Dict[str, Job]:
29    """
30    Return a dictionary of remote jobs.
31    """
32    response = self.get(JOBS_ENDPOINT, debug=debug)
33    if not response:
34        warn(f"Failed to get remote jobs from {self}.")
35        return {}
36    return {
37        name: Job(
38            name,
39            job_meta['sysargs'],
40            executor_keys=str(self),
41            _properties=job_meta['daemon']['properties']
42        )
43        for name, job_meta in response.json().items()
44    }

Return a dictionary of remote jobs.

def get_job(self, name: str, debug: bool = False) -> meerschaum.Job:
47def get_job(self, name: str, debug: bool = False) -> Job:
48    """
49    Return a single Job object.
50    """
51    metadata = self.get_job_metadata(name, debug=debug)
52    if not metadata:
53        raise ValueError(f"Job '{name}' does not exist.")
54
55    return Job(
56        name,
57        metadata['sysargs'],
58        executor_keys=str(self),
59        _properties=metadata['daemon']['properties'],
60    )

Return a single Job object.

def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]:
 63def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]:
 64    """
 65    Return the metadata for a single job.
 66    """
 67    now = time.perf_counter()
 68    _job_metadata_cache = self.__dict__.get('_job_metadata_cache', None)
 69    _job_metadata_timestamp = (
 70        _job_metadata_cache.get(name, {}).get('timestamp', None)
 71    ) if _job_metadata_cache is not None else None
 72
 73    if (
 74        _job_metadata_timestamp is not None
 75        and (now - _job_metadata_timestamp) < JOB_METADATA_CACHE_SECONDS
 76    ):
 77        if debug:
 78            dprint(f"Returning cached metadata for job '{name}'.")
 79        return _job_metadata_cache[name]['metadata']
 80
 81    response = self.get(JOBS_ENDPOINT + f"/{name}", debug=debug)
 82    if not response:
 83        if debug:
 84            msg = (
 85                response.json()['detail']
 86                if 'detail' in response.text
 87                else response.text
 88            )
 89            warn(f"Failed to get metadata for job '{name}':\n{msg}")
 90        return {}
 91
 92    metadata = response.json()
 93    if _job_metadata_cache is None:
 94        self._job_metadata_cache = {}
 95
 96    self._job_metadata_cache[name] = {
 97        'timestamp': now,
 98        'metadata': metadata,
 99    }
100    return metadata

Return the metadata for a single job.

def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]:
102def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]:
103    """
104    Return the daemon properties for a single job.
105    """
106    metadata = self.get_job_metadata(name, debug=debug)
107    return metadata.get('daemon', {}).get('properties', {})

Return the daemon properties for a single job.

def get_job_exists(self, name: str, debug: bool = False) -> bool:
149def get_job_exists(self, name: str, debug: bool = False) -> bool:
150    """
151    Return whether a job exists.
152    """
153    response = self.get(JOBS_ENDPOINT + f'/{name}/exists', debug=debug)
154    if not response:
155        warn(f"Failed to determine whether job '{name}' exists.")
156        return False
157
158    return response.json()

Return whether a job exists.

def delete_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
161def delete_job(self, name: str, debug: bool = False) -> SuccessTuple:
162    """
163    Delete a job.
164    """
165    response = self.delete(JOBS_ENDPOINT + f"/{name}", debug=debug)
166    if not response:
167        if 'detail' in response.text:
168            return False, response.json()['detail']
169
170        return False, response.text
171
172    return tuple(response.json())

Delete a job.

def start_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
175def start_job(self, name: str, debug: bool = False) -> SuccessTuple:
176    """
177    Start a job.
178    """
179    response = self.post(JOBS_ENDPOINT + f"/{name}/start", debug=debug)
180    if not response:
181        if 'detail' in response.text:
182            return False, response.json()['detail']
183        return False, response.text
184
185    return tuple(response.json())

Start a job.

def create_job( self, name: str, sysargs: List[str], properties: Optional[Dict[str, str]] = None, debug: bool = False) -> Tuple[bool, str]:
188def create_job(
189    self,
190    name: str,
191    sysargs: List[str],
192    properties: Optional[Dict[str, str]] = None,
193    debug: bool = False,
194) -> SuccessTuple:
195    """
196    Create a job.
197    """
198    response = self.post(
199        JOBS_ENDPOINT + f"/{name}",
200        json={
201            'sysargs': sysargs,
202            'properties': properties,
203        },
204        debug=debug,
205    )
206    if not response:
207        if 'detail' in response.text:
208            return False, response.json()['detail']
209        return False, response.text
210
211    return tuple(response.json())

Create a job.

def stop_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
214def stop_job(self, name: str, debug: bool = False) -> SuccessTuple:
215    """
216    Stop a job.
217    """
218    response = self.post(JOBS_ENDPOINT + f"/{name}/stop", debug=debug)
219    if not response:
220        if 'detail' in response.text:
221            return False, response.json()['detail']
222        return False, response.text
223
224    return tuple(response.json())

Stop a job.

def pause_job(self, name: str, debug: bool = False) -> Tuple[bool, str]:
227def pause_job(self, name: str, debug: bool = False) -> SuccessTuple:
228    """
229    Pause a job.
230    """
231    response = self.post(JOBS_ENDPOINT + f"/{name}/pause", debug=debug)
232    if not response:
233        if 'detail' in response.text:
234            return False, response.json()['detail']
235        return False, response.text
236
237    return tuple(response.json())

Pause a job.

def get_logs(self, name: str, debug: bool = False) -> str:
240def get_logs(self, name: str, debug: bool = False) -> str:
241    """
242    Return the logs for a job.
243    """
244    response = self.get(LOGS_ENDPOINT + f"/{name}")
245    if not response:
246        raise ValueError(f"Cannot fetch logs for job '{name}':\n{response.text}")
247
248    return response.json()

Return the logs for a job.

def get_job_stop_time(self, name: str, debug: bool = False) -> Optional[datetime.datetime]:
251def get_job_stop_time(self, name: str, debug: bool = False) -> Union[datetime, None]:
252    """
253    Return the job's manual stop time.
254    """
255    response = self.get(JOBS_ENDPOINT + f"/{name}/stop_time")
256    if not response:
257        warn(f"Failed to get stop time for job '{name}':\n{response.text}")
258        return None
259
260    data = response.json()
261    if data is None:
262        return None
263
264    return datetime.fromisoformat(data)

Return the job's manual stop time.

def monitor_logs( self, name: str, callback_function: Callable[[Any], Any], input_callback_function: Callable[[NoneType], str], stop_callback_function: Callable[[NoneType], str], stop_on_exit: bool = False, strip_timestamps: bool = False, accept_input: bool = True, debug: bool = False):
348def monitor_logs(
349    self,
350    name: str,
351    callback_function: Callable[[Any], Any],
352    input_callback_function: Callable[[None], str],
353    stop_callback_function: Callable[[None], str],
354    stop_on_exit: bool = False,
355    strip_timestamps: bool = False,
356    accept_input: bool = True,
357    debug: bool = False,
358):
359    """
360    Monitor a job's log files and execute a callback with the changes.
361    """
362    return asyncio.run(
363        self.monitor_logs_async(
364            name,
365            callback_function,
366            input_callback_function=input_callback_function,
367            stop_callback_function=stop_callback_function,
368            stop_on_exit=stop_on_exit,
369            strip_timestamps=strip_timestamps,
370            accept_input=accept_input,
371            debug=debug
372        )
373    )

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

async def monitor_logs_async( self, name: str, callback_function: Callable[[Any], Any], input_callback_function: Callable[[], str], stop_callback_function: Callable[[Tuple[bool, str]], str], stop_on_exit: bool = False, strip_timestamps: bool = False, accept_input: bool = True, debug: bool = False):
267async def monitor_logs_async(
268    self,
269    name: str,
270    callback_function: Callable[[Any], Any],
271    input_callback_function: Callable[[], str],
272    stop_callback_function: Callable[[SuccessTuple], str],
273    stop_on_exit: bool = False,
274    strip_timestamps: bool = False,
275    accept_input: bool = True,
276    debug: bool = False,
277):
278    """
279    Monitor a job's log files and await a callback with the changes.
280    """
281    import traceback
282    from meerschaum.jobs import StopMonitoringLogs
283    from meerschaum.utils.formatting._jobs import strip_timestamp_from_line
284
285    websockets, websockets_exceptions = mrsm.attempt_import('websockets', 'websockets.exceptions')
286    protocol = 'ws' if self.URI.startswith('http://') else 'wss'
287    port = self.port if 'port' in self.__dict__ else ''
288    uri = f"{protocol}://{self.host}:{port}{LOGS_ENDPOINT}/{name}/ws"
289
290    async def _stdin_callback(client):
291        if input_callback_function is None:
292            return
293
294        if asyncio.iscoroutinefunction(input_callback_function):
295            data = await input_callback_function()
296        else:
297            data = input_callback_function()
298
299        await client.send(data)
300
301    async def _stop_callback(client):
302        try:
303            result = tuple(json.loads(await client.recv()))
304        except Exception as e:
305            warn(traceback.format_exc())
306            result = False, str(e)
307
308        if stop_callback_function is not None:
309            if asyncio.iscoroutinefunction(stop_callback_function):
310                await stop_callback_function(result)
311            else:
312                stop_callback_function(result)
313
314        if stop_on_exit:
315            raise StopMonitoringLogs
316
317    message_callbacks = {
318        JOBS_STDIN_MESSAGE: _stdin_callback,
319        JOBS_STOP_MESSAGE: _stop_callback,
320    }
321
322    async with websockets.connect(uri) as websocket:
323        try:
324            await websocket.send(self.token or 'no-login')
325        except websockets_exceptions.ConnectionClosedOK:
326            pass
327
328        while True:
329            try:
330                response = await websocket.recv()
331                callback = message_callbacks.get(response, None)
332                if callback is not None:
333                    await callback(websocket)
334                    continue
335
336                if strip_timestamps:
337                    response = strip_timestamp_from_line(response)
338
339                if asyncio.iscoroutinefunction(callback_function):
340                    await callback_function(response)
341                else:
342                    callback_function(response)
343            except (KeyboardInterrupt, StopMonitoringLogs):
344                await websocket.close()
345                break

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

def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool:
375def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool:
376    """
377    Return whether a remote job is blocking on stdin.
378    """
379    response = self.get(JOBS_ENDPOINT + f'/{name}/is_blocking_on_stdin', debug=debug)
380    if not response:
381        return False
382
383    return response.json()

Return whether a remote job is blocking on stdin.

def get_job_began(self, name: str, debug: bool = False) -> Optional[str]:
116def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
117    """
118    Return a job's `began` timestamp, if it exists.
119    """
120    properties = self.get_job_properties(name, debug=debug)
121    began_str = properties.get('daemon', {}).get('began', None)
122    if began_str is None:
123        return None
124
125    return began_str

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

def get_job_ended(self, name: str, debug: bool = False) -> Optional[str]:
127def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
128    """
129    Return a job's `ended` timestamp, if it exists.
130    """
131    properties = self.get_job_properties(name, debug=debug)
132    ended_str = properties.get('daemon', {}).get('ended', None)
133    if ended_str is None:
134        return None
135
136    return ended_str

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

def get_job_paused(self, name: str, debug: bool = False) -> Optional[str]:
138def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]:
139    """
140    Return a job's `paused` timestamp, if it exists.
141    """
142    properties = self.get_job_properties(name, debug=debug)
143    paused_str = properties.get('daemon', {}).get('paused', None)
144    if paused_str is None:
145        return None
146
147    return paused_str

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

def get_job_status(self, name: str, debug: bool = False) -> str:
109def get_job_status(self, name: str, debug: bool = False) -> str:
110    """
111    Return the job's status.
112    """
113    metadata = self.get_job_metadata(name, debug=debug)
114    return metadata.get('status', 'stopped')

Return the job's status.