Coverage for filip/utils/simple_ql.py: 72%
111 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-11-20 16:54 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-11-20 16:54 +0000
1"""
2The Simple Query Language provides a simplified syntax to retrieve entities
3which match a set of conditions. A query is composed by a list of
4statements separated by the ';' character. Each statement expresses a
5matching condition. The query returns all the entities that match all
6the matching conditions (AND logical operator).
8For further details of the language please refer to:
10https://telefonicaid.github.io/fiware-orion/api/v2/stable/
11"""
12import regex as re
13from aenum import Enum
14from typing import Union, List, Tuple, Any
17class Operator(str, Enum):
18 """
19 The list of operators (and the format of the values they use) is as follows:
20 """
21 _init_ = 'value __doc__'
23 EQUAL = '==', "Single element, e.g. temperature!=41. For an entity to " \
24 "match, it must contain the target property (temperature) " \
25 "and the target property value must not be the query value " \
26 "(41). " \
27 "A list of comma-separated values, e.g. color!=black," \
28 "red. For an entity to match, it must contain the target " \
29 "property and the target property value must not be any " \
30 "of the values in the list (AND clause) (or not include any "\
31 "of the values in the list in case the target property " \
32 "value is an array). Eg. entities whose attribute color is " \
33 "set to black will not match, while entities whose " \
34 "attribute color is set to white will match." \
35 "A range, specified as a minimum and maximum separated by " \
36 ".., e.g. temperature!=10..20. For an entity to match, " \
37 "it must contain the target property (temperature) and the " \
38 "target property value must not be between the upper and " \
39 "lower limits (both included). Ranges can only be used " \
40 "with elements target properties that represent dates (in " \
41 "ISO8601 format), numbers or strings. "
42 UNEQUAL = '!=', "Single element, e.g. temperature!=41. For an entity to " \
43 "match, it must contain the target property " \
44 "(temperature) and the target property value must not be " \
45 "the query value (41). A list of comma-separated values, " \
46 "e.g. color!=black,red. For an entity to match, it must " \
47 "contain the target property and the target property " \
48 "value must not be any of the values in the list (AND " \
49 "clause) (or not include any of the values in the list " \
50 "in case the target property value is an array). Eg. " \
51 "entities whose attribute color is set to black will not " \
52 "match, while entities whose attribute color is set to " \
53 "white will match. A range, specified as a minimum and " \
54 "maximum separated by .., e.g. temperature!=10..20. For " \
55 "an entity to match, it must contain the target property " \
56 "(temperature) and the target property value must not be " \
57 "between the upper and lower limits (both included). " \
58 "Ranges can only be used with elements target properties " \
59 "that represent dates (in ISO8601 format), numbers or " \
60 "strings. "
61 GREATER_THAN = '>', "The right-hand side must be a single element, e.g. " \
62 "temperature>42. For an entity to match, it must " \
63 "contain the target property (temperature) and the " \
64 "target property value must be strictly greater than " \
65 "the query value (42). This operation is only valid " \
66 "for target properties of type date, number or " \
67 "string (used with target properties of other types " \
68 "may lead to unpredictable results). "
69 LESS_THAN = '<', "The right-hand side must be a single element, e.g. " \
70 "temperature<43. For an entity to match, it must " \
71 "contain the target property (temperature) and the " \
72 "target property value must be strictly less than the " \
73 "value (43). This operation is only valid for target " \
74 "properties of type date, number or string (used with " \
75 "target properties of other types may lead to " \
76 "unpredictable results). "
77 GREATER_OR_EQUAL = '>=', "The right-hand side must be a single element, " \
78 "e.g. temperature>=44. For an entity to match, " \
79 "it must contain the target property (" \
80 "temperature) and the target property value " \
81 "must be greater than or equal to that value " \
82 "(44). This operation is only valid for target " \
83 "properties of type date, number or string " \
84 "(used with target properties of other types " \
85 "may lead to unpredictable results). "
86 LESS_OR_EQUAL = '<=', "The right-hand side must be a single element, " \
87 "e.g. temperature<=45. For an entity to match, " \
88 "it must contain the target property (temperature) " \
89 "and the target property value must be less than " \
90 "or equal to that value (45). This operation is " \
91 "only valid for target properties of type date, " \
92 "number or string (used with target properties of " \
93 "other types may lead to unpredictable results). "
94 MATCH_PATTERN = '~=', "The value matches a given pattern, expressed as a " \
95 "regular expression, e.g. color~=ow. For an entity " \
96 "to match, it must contain the target property (" \
97 "color) and the target property value must match " \
98 "the string in the right-hand side, 'ow' in this " \
99 "example (brown and yellow would match, black and " \
100 "white would not). This operation is only valid " \
101 "for target properties of type string. "
103 @classmethod
104 def list(cls):
105 """
107 Returns:
108 list of all valid values
109 """
110 return list(map(lambda c: c.value, cls))
113class QueryStatement(Tuple):
114 """
115 Simple query statement
116 """
118 def __new__(cls, left: str, op: Union[str, Operator], right: Any):
119 q = tuple.__new__(QueryStatement, (left, op, right))
120 q = cls.validate(q)
121 return q
123 @classmethod
124 def __get_validators__(cls):
125 yield cls.validate
127 @classmethod
128 def validate(cls, value):
129 """
130 Validates statements
132 Args:
133 value:
135 Returns:
136 """
137 if isinstance(value, (tuple, QueryStatement)):
138 if len(value) != 3:
139 raise TypeError('3-tuple required')
140 if not isinstance(value[0], str):
141 raise TypeError('First argument must be a string!')
142 if value[1] not in Operator.list():
143 raise TypeError('Invalid comparison operator!')
144 if value[1] not in [Operator.EQUAL,
145 Operator.UNEQUAL,
146 Operator.MATCH_PATTERN]:
147 try:
148 float(value[2])
149 except ValueError as err:
150 err.args += ("Invalid combination of operator and right "
151 "hand side!",)
152 raise
153 return value
154 elif isinstance(value, str):
155 return cls.parse_str(value)
156 raise TypeError
158 def to_str(self):
159 """
160 Parses QueryStatement to String
162 Returns:
163 String
164 """
165 if not isinstance(self[2], str):
166 right = str(self[2])
167 elif self[2].isnumeric():
168 right = f"{self[2]}"
169 else:
170 right = self[2]
171 return ''.join([self[0], self[1], right])
173 @classmethod
174 def parse_str(cls, string: str):
175 """
176 Generates QueryStatement form string
178 Args:
179 string:
181 Returns:
182 QueryStatement
183 """
184 for op in Operator.list():
185 if re.fullmatch(rf"^\w((\w|[^&,?,/,#,\*,\s]\w)?)*{op}\w+$", string):
186 args = string.split(op)
187 if len(args) == 2:
188 if args[1].isnumeric():
189 try:
190 right = int(args[1])
191 except ValueError:
192 right = float(args[1])
193 return QueryStatement(args[0], op, right)
194 return QueryStatement(args[0], op, args[1])
195 raise ValueError
197 def __str__(self):
198 """ Return str(self). """
199 return self.to_str()
201 def __repr__(self):
202 """ Return repr(self). """
203 return self.to_str().__repr__()
206class QueryString:
207 """
208 Class for validated QueryStrings that can be used in api clients
209 """
210 def __init__(self, qs: Union[Tuple,
211 QueryStatement,
212 List[Union[QueryStatement, Tuple]]]):
213 qs = self.__check_arguments(qs=qs)
214 self._qs = qs
216 @classmethod
217 def __check_arguments(cls, qs):
218 """
219 Check arguments on consistency
221 Args:
222 qs: queny statement object
224 returns:
225 List of QueryStatements
226 """
227 if isinstance(qs, List):
228 for idx, item in enumerate(qs):
229 if not isinstance(item, QueryStatement):
230 qs[idx] = QueryStatement(*item)
231 # Remove duplicates
232 qs = list(dict.fromkeys(qs))
233 elif isinstance(qs, QueryStatement):
234 qs = [qs]
235 elif isinstance(qs, tuple):
236 qs = [QueryStatement(*qs)]
237 else:
238 raise ValueError('Invalid argument!')
239 return qs
241 def update(self, qs: Union[Tuple, QueryStatement, List[QueryStatement]]):
242 """
243 Adds or updates QueryStatement within QueryString. First to arguments
244 must match an existing argument for update. This redundant rules
246 Args:
247 qs: Query statement to add to the string object
249 Returns:
250 None
251 """
252 qs = self.__check_arguments(qs=qs)
253 self._qs.extend(qs)
254 self._qs = list(dict.fromkeys(qs))
256 def remove(self, qs: Union[Tuple, QueryStatement, List[QueryStatement]]):
257 """
258 Remove Statement from QueryString
259 Args:
260 qs:
262 Returns:
264 """
265 qs = self.__check_arguments(qs=qs)
266 for q in qs:
267 self._qs.remove(q)
269 @classmethod
270 def __get_validators__(cls):
271 yield cls.validate
273 @classmethod
274 def validate(cls, v):
275 """validate QueryString"""
276 if isinstance(v, QueryString):
277 return v
278 if isinstance(v, str):
279 return cls.parse_str(v)
280 raise ValueError('Invalid argument!')
282 def to_str(self):
283 """
284 Parsing self.qs to string object
286 Returns:
287 String: query string that can be added to requests as parameter
288 """
289 return ';'.join([q.to_str() for q in self._qs])
291 @classmethod
292 def parse_str(cls, string: str):
293 """
294 Creates QueryString from string
296 Args:
297 string:
299 Returns:
300 QueryString
301 """
302 q_parts = string.split(';')
303 qs = []
304 for part in q_parts:
305 q = QueryStatement.parse_str(part)
306 qs.append(q)
307 return QueryString(qs=qs)
309 def __str__(self):
310 """ Return str(self). """
311 return self.to_str()
313 def __repr__(self):
314 """ Return repr(self). """
315 return self.to_str().__repr__()