meerschaum.utils.pipes
Define utilities for working with pipes.
1#! /usr/bin/env python3 2# vim:fenc=utf-8 3 4""" 5Define utilities for working with pipes. 6""" 7 8from __future__ import annotations 9 10from typing import Any, Dict, Callable 11import re 12import ast 13import copy 14 15from meerschaum.utils.typing import PipesDict, Optional 16import meerschaum as mrsm 17from meerschaum import _core 18 19 20def get_pipe_from_value( 21 value: str | dict[str, str], 22 _pipe: mrsm.Pipe | None = None, 23) -> mrsm.Pipe | None: 24 """ 25 Given an input value (string or dictionary), return the corresponding pipe. 26 27 Parameters 28 ---------- 29 value: str | dict[str, str] 30 The definition keys for the pipe to be constructed. 31 This may either be the literal constructor string or a dictionary of constructor kwargs. 32 33 _pipe: mrsm.Pipe | None, default None 34 If provided, use the instance connector for this reference pipe 35 if an explicit instance connector is undefined. 36 37 Returns 38 ------- 39 A `Pipe` corresponding to the given keys. 40 """ 41 from meerschaum.utils.warnings import warn 42 if isinstance(value, mrsm.Pipe): 43 return value 44 45 if isinstance(value, str): 46 return get_pipe_from_string(value, _pipe=_pipe) 47 48 if not isinstance(value, dict): 49 raise ValueError("Expecting kwargs for a Pipe.") 50 51 if ( 52 _pipe is not None 53 and not ( 54 'instance' in value 55 or 'mrsm_instance' in value 56 or 'instance_keys' in value 57 ) 58 ): 59 value = {**value} 60 value['instance'] = _pipe.instance_keys 61 62 try: 63 pipe = mrsm.Pipe(**value) 64 except Exception as e: 65 warn(f"Failed build pipe from value '{value}':\n{e}") 66 return None 67 68 return pipe 69 70 71def get_pipe_from_string( 72 string: str, 73 _pipe: mrsm.Pipe | None = None, 74) -> mrsm.Pipe | None: 75 """ 76 If a string is equal to a `Pipe` constructor, return the pipe. 77 78 Parameters 79 ---------- 80 string: str 81 The string containing the constructor syntax. 82 Must be an exact match. 83 84 _pipe: mrsm.Pipe | None, default None 85 If provided, use the instance connector for this reference pipe 86 if an explicit instance connector is undefined. 87 88 Returns 89 ------- 90 A `Pipe` corresponding the defined pipe in the input string. 91 92 Examples 93 -------- 94 >>> get_pipe_from_string('Pipe("foo", "bar")') 95 Pipe('foo', 'bar') 96 >>> get_pipe_from_string('mrsm.Pipe("spam", "eggs", instance="sql:local")') 97 Pipe('spam', 'eggs', instance='sql:local') 98 """ 99 from meerschaum.utils.warnings import warn 100 from meerschaum.utils.misc import parse_arguments_str 101 pattern = r'(?:mrsm\.)?Pipe\((.*?)\)' 102 matches = list(re.finditer(pattern, string)) 103 if not matches: 104 return None 105 106 try: 107 match = matches[0] 108 args_str = match.group(1) 109 args, kwargs = parse_arguments_str(args_str) 110 if ( 111 _pipe is not None 112 and not ( 113 'instance' in kwargs 114 or 'mrsm_instance' in kwargs 115 or 'instance_keys' in kwargs 116 ) 117 ): 118 kwargs['instance'] = _pipe.instance_keys 119 pipe = mrsm.Pipe(*args, **kwargs) 120 except Exception as e: 121 warn(f"Failed to build pipe from string '{string}': \n{e}") 122 return None 123 124 return pipe 125 126 127def evaluate_pipe_access_chain(access_chain: str, pipe: mrsm.Pipe, _pipe=None): 128 """ 129 Safely evaluate the access chain on a Pipe. 130 """ 131 expr = f"pipe{access_chain}" 132 if pipe == _pipe and access_chain.lstrip('.').startswith('parameters'): 133 parameters_access_chain = access_chain.lstrip('.').split('parameters', maxsplit=1)[-1] 134 expr = f"pipe.attributes['parameters']{parameters_access_chain}" 135 tree = ast.parse(expr, mode='eval') 136 137 def _eval(node, context): 138 if isinstance(node, ast.Expression): 139 return _eval(node.body, context) 140 141 elif isinstance(node, ast.Name): 142 if node.id == "pipe": 143 return context 144 raise ValueError(f"Unknown variable: {node.id}") 145 146 elif isinstance(node, ast.Attribute): 147 value = _eval(node.value, context) 148 return getattr(value, node.attr) 149 150 elif isinstance(node, ast.Subscript): 151 value = _eval(node.value, context) 152 key = _eval(node.slice, context) if isinstance(node.slice, ast.Index) else _eval(node.slice, context) 153 return value[key] 154 155 elif isinstance(node, ast.Constant): # Python 3.8+ 156 return node.value 157 158 elif isinstance(node, ast.Str): # Older Python 159 return node.s 160 161 elif isinstance(node, ast.Index): # Older Python AST style 162 return _eval(node.value, context) 163 164 else: 165 raise TypeError(f"Unsupported AST node: {ast.dump(node)}") 166 167 return _eval(tree, pipe) 168 169 170 171def replace_pipes_syntax(text: str, _pipe=None) -> Any: 172 """ 173 Parse a string containing the `{{ Pipe() }}` syntax. 174 """ 175 _rust = _core.replace_pipes_syntax 176 return _rust(text, _pipe=_pipe) 177 178 179def replace_pipes_in_dict( 180 pipes: Optional[PipesDict] = None, 181 func: Callable[[Any], Any] = str, 182 debug: bool = False, 183 **kw 184) -> PipesDict: 185 """ 186 Replace the Pipes in a Pipes dict with the result of another function. 187 188 Parameters 189 ---------- 190 pipes: Optional[PipesDict], default None 191 The pipes dict to be processed. 192 193 func: Callable[[Any], Any], default str 194 The function to be applied to every pipe. 195 Defaults to the string constructor. 196 197 debug: bool, default False 198 Verbosity toggle. 199 200 Returns 201 ------- 202 A dictionary where every pipe is replaced with the output of a function. 203 204 """ 205 def change_dict(d: Dict[Any, Any]) -> None: 206 for k, v in d.items(): 207 if isinstance(v, dict): 208 change_dict(v) 209 elif isinstance(v, list): 210 d[k] = [func(i) for i in v] 211 elif isinstance(v, tuple): 212 d[k] = tuple([func(i) for i in v]) 213 else: 214 d[k] = func(v) 215 216 if pipes is None: 217 from meerschaum import get_pipes 218 pipes = get_pipes(debug=debug, **kw) 219 220 result = copy.deepcopy(pipes) 221 change_dict(result) 222 return result 223 224 225def is_pipe_registered( 226 pipe: mrsm.Pipe, 227 pipes: PipesDict, 228 debug: bool = False 229) -> bool: 230 """ 231 Check if a Pipe is inside the pipes dictionary. 232 233 Parameters 234 ---------- 235 pipe: meerschaum.Pipe 236 The pipe to see if it's in the dictionary. 237 238 pipes: PipesDict 239 The dictionary to search inside. 240 241 debug: bool, default False 242 Verbosity toggle. 243 244 Returns 245 ------- 246 A bool indicating whether the pipe is inside the dictionary. 247 """ 248 ck, mk, lk = pipe.connector_keys, pipe.metric_key, pipe.location_key 249 try: 250 return ck in pipes and mk in pipes[ck] and lk in pipes[ck][mk] 251 except KeyError: 252 return False 253 254 255def pipes_dict_from_list(pipes_list: list[mrsm.Pipe]) -> PipesDict: 256 """ 257 Return a PipesDict from a list of pipes. 258 Note that all pipes must share the same instance connector. 259 260 Examples 261 -------- 262 >>> pipes_list = [mrsm.Pipe('a', 'b'), mrsm.Pipe('a', 'b', 'c')] 263 >>> pipes_dict_from_list(pipes) 264 { 265 'a': { 266 'b': { 267 None: Pipe('a', 'b') 268 'c': Pipe('a', 'b', 'c') 269 } 270 } 271 } 272 """ 273 from meerschaum.utils.warnings import warn 274 from meerschaum.utils.misc import items_str 275 if not pipes_list: 276 return {} 277 278 instance_keys = pipes_list[0].instance_keys 279 same_pipes = [pipe for pipe in pipes_list if pipe.instance_keys == instance_keys] 280 diff_pipes = [pipe for pipe in pipes_list if pipe.instance_keys != instance_keys] 281 if diff_pipes: 282 warn( 283 f"Skipping pipes with instance keys different from '{instance_keys}':\n" 284 f"{items_str(diff_pipes, quotes=False)}" 285 ) 286 287 pipes_dict: PipesDict = {} 288 for pipe in same_pipes: 289 if pipe.connector_keys not in pipes_dict: 290 pipes_dict[pipe.connector_keys] = {} 291 if pipe.metric_key not in pipes_dict[pipe.connector_key]: 292 pipes_dict[pipe.connector_keys][pipe.metric_key] = {} 293 pipes_dict[pipe.connector_keys][pipe.metric_key][pipe.location_key] = pipe 294 295 return pipes_dict 296 297 298def flatten_pipes_dict(pipes_dict: PipesDict) -> list[mrsm.Pipe]: 299 """ 300 Convert the standard pipes dictionary into a list. 301 302 Parameters 303 ---------- 304 pipes_dict: PipesDict 305 The pipes dictionary to be flattened. 306 307 Returns 308 ------- 309 A list of `Pipe` objects. 310 311 """ 312 pipes_list = [] 313 for ck in pipes_dict.values(): 314 for mk in ck.values(): 315 pipes_list.extend(list(mk.values())) 316 return pipes_list
def
get_pipe_from_value( value: str | dict[str, str], _pipe: meerschaum.Pipe | None = None) -> meerschaum.Pipe | None:
21def get_pipe_from_value( 22 value: str | dict[str, str], 23 _pipe: mrsm.Pipe | None = None, 24) -> mrsm.Pipe | None: 25 """ 26 Given an input value (string or dictionary), return the corresponding pipe. 27 28 Parameters 29 ---------- 30 value: str | dict[str, str] 31 The definition keys for the pipe to be constructed. 32 This may either be the literal constructor string or a dictionary of constructor kwargs. 33 34 _pipe: mrsm.Pipe | None, default None 35 If provided, use the instance connector for this reference pipe 36 if an explicit instance connector is undefined. 37 38 Returns 39 ------- 40 A `Pipe` corresponding to the given keys. 41 """ 42 from meerschaum.utils.warnings import warn 43 if isinstance(value, mrsm.Pipe): 44 return value 45 46 if isinstance(value, str): 47 return get_pipe_from_string(value, _pipe=_pipe) 48 49 if not isinstance(value, dict): 50 raise ValueError("Expecting kwargs for a Pipe.") 51 52 if ( 53 _pipe is not None 54 and not ( 55 'instance' in value 56 or 'mrsm_instance' in value 57 or 'instance_keys' in value 58 ) 59 ): 60 value = {**value} 61 value['instance'] = _pipe.instance_keys 62 63 try: 64 pipe = mrsm.Pipe(**value) 65 except Exception as e: 66 warn(f"Failed build pipe from value '{value}':\n{e}") 67 return None 68 69 return pipe
Given an input value (string or dictionary), return the corresponding pipe.
Parameters
- value (str | dict[str, str]): The definition keys for the pipe to be constructed. This may either be the literal constructor string or a dictionary of constructor kwargs.
- _pipe (mrsm.Pipe | None, default None): If provided, use the instance connector for this reference pipe if an explicit instance connector is undefined.
Returns
- A
Pipecorresponding to the given keys.
def
get_pipe_from_string( string: str, _pipe: meerschaum.Pipe | None = None) -> meerschaum.Pipe | None:
72def get_pipe_from_string( 73 string: str, 74 _pipe: mrsm.Pipe | None = None, 75) -> mrsm.Pipe | None: 76 """ 77 If a string is equal to a `Pipe` constructor, return the pipe. 78 79 Parameters 80 ---------- 81 string: str 82 The string containing the constructor syntax. 83 Must be an exact match. 84 85 _pipe: mrsm.Pipe | None, default None 86 If provided, use the instance connector for this reference pipe 87 if an explicit instance connector is undefined. 88 89 Returns 90 ------- 91 A `Pipe` corresponding the defined pipe in the input string. 92 93 Examples 94 -------- 95 >>> get_pipe_from_string('Pipe("foo", "bar")') 96 Pipe('foo', 'bar') 97 >>> get_pipe_from_string('mrsm.Pipe("spam", "eggs", instance="sql:local")') 98 Pipe('spam', 'eggs', instance='sql:local') 99 """ 100 from meerschaum.utils.warnings import warn 101 from meerschaum.utils.misc import parse_arguments_str 102 pattern = r'(?:mrsm\.)?Pipe\((.*?)\)' 103 matches = list(re.finditer(pattern, string)) 104 if not matches: 105 return None 106 107 try: 108 match = matches[0] 109 args_str = match.group(1) 110 args, kwargs = parse_arguments_str(args_str) 111 if ( 112 _pipe is not None 113 and not ( 114 'instance' in kwargs 115 or 'mrsm_instance' in kwargs 116 or 'instance_keys' in kwargs 117 ) 118 ): 119 kwargs['instance'] = _pipe.instance_keys 120 pipe = mrsm.Pipe(*args, **kwargs) 121 except Exception as e: 122 warn(f"Failed to build pipe from string '{string}': \n{e}") 123 return None 124 125 return pipe
If a string is equal to a Pipe constructor, return the pipe.
Parameters
- string (str): The string containing the constructor syntax. Must be an exact match.
- _pipe (mrsm.Pipe | None, default None): If provided, use the instance connector for this reference pipe if an explicit instance connector is undefined.
Returns
- A
Pipecorresponding the defined pipe in the input string.
Examples
>>> get_pipe_from_string('Pipe("foo", "bar")')
Pipe('foo', 'bar')
>>> get_pipe_from_string('mrsm.Pipe("spam", "eggs", instance="sql:local")')
Pipe('spam', 'eggs', instance='sql:local')
128def evaluate_pipe_access_chain(access_chain: str, pipe: mrsm.Pipe, _pipe=None): 129 """ 130 Safely evaluate the access chain on a Pipe. 131 """ 132 expr = f"pipe{access_chain}" 133 if pipe == _pipe and access_chain.lstrip('.').startswith('parameters'): 134 parameters_access_chain = access_chain.lstrip('.').split('parameters', maxsplit=1)[-1] 135 expr = f"pipe.attributes['parameters']{parameters_access_chain}" 136 tree = ast.parse(expr, mode='eval') 137 138 def _eval(node, context): 139 if isinstance(node, ast.Expression): 140 return _eval(node.body, context) 141 142 elif isinstance(node, ast.Name): 143 if node.id == "pipe": 144 return context 145 raise ValueError(f"Unknown variable: {node.id}") 146 147 elif isinstance(node, ast.Attribute): 148 value = _eval(node.value, context) 149 return getattr(value, node.attr) 150 151 elif isinstance(node, ast.Subscript): 152 value = _eval(node.value, context) 153 key = _eval(node.slice, context) if isinstance(node.slice, ast.Index) else _eval(node.slice, context) 154 return value[key] 155 156 elif isinstance(node, ast.Constant): # Python 3.8+ 157 return node.value 158 159 elif isinstance(node, ast.Str): # Older Python 160 return node.s 161 162 elif isinstance(node, ast.Index): # Older Python AST style 163 return _eval(node.value, context) 164 165 else: 166 raise TypeError(f"Unsupported AST node: {ast.dump(node)}") 167 168 return _eval(tree, pipe)
Safely evaluate the access chain on a Pipe.
def
replace_pipes_syntax(text: str, _pipe=None) -> Any:
172def replace_pipes_syntax(text: str, _pipe=None) -> Any: 173 """ 174 Parse a string containing the `{{ Pipe() }}` syntax. 175 """ 176 _rust = _core.replace_pipes_syntax 177 return _rust(text, _pipe=_pipe)
Parse a string containing the {{ Pipe() }} syntax.
def
replace_pipes_in_dict( pipes: Dict[str, Dict[str, Dict[str | None, meerschaum.Pipe]]] | None = None, func: Callable[[Any], Any] = <class 'str'>, debug: bool = False, **kw) -> Dict[str, Dict[str, Dict[str | None, meerschaum.Pipe]]]:
180def replace_pipes_in_dict( 181 pipes: Optional[PipesDict] = None, 182 func: Callable[[Any], Any] = str, 183 debug: bool = False, 184 **kw 185) -> PipesDict: 186 """ 187 Replace the Pipes in a Pipes dict with the result of another function. 188 189 Parameters 190 ---------- 191 pipes: Optional[PipesDict], default None 192 The pipes dict to be processed. 193 194 func: Callable[[Any], Any], default str 195 The function to be applied to every pipe. 196 Defaults to the string constructor. 197 198 debug: bool, default False 199 Verbosity toggle. 200 201 Returns 202 ------- 203 A dictionary where every pipe is replaced with the output of a function. 204 205 """ 206 def change_dict(d: Dict[Any, Any]) -> None: 207 for k, v in d.items(): 208 if isinstance(v, dict): 209 change_dict(v) 210 elif isinstance(v, list): 211 d[k] = [func(i) for i in v] 212 elif isinstance(v, tuple): 213 d[k] = tuple([func(i) for i in v]) 214 else: 215 d[k] = func(v) 216 217 if pipes is None: 218 from meerschaum import get_pipes 219 pipes = get_pipes(debug=debug, **kw) 220 221 result = copy.deepcopy(pipes) 222 change_dict(result) 223 return result
Replace the Pipes in a Pipes dict with the result of another function.
Parameters
- pipes (Optional[PipesDict], default None): The pipes dict to be processed.
- func (Callable[[Any], Any], default str): The function to be applied to every pipe. Defaults to the string constructor.
- debug (bool, default False): Verbosity toggle.
Returns
- A dictionary where every pipe is replaced with the output of a function.
def
is_pipe_registered( pipe: meerschaum.Pipe, pipes: Dict[str, Dict[str, Dict[str | None, meerschaum.Pipe]]], debug: bool = False) -> bool:
226def is_pipe_registered( 227 pipe: mrsm.Pipe, 228 pipes: PipesDict, 229 debug: bool = False 230) -> bool: 231 """ 232 Check if a Pipe is inside the pipes dictionary. 233 234 Parameters 235 ---------- 236 pipe: meerschaum.Pipe 237 The pipe to see if it's in the dictionary. 238 239 pipes: PipesDict 240 The dictionary to search inside. 241 242 debug: bool, default False 243 Verbosity toggle. 244 245 Returns 246 ------- 247 A bool indicating whether the pipe is inside the dictionary. 248 """ 249 ck, mk, lk = pipe.connector_keys, pipe.metric_key, pipe.location_key 250 try: 251 return ck in pipes and mk in pipes[ck] and lk in pipes[ck][mk] 252 except KeyError: 253 return False
Check if a Pipe is inside the pipes dictionary.
Parameters
- pipe (meerschaum.Pipe): The pipe to see if it's in the dictionary.
- pipes (PipesDict): The dictionary to search inside.
- debug (bool, default False): Verbosity toggle.
Returns
- A bool indicating whether the pipe is inside the dictionary.
def
pipes_dict_from_list( pipes_list: list[meerschaum.Pipe]) -> Dict[str, Dict[str, Dict[str | None, meerschaum.Pipe]]]:
256def pipes_dict_from_list(pipes_list: list[mrsm.Pipe]) -> PipesDict: 257 """ 258 Return a PipesDict from a list of pipes. 259 Note that all pipes must share the same instance connector. 260 261 Examples 262 -------- 263 >>> pipes_list = [mrsm.Pipe('a', 'b'), mrsm.Pipe('a', 'b', 'c')] 264 >>> pipes_dict_from_list(pipes) 265 { 266 'a': { 267 'b': { 268 None: Pipe('a', 'b') 269 'c': Pipe('a', 'b', 'c') 270 } 271 } 272 } 273 """ 274 from meerschaum.utils.warnings import warn 275 from meerschaum.utils.misc import items_str 276 if not pipes_list: 277 return {} 278 279 instance_keys = pipes_list[0].instance_keys 280 same_pipes = [pipe for pipe in pipes_list if pipe.instance_keys == instance_keys] 281 diff_pipes = [pipe for pipe in pipes_list if pipe.instance_keys != instance_keys] 282 if diff_pipes: 283 warn( 284 f"Skipping pipes with instance keys different from '{instance_keys}':\n" 285 f"{items_str(diff_pipes, quotes=False)}" 286 ) 287 288 pipes_dict: PipesDict = {} 289 for pipe in same_pipes: 290 if pipe.connector_keys not in pipes_dict: 291 pipes_dict[pipe.connector_keys] = {} 292 if pipe.metric_key not in pipes_dict[pipe.connector_key]: 293 pipes_dict[pipe.connector_keys][pipe.metric_key] = {} 294 pipes_dict[pipe.connector_keys][pipe.metric_key][pipe.location_key] = pipe 295 296 return pipes_dict
Return a PipesDict from a list of pipes. Note that all pipes must share the same instance connector.
Examples
>>> pipes_list = [mrsm.Pipe('a', 'b'), mrsm.Pipe('a', 'b', 'c')]
>>> pipes_dict_from_list(pipes)
{
'a': {
'b': {
None: Pipe('a', 'b')
'c': Pipe('a', 'b', 'c')
}
}
}
def
flatten_pipes_dict( pipes_dict: Dict[str, Dict[str, Dict[str | None, meerschaum.Pipe]]]) -> list[meerschaum.Pipe]:
299def flatten_pipes_dict(pipes_dict: PipesDict) -> list[mrsm.Pipe]: 300 """ 301 Convert the standard pipes dictionary into a list. 302 303 Parameters 304 ---------- 305 pipes_dict: PipesDict 306 The pipes dictionary to be flattened. 307 308 Returns 309 ------- 310 A list of `Pipe` objects. 311 312 """ 313 pipes_list = [] 314 for ck in pipes_dict.values(): 315 for mk in ck.values(): 316 pipes_list.extend(list(mk.values())) 317 return pipes_list
Convert the standard pipes dictionary into a list.
Parameters
- pipes_dict (PipesDict): The pipes dictionary to be flattened.
Returns
- A list of
Pipeobjects.