omlish 0.0.0.dev46__py3-none-any.whl → 0.0.0.dev48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,593 @@
1
+ import inspect
2
+ import json
3
+ import math
4
+
5
+ from . import exceptions
6
+
7
+
8
+ # python types -> jmespath types
9
+ TYPES_MAP = {
10
+ 'bool': 'boolean',
11
+ 'list': 'array',
12
+ 'dict': 'object',
13
+ 'NoneType': 'null',
14
+ 'unicode': 'string',
15
+ 'str': 'string',
16
+ 'float': 'number',
17
+ 'int': 'number',
18
+ 'long': 'number',
19
+ 'OrderedDict': 'object',
20
+ '_Projection': 'array',
21
+ '_Expression': 'expref',
22
+ }
23
+
24
+
25
+ # jmespath types -> python types
26
+ REVERSE_TYPES_MAP = {
27
+ 'boolean': ('bool',),
28
+ 'array': ('list', '_Projection'),
29
+ 'object': ('dict', 'OrderedDict'),
30
+ 'null': ('NoneType',),
31
+ 'string': ('unicode', 'str'),
32
+ 'number': ('float', 'int', 'long'),
33
+ 'expref': ('_Expression',),
34
+ }
35
+
36
+
37
+ def signature(*arguments):
38
+ def _record_signature(func):
39
+ func.signature = arguments
40
+ return func
41
+ return _record_signature
42
+
43
+
44
+ class FunctionRegistry(type):
45
+ def __init__(cls, name, bases, attrs):
46
+ cls._populate_function_table()
47
+ super().__init__(name, bases, attrs)
48
+
49
+ def _populate_function_table(cls):
50
+ function_table = {}
51
+
52
+ # Any method with a @signature decorator that also starts with "_func_" is registered as a function.
53
+ # _func_max_by -> max_by function.
54
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
55
+ if not name.startswith('_func_'):
56
+ continue
57
+
58
+ signature = getattr(method, 'signature', None)
59
+ if signature is not None:
60
+ function_table[name[6:]] = {
61
+ 'function': method,
62
+ 'signature': signature,
63
+ }
64
+
65
+ cls.FUNCTION_TABLE = function_table
66
+
67
+
68
+ class Functions(metaclass=FunctionRegistry):
69
+
70
+ FUNCTION_TABLE: dict = {} # noqa
71
+
72
+ def call_function(self, function_name, resolved_args):
73
+ try:
74
+ spec = self.FUNCTION_TABLE[function_name]
75
+ except KeyError:
76
+ raise exceptions.UnknownFunctionError(f'Unknown function: {function_name}()') # noqa
77
+
78
+ function = spec['function']
79
+ signature = spec['signature']
80
+
81
+ self._validate_arguments(resolved_args, signature, function_name)
82
+
83
+ return function(self, *resolved_args)
84
+
85
+ def _validate_arguments(self, args, signature, function_name):
86
+
87
+ if len(signature) == 0:
88
+ return self._type_check(args, signature, function_name)
89
+
90
+ required_arguments_count = len([
91
+ param for param in signature if param and (not param.get('optional') or not param['optional'])
92
+ ])
93
+ optional_arguments_count = len([
94
+ param for param in signature if param and param.get('optional') and param['optional']
95
+ ])
96
+ has_variadic = signature[-1].get('variadic') if signature is not None else False
97
+
98
+ if has_variadic:
99
+ if len(args) < len(signature):
100
+ raise exceptions.VariadicArityError(len(signature), len(args), function_name)
101
+
102
+ elif optional_arguments_count > 0:
103
+ if (
104
+ len(args) < required_arguments_count or
105
+ len(args) > (required_arguments_count + optional_arguments_count)
106
+ ):
107
+ raise exceptions.ArityError(len(signature), len(args), function_name)
108
+ elif len(args) != required_arguments_count:
109
+ raise exceptions.ArityError(len(signature), len(args), function_name)
110
+
111
+ return self._type_check(args, signature, function_name)
112
+
113
+ def _type_check(self, actual, signature, function_name):
114
+ for i in range(min(len(signature), len(actual))):
115
+ allowed_types = self._get_allowed_types_from_signature(signature[i])
116
+ if allowed_types:
117
+ self._type_check_single(actual[i], allowed_types, function_name)
118
+
119
+ def _type_check_single(self, current, types, function_name):
120
+ # Type checking involves checking the top level type, and in the case of arrays, potentially checking the types
121
+ # of each element.
122
+ allowed_types, allowed_subtypes = self._get_allowed_pytypes(types)
123
+
124
+ # We're not using isinstance() on purpose. The type model for jmespath does not map 1-1 with python types
125
+ # (booleans are considered integers in python for example).
126
+ actual_typename = type(current).__name__
127
+ if actual_typename not in allowed_types:
128
+ raise exceptions.JmespathTypeError(
129
+ function_name, current,
130
+ self._convert_to_jmespath_type(actual_typename), types)
131
+
132
+ # If we're dealing with a list type, we can have additional restrictions on the type of the list elements (for
133
+ # example a function can require a list of numbers or a list of strings). Arrays are the only types that can
134
+ # have subtypes.
135
+ if allowed_subtypes:
136
+ self._subtype_check(current, allowed_subtypes, types, function_name)
137
+
138
+ def _get_allowed_types_from_signature(self, spec):
139
+ # signature supports monotype {'type': 'type-name'}## or multiple types {'types': ['type1-name', 'type2-name']}
140
+ if spec.get('type'):
141
+ spec.update({'types': [spec.get('type')]})
142
+ return spec.get('types')
143
+
144
+ def _get_allowed_pytypes(self, types):
145
+ allowed_types: list = []
146
+ allowed_subtypes: list = []
147
+
148
+ for t in types:
149
+ type_ = t.split('-', 1)
150
+ if len(type_) == 2:
151
+ type_, subtype = type_
152
+ allowed_subtypes.append(REVERSE_TYPES_MAP[subtype])
153
+ else:
154
+ type_ = type_[0]
155
+
156
+ allowed_types.extend(REVERSE_TYPES_MAP[type_])
157
+
158
+ return allowed_types, allowed_subtypes
159
+
160
+ def _subtype_check(self, current, allowed_subtypes, types, function_name):
161
+ if len(allowed_subtypes) == 1:
162
+ # The easy case, we know up front what type we need to validate.
163
+ allowed_subtypes = allowed_subtypes[0]
164
+ for element in current:
165
+ actual_typename = type(element).__name__
166
+ if actual_typename not in allowed_subtypes:
167
+ raise exceptions.JmespathTypeError(function_name, element, actual_typename, types)
168
+
169
+ elif len(allowed_subtypes) > 1 and current:
170
+ # Dynamic type validation. Based on the first type we see, we validate that the remaining types match.
171
+ first = type(current[0]).__name__
172
+ for subtypes in allowed_subtypes:
173
+ if first in subtypes:
174
+ allowed = subtypes
175
+ break
176
+ else:
177
+ raise exceptions.JmespathTypeError(function_name, current[0], first, types)
178
+
179
+ for element in current:
180
+ actual_typename = type(element).__name__
181
+ if actual_typename not in allowed:
182
+ raise exceptions.JmespathTypeError(function_name, element, actual_typename, types)
183
+
184
+ @signature({'types': ['number']})
185
+ def _func_abs(self, arg):
186
+ return abs(arg)
187
+
188
+ @signature({'types': ['string']})
189
+ def _func_lower(self, arg):
190
+ return arg.lower()
191
+
192
+ @signature({'types': ['string']})
193
+ def _func_upper(self, arg):
194
+ return arg.upper()
195
+
196
+ @signature({'types': ['array-number']})
197
+ def _func_avg(self, arg):
198
+ if arg:
199
+ return sum(arg) / len(arg)
200
+ else:
201
+ return None
202
+
203
+ @signature({'types': [], 'variadic': True})
204
+ def _func_not_null(self, *arguments):
205
+ for argument in arguments:
206
+ if argument is not None:
207
+ return argument
208
+ return None
209
+
210
+ @signature({'types': []})
211
+ def _func_to_array(self, arg):
212
+ if isinstance(arg, list):
213
+ return arg
214
+ else:
215
+ return [arg]
216
+
217
+ @signature({'types': []})
218
+ def _func_to_string(self, arg):
219
+ if isinstance(arg, str):
220
+ return arg
221
+ else:
222
+ return json.dumps(arg, separators=(',', ':'), default=str)
223
+
224
+ @signature({'types': []})
225
+ def _func_to_number(self, arg):
226
+ if isinstance(arg, (list, dict, bool)):
227
+ return None
228
+
229
+ elif arg is None:
230
+ return None
231
+
232
+ elif isinstance(arg, (int, float)):
233
+ return arg
234
+
235
+ else:
236
+ try:
237
+ return int(arg)
238
+ except ValueError:
239
+ try:
240
+ return float(arg)
241
+ except ValueError:
242
+ return None
243
+
244
+ @signature({'types': ['array', 'string']}, {'types': []})
245
+ def _func_contains(self, subject, search):
246
+ return search in subject
247
+
248
+ @signature({'types': ['string', 'array', 'object']})
249
+ def _func_length(self, arg):
250
+ return len(arg)
251
+
252
+ @signature({'types': ['string']}, {'types': ['string']})
253
+ def _func_ends_with(self, search, suffix):
254
+ return search.endswith(suffix)
255
+
256
+ @signature({'types': ['string']}, {'types': ['string']})
257
+ def _func_starts_with(self, search, suffix):
258
+ return search.startswith(suffix)
259
+
260
+ @signature({'types': ['array', 'string']})
261
+ def _func_reverse(self, arg):
262
+ if isinstance(arg, str):
263
+ return arg[::-1]
264
+ else:
265
+ return list(reversed(arg))
266
+
267
+ @signature({'types': ['number']})
268
+ def _func_ceil(self, arg):
269
+ return math.ceil(arg)
270
+
271
+ @signature({'types': ['number']})
272
+ def _func_floor(self, arg):
273
+ return math.floor(arg)
274
+
275
+ @signature({'types': ['string']}, {'types': ['array-string']})
276
+ def _func_join(self, separator, array):
277
+ return separator.join(array)
278
+
279
+ @signature({'types': ['expref']}, {'types': ['array']})
280
+ def _func_map(self, expref, arg):
281
+ result = []
282
+ for element in arg:
283
+ result.append(expref.visit(expref.expression, element))
284
+ return result
285
+
286
+ @signature({'types': ['array-number', 'array-string']})
287
+ def _func_max(self, arg):
288
+ if arg:
289
+ return max(arg)
290
+ else:
291
+ return None
292
+
293
+ @signature({'types': ['object'], 'variadic': True})
294
+ def _func_merge(self, *arguments):
295
+ merged = {}
296
+ for arg in arguments:
297
+ merged.update(arg)
298
+ return merged
299
+
300
+ @signature({'types': ['array-number', 'array-string']})
301
+ def _func_min(self, arg):
302
+ if arg:
303
+ return min(arg)
304
+ else:
305
+ return None
306
+
307
+ @signature({'types': ['array-string', 'array-number']})
308
+ def _func_sort(self, arg):
309
+ return sorted(arg)
310
+
311
+ @signature({'types': ['array-number']})
312
+ def _func_sum(self, arg):
313
+ return sum(arg)
314
+
315
+ @signature({'types': ['object']})
316
+ def _func_items(self, arg):
317
+ return [list(t) for t in arg.items()]
318
+
319
+ @signature({'types': ['array']})
320
+ def _func_from_items(self, items):
321
+ return dict(items)
322
+
323
+ @signature({'types': ['object']})
324
+ def _func_keys(self, arg):
325
+ # To be consistent with .values() should we also return the indices of a list?
326
+ return list(arg.keys())
327
+
328
+ @signature({'types': ['object']})
329
+ def _func_values(self, arg):
330
+ return list(arg.values())
331
+
332
+ @signature(
333
+ {'type': 'string'},
334
+ {'type': 'string'},
335
+ {'type': 'number', 'optional': True},
336
+ {'type': 'number', 'optional': True},
337
+ )
338
+ def _func_find_first(self, text, search, start=0, end=None):
339
+ self._ensure_integer('find_first', 'start', start)
340
+ self._ensure_integer('find_first', 'end', end)
341
+ return self._find_impl(
342
+ text,
343
+ search,
344
+ lambda t, s: t.find(s),
345
+ start,
346
+ end,
347
+ )
348
+
349
+ @signature(
350
+ {'type': 'string'},
351
+ {'type': 'string'},
352
+ {'type': 'number', 'optional': True},
353
+ {'type': 'number', 'optional': True})
354
+ def _func_find_last(self, text, search, start=0, end=None):
355
+ self._ensure_integer('find_last', 'start', start)
356
+ self._ensure_integer('find_last', 'end', end)
357
+ return self._find_impl(
358
+ text,
359
+ search,
360
+ lambda t, s: t.rfind(s),
361
+ start,
362
+ end,
363
+ )
364
+
365
+ def _find_impl(self, text, search, func, start, end):
366
+ if len(search) == 0:
367
+ return None
368
+ if end is None:
369
+ end = len(text)
370
+
371
+ pos = func(text[start:end], search)
372
+ if start < 0:
373
+ start = start + len(text)
374
+
375
+ # restrict resulting range to valid indices
376
+ start = min(max(start, 0), len(text))
377
+ return start + pos if pos != -1 else None
378
+
379
+ @signature(
380
+ {'type': 'string'},
381
+ {'type': 'number'},
382
+ {'type': 'string', 'optional': True},
383
+ )
384
+ def _func_pad_left(self, text, width, padding=' '):
385
+ self._ensure_non_negative_integer('pad_left', 'width', width)
386
+ return self._pad_impl(lambda: text.rjust(width, padding), padding)
387
+
388
+ @signature(
389
+ {'type': 'string'},
390
+ {'type': 'number'},
391
+ {'type': 'string', 'optional': True},
392
+ )
393
+ def _func_pad_right(self, text, width, padding=' '):
394
+ self._ensure_non_negative_integer('pad_right', 'width', width)
395
+ return self._pad_impl(lambda: text.ljust(width, padding), padding)
396
+
397
+ def _pad_impl(self, func, padding):
398
+ if len(padding) != 1:
399
+ raise exceptions.JmespathError(
400
+ f'syntax-error: pad_right() expects $padding to have a single character, but received '
401
+ f'`{padding}` instead.',
402
+ )
403
+ return func()
404
+
405
+ @signature(
406
+ {'type': 'string'},
407
+ {'type': 'string'},
408
+ {'type': 'string'},
409
+ {'type': 'number', 'optional': True},
410
+ )
411
+ def _func_replace(self, text, search, replacement, count=None):
412
+ self._ensure_non_negative_integer(
413
+ 'replace',
414
+ 'count',
415
+ count,
416
+ )
417
+
418
+ if count is not None:
419
+ return text.replace(search, replacement, int(count))
420
+
421
+ return text.replace(search, replacement)
422
+
423
+ @signature(
424
+ {'type': 'string'},
425
+ {'type': 'string'},
426
+ {'type': 'number', 'optional': True},
427
+ )
428
+ def _func_split(self, text, search, count=None):
429
+ self._ensure_non_negative_integer(
430
+ 'split',
431
+ 'count',
432
+ count,
433
+ )
434
+
435
+ if len(search) == 0:
436
+ chars = list(text)
437
+ if count is None:
438
+ return chars
439
+
440
+ head = list(chars[:count])
441
+ tail = [''.join(chars[count:])]
442
+ return head + tail
443
+
444
+ if count is not None:
445
+ return text.split(search, count)
446
+
447
+ return text.split(search)
448
+
449
+ def _ensure_integer(
450
+ self,
451
+ func_name,
452
+ param_name,
453
+ param_value,
454
+ ):
455
+ if param_value is not None:
456
+ if int(param_value) != param_value:
457
+ raise exceptions.JmespathValueError(
458
+ func_name,
459
+ param_value,
460
+ 'integer',
461
+ )
462
+
463
+ def _ensure_non_negative_integer(
464
+ self,
465
+ func_name,
466
+ param_name,
467
+ param_value,
468
+ ):
469
+ if param_value is not None:
470
+ if int(param_value) != param_value or int(param_value) < 0:
471
+ raise exceptions.JmespathValueError(
472
+ func_name,
473
+ param_name,
474
+ 'non-negative integer',
475
+ )
476
+
477
+ @signature({'type': 'string'}, {'type': 'string', 'optional': True})
478
+ def _func_trim(self, text, chars=None):
479
+ if chars is None or len(chars) == 0:
480
+ return text.strip()
481
+ return text.strip(chars)
482
+
483
+ @signature({'type': 'string'}, {'type': 'string', 'optional': True})
484
+ def _func_trim_left(self, text, chars=None):
485
+ if chars is None or len(chars) == 0:
486
+ return text.lstrip()
487
+ return text.lstrip(chars)
488
+
489
+ @signature({'type': 'string'}, {'type': 'string', 'optional': True})
490
+ def _func_trim_right(self, text, chars=None):
491
+ if chars is None or len(chars) == 0:
492
+ return text.rstrip()
493
+ return text.rstrip(chars)
494
+
495
+ @signature({'types': []})
496
+ def _func_type(self, arg):
497
+ if isinstance(arg, str):
498
+ return 'string'
499
+ elif isinstance(arg, bool):
500
+ return 'boolean'
501
+ elif isinstance(arg, list):
502
+ return 'array'
503
+ elif isinstance(arg, dict):
504
+ return 'object'
505
+ elif isinstance(arg, (float, int)):
506
+ return 'number'
507
+ elif arg is None:
508
+ return 'null'
509
+ else:
510
+ return None
511
+
512
+ @signature({'types': ['array']}, {'types': ['expref']})
513
+ def _func_sort_by(self, array, expref):
514
+ if not array:
515
+ return array
516
+
517
+ # sort_by allows for the expref to be either a number of a string, so we have some special logic to handle this.
518
+ # We evaluate the first array element and verify that it's either a string of a number. We then create a key
519
+ # function that validates that type, which requires that remaining array elements resolve to the same type as
520
+ # the first element.
521
+ required_type = self._convert_to_jmespath_type(type(expref.visit(expref.expression, array[0])).__name__)
522
+ if required_type not in ['number', 'string']:
523
+ raise exceptions.JmespathTypeError(
524
+ 'sort_by',
525
+ array[0],
526
+ required_type,
527
+ ['string', 'number'],
528
+ )
529
+
530
+ keyfunc = self._create_key_func(expref, [required_type], 'sort_by')
531
+ return sorted(array, key=keyfunc)
532
+
533
+ @signature({'types': ['array']}, {'types': ['expref']})
534
+ def _func_min_by(self, array, expref):
535
+ keyfunc = self._create_key_func(
536
+ expref,
537
+ ['number', 'string'],
538
+ 'min_by',
539
+ )
540
+
541
+ if array:
542
+ return min(array, key=keyfunc)
543
+ else:
544
+ return None
545
+
546
+ @signature({'types': ['array']}, {'types': ['expref']})
547
+ def _func_max_by(self, array, expref):
548
+ keyfunc = self._create_key_func(
549
+ expref,
550
+ ['number', 'string'],
551
+ 'max_by',
552
+ )
553
+
554
+ if array:
555
+ return max(array, key=keyfunc)
556
+ else:
557
+ return None
558
+
559
+ @signature({'types': ['array'], 'variadic': True})
560
+ def _func_zip(self, *arguments):
561
+ return [list(t) for t in zip(*arguments)]
562
+
563
+ @signature({'types': ['array']}, {'types': ['expref']})
564
+ def _func_group_by(self, array, expref):
565
+ keyfunc = self._create_key_func(expref, ['null', 'string'], 'group_by')
566
+ if not array:
567
+ return None
568
+
569
+ result = {}
570
+ keys = list(dict.fromkeys([keyfunc(item) for item in array if keyfunc(item) is not None]))
571
+ for key in keys:
572
+ items = [item for item in array if keyfunc(item) == key]
573
+ result.update({key: items})
574
+
575
+ return result
576
+
577
+ def _create_key_func(self, expref, allowed_types, function_name):
578
+ def keyfunc(x):
579
+ result = expref.visit(expref.expression, x)
580
+ actual_typename = type(result).__name__
581
+
582
+ jmespath_type = self._convert_to_jmespath_type(actual_typename)
583
+ # allowed_types is in terms of jmespath types, not python types.
584
+ if jmespath_type not in allowed_types:
585
+ raise exceptions.JmespathTypeError(
586
+ function_name, result, jmespath_type, allowed_types)
587
+
588
+ return result
589
+
590
+ return keyfunc
591
+
592
+ def _convert_to_jmespath_type(self, pyobject):
593
+ return TYPES_MAP.get(pyobject, 'unknown')