meerschaum.connectors.api
Interact with the Meerschaum API (send commands, pull data, etc.)
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.
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)
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.
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
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
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.
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.
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_tokenisTrue, 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.Reponseobject.
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_tokenisTrue, 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.Reponseobject.
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_tokenisTrue, 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.Reponseobject.
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_tokenisTrue, 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.Reponseobject.
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_tokenisTrue, 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.Reponseobject.
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_tokenisTrue, 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.Reponseobject.
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.
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.
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.
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.
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")
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.
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.
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.
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).
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
pipestable (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.
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).
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.
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.
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.
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.
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.
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
- pipe (meerschaum.Pipe): The pipe whose attributes we are fetching.
Returns
- A dictionary of a pipe's attributes.
- If the pipe does not exist, return an empty dictionary.
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 (honoringparams). IfFalse, get the oldest datetime (ASC instead of DESC).
Returns
- The most recent (or oldest if
newestisFalse) datetime of a pipe, - rounded down to the closest minute.
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
- pipe ('meerschaum.Pipe'): The pipe which were are querying.
Returns
- A bool indicating whether a pipe's underlying table exists.
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.
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.
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
- pipe (meerschaum.Pipe:): The pipe to be dropped.
Returns
- A success 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
- pipe (meerschaum.Pipe): The pipe with rows to be deleted.
Returns
- A success tuple.
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
- pipe (meerschaum.Pipe): The pipe whose columns to be queried.
Returns
- A dictionary mapping column names to their database types.
Examples
>>> {
... 'dt': 'TIMESTAMP WITHOUT TIMEZONE',
... 'id': 'BIGINT',
... 'val': 'DOUBLE PRECISION',
... }
>>>
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.
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.
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
intof the number of bytes occupied by the target table, - or
Noneif the size could not be determined.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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, runVACUUM FULL(PostgreSQL family only).
Returns
- A
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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
SuccessTupleindicating success.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 newAPIConnector, otherwise create a new object.
Returns
- A new APIConnector object or a dictionary of attributes (if
as_dictisTrue).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.