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