1"""This module defines data parser 2which can be used to exchange information between processes. 3""" 4import json 5import abc 6import shlex 7import itertools 8import enum 9 10 11class Parser: 12 """Subclasses of this abstract class define 13 how to input will be parsed. 14 """ 15 16 @staticmethod 17 @abc.abstractmethod 18 def get_name(): 19 """Returns the constant name which is associated to this parser.""" 20 raise NotImplementedError() 21 22 @abc.abstractmethod 23 def parse(self, line): 24 """Parses a line. 25 26 Args: 27 line (str): read line 28 29 Returns: 30 dict containing the key value pairs of the line 31 """ 32 raise NotImplementedError() 33 34 @abc.abstractmethod 35 def unparse(self, data): 36 """Composes the data to a string 37 whichs follows the syntax of the parser. 38 39 Args: 40 data (dict): data as key value pairs 41 42 Returns: 43 string 44 """ 45 raise NotImplementedError() 46 47 48class JsonParser(Parser): 49 """Parses json input""" 50 51 @staticmethod 52 def get_name(): 53 return 'json' 54 55 def parse(self, line): 56 try: 57 data = json.loads(line) 58 if not isinstance(data, dict): 59 raise ValueError( 60 'Expected to parse an json object, got ' + line) 61 return data 62 except json.JSONDecodeError as error: 63 raise ValueError(error) 64 65 def unparse(self, data): 66 return json.dumps(data) 67 68 69class SimpleParser(Parser): 70 """Parses key value pairs separated by a tab. 71 Does not support escaping spaces. 72 """ 73 SEPARATOR = '\t' 74 75 @staticmethod 76 def get_name(): 77 return 'simple' 78 79 def parse(self, line): 80 components = line.split(SimpleParser.SEPARATOR) 81 82 if len(components) % 2 != 0: 83 raise ValueError( 84 'Expected key value pairs, ' + 85 'but at least one key has no value: ' + 86 line) 87 88 return { 89 key: value 90 for key, value in itertools.zip_longest( 91 components[::2], components[1::2]) 92 } 93 94 def unparse(self, data): 95 return SimpleParser.SEPARATOR.join( 96 str(key) + SimpleParser.SEPARATOR + str(value.replace('\n', '')) 97 for key, value in data.items()) 98 99 100class BashParser(Parser): 101 """Parses input generated 102 by dumping associative arrays with `declare -p`. 103 """ 104 105 @staticmethod 106 def get_name(): 107 return 'bash' 108 109 def parse(self, line): 110 # remove 'typeset -A varname=( ' and ')' 111 start = line.find('(') 112 end = line.rfind(')') 113 114 if not 0 <= start < end: 115 raise ValueError( 116 "Expected input to be formatted like " 117 "the output of bashs `declare -p` function. " 118 "Got: " + line) 119 120 components = itertools.dropwhile( 121 lambda text: not text or text[0] != '[', 122 shlex.split(line[start + 1:end])) 123 return { 124 key[1:-1]: value 125 for pair in components 126 for key, value in (pair.split('=', maxsplit=1),) 127 } 128 129 def unparse(self, data): 130 return ' '.join( 131 '[' + str(key) + ']=' + shlex.quote(value) 132 for key, value in data.items()) 133 134 135@enum.unique 136class ParserOption(str, enum.Enum): 137 JSON = JsonParser 138 SIMPLE = SimpleParser 139 BASH = BashParser 140 141 def __new__(cls, parser_class): 142 inst = str.__new__(cls) 143 inst._value_ = parser_class.get_name() 144 inst.parser_class = parser_class 145 return inst 146