jsonata-python 0.5.2__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.
jsonata/signature.py ADDED
@@ -0,0 +1,433 @@
1
+ #
2
+ # Copyright Robert Yokota
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License")
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Derived from the following code:
17
+ #
18
+ # Project name: jsonata-java
19
+ # Copyright Dashjoin GmbH. https://dashjoin.com
20
+ # Licensed under the Apache License, Version 2.0 (the "License")
21
+ #
22
+ # Project name: JSONata4Java
23
+ # (c) Copyright 2018, 2019 IBM Corporation
24
+ # Licensed under the Apache License, Version 2.0 (the "License")
25
+ # 1 New Orchard Road,
26
+ # Armonk, New York, 10504-1722
27
+ # United States
28
+ # +1 914 499 1900
29
+ # support: Nathaniel Mills wnm3@us.ibm.com
30
+ #
31
+
32
+ import re
33
+ from typing import MutableSequence, Optional, Sequence, NoReturn, Any
34
+
35
+ from jsonata import jexception, utils
36
+
37
+
38
+ #
39
+ # Manages signature related functions
40
+ #
41
+ class Signature:
42
+ SERIAL_VERSION_UID = -450755246855587271
43
+
44
+ class Param:
45
+
46
+ type: Optional[str]
47
+ regex: Optional[str]
48
+ context: bool
49
+ array: bool
50
+ subtype: Optional[str]
51
+ context_regex: Optional[str]
52
+
53
+ def __init__(self):
54
+ self.type = None
55
+ self.regex = None
56
+ self.context = False
57
+ self.array = False
58
+ self.subtype = None
59
+ self.context_regex = None
60
+
61
+ def __repr__(self):
62
+ return "Param " + self.type + " regex=" + self.regex + " ctx=" + str(self.context) + " array=" + str(
63
+ self.array)
64
+
65
+ signature: str
66
+ function_name: str
67
+
68
+ _param: 'Signature.Param'
69
+ _params: MutableSequence['Signature.Param']
70
+ _prev_param: 'Signature.Param'
71
+ _regex: Optional[re.Pattern]
72
+ _signature: str
73
+
74
+ def __init__(self, signature, function):
75
+ self._param = Signature.Param()
76
+ self._params = []
77
+ self._prev_param = self._param
78
+ self._regex = None
79
+ self._signature = ""
80
+
81
+ self.function_name = function
82
+ self.parse_signature(signature)
83
+
84
+ def set_function_name(self, function_name: str) -> None:
85
+ self.function_name = function_name
86
+
87
+ @staticmethod
88
+ def main(args: Sequence[str]) -> None:
89
+ s = Signature("<s-:s>", "test") # <s-(sf)(sf)n?:s>");
90
+ print(s._params)
91
+
92
+ def find_closing_bracket(self, string: str, start: int, open_symbol: str, close_symbol: str) -> int:
93
+ # returns the position of the closing symbol (e.g. bracket) in a string
94
+ # that balances the opening symbol at position start
95
+ depth = 1
96
+ position = start
97
+ while position < len(string):
98
+ position += 1
99
+ symbol = string[position]
100
+ if symbol == close_symbol:
101
+ depth -= 1
102
+ if depth == 0:
103
+ # we're done
104
+ break # out of while loop
105
+ elif symbol == open_symbol:
106
+ depth += 1
107
+ return position
108
+
109
+ def get_symbol(self, value: Optional[Any]) -> str:
110
+ from jsonata import functions
111
+ if value is None:
112
+ symbol = "m"
113
+ else:
114
+ # first check to see if this is a function
115
+ if utils.Utils.is_function(value) or functions.Functions.is_lambda(value) or isinstance(value, re.Pattern):
116
+ symbol = "f"
117
+ elif isinstance(value, str):
118
+ symbol = "s"
119
+ elif isinstance(value, bool):
120
+ symbol = "b"
121
+ elif isinstance(value, (int, float)):
122
+ symbol = "n"
123
+ elif isinstance(value, list):
124
+ symbol = "a"
125
+ elif isinstance(value, dict):
126
+ symbol = "o"
127
+ elif value is None: # Uli: is this used???
128
+ symbol = "l"
129
+ else:
130
+ # any value can be undefined, but should be allowed to match
131
+ symbol = "m" # m for missing
132
+ return symbol
133
+
134
+ def next(self) -> None:
135
+ self._params.append(self._param)
136
+ self._prev_param = self._param
137
+ self._param = Signature.Param()
138
+
139
+ #
140
+ # Parses a function signature definition and returns a validation function
141
+ #
142
+ # @param {string}
143
+ # signature - the signature between the <angle brackets>
144
+ # @returns validation pattern
145
+ #
146
+ def parse_signature(self, signature: str) -> Optional[re.Pattern]:
147
+ # create a Regex that represents this signature and return a function that when
148
+ # invoked,
149
+ # returns the validated (possibly fixed-up) arguments, or throws a validation
150
+ # error
151
+ # step through the signature, one symbol at a time
152
+ position = 1
153
+ while position < len(signature):
154
+ symbol = signature[position]
155
+ if symbol == ':':
156
+ # TODO figure out what to do with the return type
157
+ # ignore it for now
158
+ break
159
+
160
+ if symbol == 's' or symbol == 'n' or symbol == 'b' or symbol == 'l' or symbol == 'o':
161
+ self._param.regex = ("[" + symbol + "m]")
162
+ self._param.type = str(symbol)
163
+ self.next()
164
+ elif symbol == 'a':
165
+ # normally treat any value as singleton array
166
+ self._param.regex = "[asnblfom]"
167
+ self._param.type = str(symbol)
168
+ self._param.array = True
169
+ self.next()
170
+ elif symbol == 'f':
171
+ self._param.regex = "f"
172
+ self._param.type = str(symbol)
173
+ self.next()
174
+ elif symbol == 'j':
175
+ self._param.regex = "[asnblom]"
176
+ self._param.type = str(symbol)
177
+ self.next()
178
+ elif symbol == 'x':
179
+ self._param.regex = "[asnblfom]"
180
+ self._param.type = str(symbol)
181
+ self.next()
182
+ elif symbol == '-':
183
+ self._prev_param.context = True
184
+ self._prev_param.regex += "?"
185
+ elif symbol == '?' or symbol == '+':
186
+ self._prev_param.regex += symbol
187
+ elif symbol == '(':
188
+ # search forward for matching ')'
189
+ end_paren = self.find_closing_bracket(signature, position, '(', ')')
190
+ choice = signature[position + 1:end_paren]
191
+ if choice.find("<") == -1:
192
+ # no _parameterized types, simple regex
193
+ self._param.regex = ("[" + choice + "m]")
194
+ else:
195
+ # TODO harder
196
+ raise RuntimeError("Choice groups containing parameterized types are not supported")
197
+ self._param.type = ("(" + choice + ")")
198
+ position = end_paren
199
+ self.next()
200
+ elif symbol == '<':
201
+ test = self._prev_param.type
202
+ if test is not None:
203
+ type = test # .asText();
204
+ if type == "a" or type == "f":
205
+ # search forward for matching '>'
206
+ end_pos = self.find_closing_bracket(signature, position, '<', '>')
207
+ self._prev_param.subtype = signature[position + 1:end_pos]
208
+ position = end_pos
209
+ else:
210
+ raise RuntimeError("Type parameters can only be applied to functions and arrays")
211
+ else:
212
+ raise RuntimeError("Type parameters can only be applied to functions and arrays")
213
+ position += 1 # end while processing symbols in signature
214
+
215
+ regex_str = "^"
216
+ for param in self._params:
217
+ regex_str += "(" + param.regex + ")"
218
+ regex_str += "$"
219
+
220
+ self._regex = re.compile(regex_str)
221
+ self._signature = regex_str
222
+ return self._regex
223
+
224
+ def throw_validation_error(self, bad_args: Optional[Sequence], bad_sig: Optional[str],
225
+ function_name: Optional[str]) -> NoReturn:
226
+ # to figure out where this went wrong we need apply each component of the
227
+ # regex to each argument until we get to the one that fails to match
228
+ partial_pattern = "^"
229
+
230
+ good_to = 0
231
+ for param in self._params:
232
+ partial_pattern += param.regex
233
+ tester = re.compile(partial_pattern)
234
+ match_ = tester.fullmatch(bad_sig)
235
+ if match_ is None:
236
+ # failed here
237
+ raise jexception.JException("T0410", -1, (good_to + 1), function_name)
238
+ good_to = match_.end()
239
+ # if it got this far, it's probably because of extraneous arguments (we
240
+ # haven't added the trailing '$' in the regex yet.
241
+ raise jexception.JException("T0410", -1, (good_to + 1), function_name)
242
+
243
+ def validate(self, args: Any, context: Optional[Any]) -> Optional[Any]:
244
+ supplied_sig = ""
245
+ for arg in args:
246
+ supplied_sig += self.get_symbol(arg)
247
+
248
+ is_valid = self._regex.fullmatch(supplied_sig)
249
+ if is_valid is not None:
250
+ validated_args = []
251
+ arg_index = 0
252
+ index = 0
253
+ for param in self._params:
254
+ arg = args[arg_index] if arg_index < len(args) else None
255
+ match_ = is_valid.group(index + 1)
256
+ if "" == match_:
257
+ if param.context and param.regex is not None:
258
+ # substitute context value for missing arg
259
+ # first check that the context value is the right type
260
+ context_type = self.get_symbol(context)
261
+ # test context_type against the regex for this arg (without the trailing ?)
262
+ if re.fullmatch(param.regex, context_type):
263
+ # if (param.contextRegex.test(context_type)) {
264
+ validated_args.append(context)
265
+ else:
266
+ # context value not compatible with this argument
267
+ raise jexception.JException("T0411", -1, context, arg_index + 1)
268
+ else:
269
+ validated_args.append(arg)
270
+ arg_index += 1
271
+ else:
272
+ # may have matched multiple args (if the regex ends with a '+'
273
+ # split into single tokens
274
+ singles = list(match_) # split on empty separator
275
+ for single in singles:
276
+ # match.split('').forEach(function (single) {
277
+ if param.type == "a":
278
+ if single == "m":
279
+ # missing (undefined)
280
+ arg = None
281
+ else:
282
+ arg = args[arg_index] if arg_index < len(args) else None
283
+ array_ok = True
284
+ # is there type information on the contents of the array?
285
+ if param.subtype is not None:
286
+ if single != "a" and match_ != param.subtype:
287
+ array_ok = False
288
+ elif single == "a":
289
+ arg_arr = arg
290
+ if arg_arr:
291
+ item_type = self.get_symbol(arg_arr[0])
292
+ if item_type != str(param.subtype[0]):
293
+ array_ok = False
294
+ else:
295
+ # make sure every item in the array is this type
296
+ for o in arg_arr:
297
+ if self.get_symbol(o) != item_type:
298
+ array_ok = False
299
+ break
300
+ if not array_ok:
301
+ raise jexception.JException("T0412", -1, arg, param.subtype)
302
+ # the function expects an array. If it's not one, make it so
303
+ if single != "a":
304
+ arg = [arg]
305
+ validated_args.append(arg)
306
+ arg_index += 1
307
+ else:
308
+ arg = args[arg_index] if arg_index < len(args) else None
309
+ validated_args.append(arg)
310
+ arg_index += 1
311
+ return validated_args
312
+ self.throw_validation_error(args, supplied_sig, self.function_name)
313
+
314
+ def get_number_of_args(self) -> int:
315
+ return len(self._params)
316
+
317
+ #
318
+ # Returns the minimum # of arguments.
319
+ # I.e. the # of all non-optional arguments.
320
+ #
321
+ def get_min_number_of_args(self) -> int:
322
+ res = 0
323
+ for p in self._params:
324
+ if "?" not in p.regex:
325
+ res += 1
326
+ return res
327
+ #
328
+ # ArrayNode validate(String functionName, ExprListContext args, ExpressionsVisitor expressionVisitor) {
329
+ # ArrayNode result = JsonNodeFactory.instance.arrayNode()
330
+ # String suppliedSig = ""
331
+ # for (Iterator<ExprContext> it = args.expr().iterator(); it.hasNext();) {
332
+ # ExprContext arg = it.next()
333
+ # suppliedSig += getSymbol(arg)
334
+ # }
335
+ # Matcher isValid = _regex.matcher(suppliedSig)
336
+ # if (isValid != null) {
337
+ # ArrayNode validatedArgs = JsonNodeFactory.instance.arrayNode()
338
+ # int argIndex = 0
339
+ # int index = 0
340
+ # for (Iterator<JsonNode> it = _params.iterator(); it.hasNext();) {
341
+ # ObjectNode param = (ObjectNode) it.next()
342
+ # JsonNode arg = expressionVisitor.visit(args.expr(argIndex))
343
+ # String match = isValid.group(index + 1)
344
+ # if ("".equals(match)) {
345
+ # boolean useContext = (param.get("context") != null && param.get("context").asBoolean())
346
+ # if (useContext) {
347
+ # // substitute context value for missing arg
348
+ # // first check that the context value is the right type
349
+ # JsonNode context = expressionVisitor.getVariable("$")
350
+ # String contextType = getSymbol(context)
351
+ # // test contextType against the regex for this arg (without the trailing ?)
352
+ # if (Pattern.matches(param.get("regex").asText(), contextType)) {
353
+ # validatedArgs.add(context)
354
+ # } else {
355
+ # // context value not compatible with this argument
356
+ # throw new EvaluateRuntimeException("Context value is not a compatible type with argument \""
357
+ # + argIndex + 1 + "\" of function \"" + functionName + "\"")
358
+ # }
359
+ # } else {
360
+ # validatedArgs.add(arg)
361
+ # argIndex++
362
+ # }
363
+ # } else {
364
+ # // may have matched multiple args (if the regex ends with a '+'
365
+ # // split into single tokens
366
+ # String[] singles = match.split("")
367
+ # for (String single : singles) {
368
+ # if ("a".equals(param.get("type").asText())) {
369
+ # if ("m".equals(single)) {
370
+ # // missing (undefined)
371
+ # arg = null
372
+ # } else {
373
+ # arg = expressionVisitor.visit(args.expr(argIndex))
374
+ # boolean arrayOK = true
375
+ # // is there type information on the contents of the array?
376
+ # String subtype = "undefined"
377
+ # JsonNode testSubType = param.get("subtype")
378
+ # if (testSubType != null) {
379
+ # subtype = testSubType.asText()
380
+ # if ("a".equals(single) == false && match.equals(subtype) == false) {
381
+ # arrayOK = false
382
+ # } else if ("a".equals(single)) {
383
+ # ArrayNode argArray = (ArrayNode) arg
384
+ # if (argArray.size() > 0) {
385
+ # String itemType = getSymbol(argArray.get(0))
386
+ # if (itemType.equals(subtype) == false) { // TODO recurse further
387
+ # arrayOK = false
388
+ # } else {
389
+ # // make sure every item in the array is this type
390
+ # ArrayNode differentItems = JsonNodeFactory.instance.arrayNode()
391
+ # for (Object val : argArray) {
392
+ # if (itemType.equals(getSymbol(val)) == false) {
393
+ # differentItems.add(expressionVisitor.visit((ExprListContext) val))
394
+ # }
395
+ # }
396
+ #
397
+ # arrayOK = (differentItems.size() == 0)
398
+ # }
399
+ # }
400
+ # }
401
+ # }
402
+ # if (!arrayOK) {
403
+ # JsonNode type = s_arraySignatureMapping.get(subtype)
404
+ # if (type == null) {
405
+ # type = JsonNodeFactory.instance.nullNode()
406
+ # }
407
+ # throw new EvaluateRuntimeException("Argument \"" + (argIndex + 1) + "\" of function \""
408
+ # + functionName + "\" must be an array of \"" + type.asText() + "\"")
409
+ # }
410
+ # // the function expects an array. If it's not one, make it so
411
+ # if ("a".equals(single) == false) {
412
+ # ArrayNode wrappedArg = JsonNodeFactory.instance.arrayNode()
413
+ # wrappedArg.add(arg)
414
+ # arg = wrappedArg
415
+ # }
416
+ # }
417
+ # validatedArgs.add(arg)
418
+ # argIndex++
419
+ # } else {
420
+ # validatedArgs.add(arg)
421
+ # argIndex++
422
+ # }
423
+ # }
424
+ # }
425
+ # index++
426
+ # }
427
+ # return validatedArgs
428
+ # }
429
+ # throwValidationError(args, suppliedSig, functionName)
430
+ # // below just for the compiler as a runtime exception is thrown above
431
+ # return result
432
+ # }
433
+ #
jsonata/timebox.py ADDED
@@ -0,0 +1,89 @@
1
+ #
2
+ # Copyright Robert Yokota
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License")
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Derived from the following code:
17
+ #
18
+ # Project name: jsonata-java
19
+ # Copyright Dashjoin GmbH. https://dashjoin.com
20
+ # Licensed under the Apache License, Version 2.0 (the "License")
21
+ #
22
+
23
+ import time
24
+
25
+ from jsonata import jexception
26
+
27
+
28
+ #
29
+ # Configure max runtime / max recursion depth.
30
+ # See Frame.setRuntimeBounds - usually not used directly
31
+ #
32
+ class Timebox:
33
+ #
34
+ # Protect the process/browser from a runnaway expression
35
+ # i.e. Infinite loop (tail recursion), or excessive stack growth
36
+ #
37
+ # @param {Object} expr - expression to protect
38
+ # @param {Number} timeout - max time in ms
39
+ # @param {Number} max_depth - max stack depth
40
+ #
41
+
42
+ timeout: int
43
+ max_depth: int
44
+ time: int
45
+ depth: int
46
+
47
+ def __init__(self, expr, timeout=10000, max_depth=100):
48
+ self.timeout = timeout
49
+ self.max_depth = max_depth
50
+ self.time = Timebox.current_milli_time()
51
+ self.depth = 0
52
+
53
+ # register callbacks
54
+ def entry_callback(exp, input, env):
55
+ if env.is_parallel_call:
56
+ return
57
+ self.depth += 1
58
+ self.check_runaway()
59
+
60
+ expr.set_evaluate_entry_callback(entry_callback)
61
+
62
+ def exit_callback(exp, input, env, res):
63
+ if env.is_parallel_call:
64
+ return
65
+ self.depth -= 1
66
+ self.check_runaway()
67
+
68
+ expr.set_evaluate_exit_callback(exit_callback)
69
+
70
+ def check_runaway(self) -> None:
71
+ if self.depth > self.max_depth:
72
+ # stack too deep
73
+ raise jexception.JException(
74
+ "Stack overflow error: Check for non-terminating recursive function. Consider rewriting as tail-recursive. Depth=" + str(
75
+ self.depth) + " max=" + str(self.max_depth), -1)
76
+ # stack: new Error().stack,
77
+ # code: "U1001"
78
+ # }
79
+ if Timebox.current_milli_time() - self.time > self.timeout:
80
+ # expression has run for too long
81
+ raise jexception.JException(
82
+ "Expression evaluation timeout: " + str(self.timeout) + "ms. Check for infinite loop", -1)
83
+ # stack: new Error().stack,
84
+ # code: "U1001"
85
+ # }
86
+
87
+ @staticmethod
88
+ def current_milli_time() -> int:
89
+ return round(time.time() * 1000)