bare-script 0.9.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.
- bare_script/__init__.py +33 -0
- bare_script/__main__.py +12 -0
- bare_script/bare.py +109 -0
- bare_script/baredoc.py +137 -0
- bare_script/library.py +1912 -0
- bare_script/model.py +257 -0
- bare_script/options.py +108 -0
- bare_script/parser.py +680 -0
- bare_script/runtime.py +345 -0
- bare_script/value.py +199 -0
- bare_script-0.9.0.dist-info/LICENSE +21 -0
- bare_script-0.9.0.dist-info/METADATA +189 -0
- bare_script-0.9.0.dist-info/RECORD +16 -0
- bare_script-0.9.0.dist-info/WHEEL +5 -0
- bare_script-0.9.0.dist-info/entry_points.txt +3 -0
- bare_script-0.9.0.dist-info/top_level.txt +1 -0
bare_script/runtime.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Licensed under the MIT License
|
|
2
|
+
# https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The BareScript runtime
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
import functools
|
|
10
|
+
|
|
11
|
+
from .library import DEFAULT_MAX_STATEMENTS, EXPRESSION_FUNCTIONS, SCRIPT_FUNCTIONS, default_args
|
|
12
|
+
from .model import lint_script
|
|
13
|
+
from .options import url_file_relative
|
|
14
|
+
from .parser import BareScriptParserError, parse_script
|
|
15
|
+
from .value import round_number, value_boolean, value_compare, value_string
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute_script(script, options=None):
|
|
19
|
+
"""
|
|
20
|
+
Execute a BareScript model
|
|
21
|
+
|
|
22
|
+
:param script: The `BareScript model <https://craigahobbs.github.io/bare-script-py/model/#var.vName='BareScript'>`__
|
|
23
|
+
:type script: dict
|
|
24
|
+
:param options: The :class:`script execution options <ExecuteScriptOptions>`
|
|
25
|
+
:type options: dict or None, optional
|
|
26
|
+
:returns: The script result
|
|
27
|
+
:raises BareScriptRuntimeError: A script runtime error occurred
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
if options is None:
|
|
31
|
+
options = {}
|
|
32
|
+
|
|
33
|
+
# Create the global variable object, if necessary
|
|
34
|
+
globals_ = options.get('globals')
|
|
35
|
+
if globals_ is None:
|
|
36
|
+
globals_ = {}
|
|
37
|
+
options['globals'] = globals_
|
|
38
|
+
|
|
39
|
+
# Set the script function globals variables
|
|
40
|
+
for script_func_name, script_func in SCRIPT_FUNCTIONS.items():
|
|
41
|
+
if script_func_name not in globals_:
|
|
42
|
+
globals_[script_func_name] = script_func
|
|
43
|
+
|
|
44
|
+
# Execute the script
|
|
45
|
+
options['statementCount'] = 0
|
|
46
|
+
return _execute_script_helper(script['statements'], options, None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _execute_script_helper(statements, options, locals_):
|
|
50
|
+
globals_ = options['globals']
|
|
51
|
+
|
|
52
|
+
# Iterate each script statement
|
|
53
|
+
label_indexes = None
|
|
54
|
+
statements_length = len(statements)
|
|
55
|
+
ix_statement = 0
|
|
56
|
+
while ix_statement < statements_length:
|
|
57
|
+
statement = statements[ix_statement]
|
|
58
|
+
statement_key = next(iter(statement.keys()))
|
|
59
|
+
|
|
60
|
+
# Increment the statement counter
|
|
61
|
+
max_statements = options.get('maxStatements', DEFAULT_MAX_STATEMENTS)
|
|
62
|
+
if max_statements > 0:
|
|
63
|
+
options['statementCount'] = options.get('statementCount', 0) + 1
|
|
64
|
+
if options['statementCount'] > max_statements:
|
|
65
|
+
raise BareScriptRuntimeError(f'Exceeded maximum script statements ({max_statements})')
|
|
66
|
+
|
|
67
|
+
# Expression?
|
|
68
|
+
if statement_key == 'expr':
|
|
69
|
+
expr_value = evaluate_expression(statement['expr']['expr'], options, locals_, False)
|
|
70
|
+
expr_name = statement['expr'].get('name')
|
|
71
|
+
if expr_name is not None:
|
|
72
|
+
if locals_ is not None:
|
|
73
|
+
locals_[expr_name] = expr_value
|
|
74
|
+
else:
|
|
75
|
+
globals_[expr_name] = expr_value
|
|
76
|
+
|
|
77
|
+
# Jump?
|
|
78
|
+
elif statement_key == 'jump':
|
|
79
|
+
# Evaluate the expression (if any)
|
|
80
|
+
if 'expr' not in statement['jump'] or value_boolean(evaluate_expression(statement['jump']['expr'], options, locals_, False)):
|
|
81
|
+
# Find the label
|
|
82
|
+
if label_indexes is not None and statement['jump']['label'] in label_indexes:
|
|
83
|
+
ix_statement = label_indexes[statement['jump']['label']]
|
|
84
|
+
else:
|
|
85
|
+
ix_label = next(
|
|
86
|
+
(ix_stmt for ix_stmt, stmt in enumerate(statements) if stmt.get('label') == statement['jump']['label']),
|
|
87
|
+
-1
|
|
88
|
+
)
|
|
89
|
+
if ix_label == -1:
|
|
90
|
+
raise BareScriptRuntimeError(f"Unknown jump label \"{statement['jump']['label']}\"")
|
|
91
|
+
if label_indexes is None:
|
|
92
|
+
label_indexes = {}
|
|
93
|
+
label_indexes[statement['jump']['label']] = ix_label
|
|
94
|
+
ix_statement = ix_label
|
|
95
|
+
|
|
96
|
+
# Return?
|
|
97
|
+
elif statement_key == 'return':
|
|
98
|
+
if 'expr' in statement['return']:
|
|
99
|
+
return evaluate_expression(statement['return']['expr'], options, locals_, False)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Function?
|
|
103
|
+
elif statement_key == 'function':
|
|
104
|
+
globals_[statement['function']['name']] = functools.partial(_script_function, statement['function'])
|
|
105
|
+
|
|
106
|
+
# Include?
|
|
107
|
+
elif statement_key == 'include':
|
|
108
|
+
system_prefix = options.get('systemPrefix')
|
|
109
|
+
fetch_fn = options.get('fetchFn')
|
|
110
|
+
log_fn = options.get('logFn')
|
|
111
|
+
url_fn = options.get('urlFn')
|
|
112
|
+
for include in statement['include']['includes']:
|
|
113
|
+
url = include['url']
|
|
114
|
+
|
|
115
|
+
# Fixup system include URL
|
|
116
|
+
if include.get('system') and system_prefix is not None:
|
|
117
|
+
url = url_file_relative(system_prefix, url)
|
|
118
|
+
elif url_fn is not None:
|
|
119
|
+
url = url_fn(url)
|
|
120
|
+
|
|
121
|
+
# Fetch the URL
|
|
122
|
+
try:
|
|
123
|
+
script_text = fetch_fn({'url': url}) if fetch_fn is not None else None
|
|
124
|
+
except: # pylint: disable=bare-except
|
|
125
|
+
script_text = None
|
|
126
|
+
if script_text is None:
|
|
127
|
+
raise BareScriptRuntimeError(f'Include of "{url}" failed')
|
|
128
|
+
|
|
129
|
+
# Parse the include script
|
|
130
|
+
try:
|
|
131
|
+
script = parse_script(script_text)
|
|
132
|
+
except BareScriptParserError as exc:
|
|
133
|
+
raise BareScriptParserError(exc.error, exc.line, exc.column_number, exc.line_number, f'Included from "{url}"')
|
|
134
|
+
|
|
135
|
+
# Run the bare-script linter?
|
|
136
|
+
if log_fn is not None and options.get('debug'):
|
|
137
|
+
warnings = lint_script(script)
|
|
138
|
+
if warnings:
|
|
139
|
+
warning_prefix = f'BareScript: Include "{url}" static analysis...'
|
|
140
|
+
log_fn(f'{warning_prefix} {len(warnings)} warning${"s" if len(warnings) > 1 else ""}:')
|
|
141
|
+
for warning in warnings:
|
|
142
|
+
log_fn(f'BareScript: {warning}')
|
|
143
|
+
|
|
144
|
+
# Execute the include script
|
|
145
|
+
include_options = options.copy()
|
|
146
|
+
include_options['urlFn'] = functools.partial(url_file_relative, url)
|
|
147
|
+
_execute_script_helper(script['statements'], include_options, None)
|
|
148
|
+
|
|
149
|
+
# Increment the statement counter
|
|
150
|
+
ix_statement += 1
|
|
151
|
+
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Runtime script function implementation
|
|
156
|
+
def _script_function(function, args, options):
|
|
157
|
+
func_locals = {}
|
|
158
|
+
func_args = function.get('args')
|
|
159
|
+
if func_args is not None:
|
|
160
|
+
args_length = len(args)
|
|
161
|
+
func_args_length = len(func_args)
|
|
162
|
+
ix_arg_last = function.get('lastArgArray', None) and (func_args_length - 1)
|
|
163
|
+
for ix_arg in range(func_args_length):
|
|
164
|
+
arg_name = func_args[ix_arg]
|
|
165
|
+
if ix_arg < args_length:
|
|
166
|
+
func_locals[arg_name] = args[ix_arg] if ix_arg != ix_arg_last else args[ix_arg:]
|
|
167
|
+
else:
|
|
168
|
+
func_locals[arg_name] = [] if ix_arg == ix_arg_last else None
|
|
169
|
+
return _execute_script_helper(function['statements'], options, func_locals)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def evaluate_expression(expr, options=None, locals_=None, builtins=True):
|
|
173
|
+
"""
|
|
174
|
+
Evaluate an expression model
|
|
175
|
+
|
|
176
|
+
:param script: The `expression model <https://craigahobbs.github.io/bare-script-py/model/#var.vName='Expression'>`__
|
|
177
|
+
:type script: dict
|
|
178
|
+
:param options: The :class:`script execution options <ExecuteScriptOptions>`
|
|
179
|
+
:type options: dict or None, optional
|
|
180
|
+
:param locals_: The local variables
|
|
181
|
+
:type locals_: dict or None, optional
|
|
182
|
+
:param builtins: If true, include the
|
|
183
|
+
`built-in expression functions <https://craigahobbs.github.io/bare-script-py/library/expression.html>`__
|
|
184
|
+
:type builtins: bool, optional
|
|
185
|
+
:returns: The expression result
|
|
186
|
+
:raises BareScriptRuntimeError: A script runtime error occurred
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
expr_key, = expr.keys()
|
|
190
|
+
globals_ = options.get('globals') if options is not None else None
|
|
191
|
+
|
|
192
|
+
# Number
|
|
193
|
+
if expr_key == 'number':
|
|
194
|
+
return expr['number']
|
|
195
|
+
|
|
196
|
+
# String
|
|
197
|
+
if expr_key == 'string':
|
|
198
|
+
return expr['string']
|
|
199
|
+
|
|
200
|
+
# Variable
|
|
201
|
+
if expr_key == 'variable':
|
|
202
|
+
# Keywords
|
|
203
|
+
if expr['variable'] == 'null':
|
|
204
|
+
return None
|
|
205
|
+
if expr['variable'] == 'false':
|
|
206
|
+
return False
|
|
207
|
+
if expr['variable'] == 'true':
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
# Get the local or global variable value or None if undefined
|
|
211
|
+
if locals_ is not None and expr['variable'] in locals_:
|
|
212
|
+
return locals_[expr['variable']]
|
|
213
|
+
else:
|
|
214
|
+
return globals_.get(expr['variable']) if globals_ is not None else None
|
|
215
|
+
|
|
216
|
+
# Function
|
|
217
|
+
if expr_key == 'function':
|
|
218
|
+
# "if" built-in function?
|
|
219
|
+
func_name = expr['function']['name']
|
|
220
|
+
if func_name == 'if':
|
|
221
|
+
value_expr, true_expr, false_expr = default_args(expr['function'].get('args', ()), (None, None, None))
|
|
222
|
+
value = evaluate_expression(value_expr, options, locals_, builtins) if value_expr else False
|
|
223
|
+
result_expr = true_expr if value_boolean(value) else false_expr
|
|
224
|
+
return evaluate_expression(result_expr, options, locals_, builtins) if result_expr else None
|
|
225
|
+
|
|
226
|
+
# Compute the function arguments
|
|
227
|
+
func_args = [evaluate_expression(arg, options, locals_, builtins) for arg in expr['function']['args']] \
|
|
228
|
+
if 'args' in expr['function'] else None
|
|
229
|
+
|
|
230
|
+
# Global/local function?
|
|
231
|
+
if locals_ is not None and func_name in locals_:
|
|
232
|
+
func_value = locals_[func_name]
|
|
233
|
+
elif globals_ is not None and func_name in globals_:
|
|
234
|
+
func_value = globals_[func_name]
|
|
235
|
+
else:
|
|
236
|
+
func_value = EXPRESSION_FUNCTIONS.get(func_name) if builtins else None
|
|
237
|
+
if func_value is not None:
|
|
238
|
+
# Call the function
|
|
239
|
+
try:
|
|
240
|
+
return func_value(func_args, options)
|
|
241
|
+
except BareScriptRuntimeError:
|
|
242
|
+
raise
|
|
243
|
+
except Exception as error: # pylint: disable=broad-exception-caught
|
|
244
|
+
# Log and return null
|
|
245
|
+
if options is not None and 'logFn' in options and options.get('debug'):
|
|
246
|
+
options['logFn'](f'BareScript: Function "{func_name}" failed with error: {error}')
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
raise BareScriptRuntimeError(f'Undefined function "{func_name}"')
|
|
250
|
+
|
|
251
|
+
# Binary expression
|
|
252
|
+
if expr_key == 'binary':
|
|
253
|
+
bin_op = expr['binary']['op']
|
|
254
|
+
left_value = evaluate_expression(expr['binary']['left'], options, locals_, builtins)
|
|
255
|
+
|
|
256
|
+
# Short-circuiting binary operators
|
|
257
|
+
if bin_op == '&&':
|
|
258
|
+
if not value_boolean(left_value):
|
|
259
|
+
return left_value
|
|
260
|
+
else:
|
|
261
|
+
return evaluate_expression(expr['binary']['right'], options, locals_, builtins)
|
|
262
|
+
elif bin_op == '||':
|
|
263
|
+
if value_boolean(left_value):
|
|
264
|
+
return left_value
|
|
265
|
+
else:
|
|
266
|
+
return evaluate_expression(expr['binary']['right'], options, locals_, builtins)
|
|
267
|
+
|
|
268
|
+
# Non-short-circuiting binary operators
|
|
269
|
+
right_value = evaluate_expression(expr['binary']['right'], options, locals_, builtins)
|
|
270
|
+
if bin_op == '+':
|
|
271
|
+
if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
|
|
272
|
+
return left_value + right_value
|
|
273
|
+
elif isinstance(left_value, str) and isinstance(right_value, str):
|
|
274
|
+
return left_value + right_value
|
|
275
|
+
elif isinstance(left_value, str):
|
|
276
|
+
return left_value + value_string(right_value)
|
|
277
|
+
elif isinstance(right_value, str):
|
|
278
|
+
return value_string(left_value) + right_value
|
|
279
|
+
elif isinstance(left_value, datetime.datetime) and isinstance(right_value, (int, float)):
|
|
280
|
+
return left_value + datetime.timedelta(milliseconds=right_value)
|
|
281
|
+
elif isinstance(left_value, (int, float)) and isinstance(right_value, datetime.datetime):
|
|
282
|
+
return right_value + datetime.timedelta(milliseconds=left_value)
|
|
283
|
+
elif bin_op == '-':
|
|
284
|
+
if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
|
|
285
|
+
return left_value - right_value
|
|
286
|
+
elif isinstance(left_value, datetime.datetime) and isinstance(right_value, datetime.datetime):
|
|
287
|
+
return round_number((left_value - right_value).total_seconds() * 1000, 0)
|
|
288
|
+
elif bin_op == '*':
|
|
289
|
+
if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
|
|
290
|
+
return left_value * right_value
|
|
291
|
+
elif bin_op == '/':
|
|
292
|
+
if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
|
|
293
|
+
return left_value / right_value
|
|
294
|
+
elif bin_op == '==':
|
|
295
|
+
return value_compare(left_value, right_value) == 0
|
|
296
|
+
elif bin_op == '!=':
|
|
297
|
+
return value_compare(left_value, right_value) != 0
|
|
298
|
+
elif bin_op == '<=':
|
|
299
|
+
return value_compare(left_value, right_value) <= 0
|
|
300
|
+
elif bin_op == '<':
|
|
301
|
+
return value_compare(left_value, right_value) < 0
|
|
302
|
+
elif bin_op == '>=':
|
|
303
|
+
return value_compare(left_value, right_value) >= 0
|
|
304
|
+
elif bin_op == '>':
|
|
305
|
+
return value_compare(left_value, right_value) > 0
|
|
306
|
+
elif bin_op == '%':
|
|
307
|
+
if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
|
|
308
|
+
return left_value % right_value
|
|
309
|
+
else:
|
|
310
|
+
# bin_op == '**':
|
|
311
|
+
if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
|
|
312
|
+
return left_value ** right_value
|
|
313
|
+
|
|
314
|
+
# Invalid operation values
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
# Unary expression
|
|
318
|
+
if expr_key == 'unary':
|
|
319
|
+
unary_op = expr['unary']['op']
|
|
320
|
+
value = evaluate_expression(expr['unary']['expr'], options, locals_, builtins)
|
|
321
|
+
if unary_op == '!':
|
|
322
|
+
return not value_boolean(value)
|
|
323
|
+
else:
|
|
324
|
+
# unary_op == '-'
|
|
325
|
+
if isinstance(value, (int, float)):
|
|
326
|
+
return -value
|
|
327
|
+
|
|
328
|
+
# Invalid operation value
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
# Expression group
|
|
332
|
+
# expr_key == 'group'
|
|
333
|
+
return evaluate_expression(expr['group'], options, locals_, builtins)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class BareScriptRuntimeError(Exception):
|
|
337
|
+
"""
|
|
338
|
+
A BareScript runtime error
|
|
339
|
+
|
|
340
|
+
:param message: The runtime error message
|
|
341
|
+
:type message: str
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
def __init__(self, message):
|
|
345
|
+
super().__init__(message)
|
bare_script/value.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Licensed under the MIT License
|
|
2
|
+
# https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
BareScript value utilities
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from schema_markdown import JSONEncoder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def round_number(value, digits):
|
|
15
|
+
"""
|
|
16
|
+
Round a number
|
|
17
|
+
|
|
18
|
+
:param value: The number to round
|
|
19
|
+
:type value: int or float
|
|
20
|
+
:param digits: The number of digits of precision
|
|
21
|
+
:type digits: int
|
|
22
|
+
:return: The rounded number
|
|
23
|
+
:rtype: float
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
multiplier = 10 ** digits
|
|
27
|
+
return int(value * multiplier + (0.5 if value >= 0 else -0.5)) / multiplier
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def value_type(value):
|
|
31
|
+
"""
|
|
32
|
+
Get a value's type string
|
|
33
|
+
|
|
34
|
+
:param value: The value
|
|
35
|
+
:return: The type string ('array', 'boolean', 'datetime', 'function', 'null', 'number', 'object', 'regex', 'string')
|
|
36
|
+
:rtype: str
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
if value is None:
|
|
40
|
+
return 'null'
|
|
41
|
+
elif isinstance(value, str):
|
|
42
|
+
return 'string'
|
|
43
|
+
elif isinstance(value, bool):
|
|
44
|
+
return 'boolean'
|
|
45
|
+
elif isinstance(value, (int, float)):
|
|
46
|
+
return 'number'
|
|
47
|
+
elif isinstance(value, datetime.datetime):
|
|
48
|
+
return 'datetime'
|
|
49
|
+
elif isinstance(value, dict):
|
|
50
|
+
return 'object'
|
|
51
|
+
elif isinstance(value, list):
|
|
52
|
+
return 'array'
|
|
53
|
+
elif callable(value):
|
|
54
|
+
return 'function'
|
|
55
|
+
elif isinstance(value, REGEX_TYPE):
|
|
56
|
+
return 'regex'
|
|
57
|
+
|
|
58
|
+
# Unknown value type
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
REGEX_TYPE = type(re.compile(''))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def value_string(value):
|
|
65
|
+
"""
|
|
66
|
+
Get a value's string representation
|
|
67
|
+
|
|
68
|
+
:param value: The value
|
|
69
|
+
:return: The value as a string
|
|
70
|
+
:rtype: str
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if value is None:
|
|
74
|
+
return 'null'
|
|
75
|
+
elif isinstance(value, str):
|
|
76
|
+
return value
|
|
77
|
+
elif isinstance(value, bool):
|
|
78
|
+
return 'true' if value else 'false'
|
|
79
|
+
elif isinstance(value, int):
|
|
80
|
+
return str(value)
|
|
81
|
+
elif isinstance(value, float):
|
|
82
|
+
return R_NUMBER_CLEANUP.sub('', str(value))
|
|
83
|
+
elif isinstance(value, datetime.datetime):
|
|
84
|
+
return value.isoformat()
|
|
85
|
+
elif isinstance(value, (dict)):
|
|
86
|
+
return value_json(value)
|
|
87
|
+
elif isinstance(value, (list)):
|
|
88
|
+
return value_json(value)
|
|
89
|
+
elif callable(value):
|
|
90
|
+
return '<function>'
|
|
91
|
+
elif isinstance(value, REGEX_TYPE):
|
|
92
|
+
return '<regex>'
|
|
93
|
+
|
|
94
|
+
# Unknown value type
|
|
95
|
+
return '<unknown>'
|
|
96
|
+
|
|
97
|
+
R_NUMBER_CLEANUP = re.compile(r'\.0*$')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def value_json(value, indent=None):
|
|
101
|
+
"""
|
|
102
|
+
Get a value's JSON string representation
|
|
103
|
+
|
|
104
|
+
:param value: The value
|
|
105
|
+
:param indent: The JSON indent
|
|
106
|
+
:type indent: int
|
|
107
|
+
:return: The value as a JSON string
|
|
108
|
+
:rtype: str
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
if indent is not None and indent > 0:
|
|
112
|
+
return JSONEncoder(allow_nan=False, indent=indent, separators=(',', ': '), sort_keys=True).encode(value)
|
|
113
|
+
else:
|
|
114
|
+
return JSONEncoder(allow_nan=False, separators=(',', ':'), sort_keys=True).encode(value)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def value_boolean(value):
|
|
118
|
+
"""
|
|
119
|
+
Interpret the value as a boolean
|
|
120
|
+
|
|
121
|
+
:param value: The value
|
|
122
|
+
:return: The value as a boolean
|
|
123
|
+
:rtype: bool
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if value is None:
|
|
127
|
+
return False
|
|
128
|
+
elif isinstance(value, str):
|
|
129
|
+
return value != ''
|
|
130
|
+
elif isinstance(value, bool):
|
|
131
|
+
return value
|
|
132
|
+
elif isinstance(value, (int, float)):
|
|
133
|
+
return value != 0
|
|
134
|
+
elif isinstance(value, datetime.datetime):
|
|
135
|
+
return True
|
|
136
|
+
elif isinstance(value, dict):
|
|
137
|
+
return True
|
|
138
|
+
elif isinstance(value, list):
|
|
139
|
+
return len(value) != 0
|
|
140
|
+
elif callable(value):
|
|
141
|
+
return True
|
|
142
|
+
elif isinstance(value, REGEX_TYPE):
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
# Unknown value type
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def value_is(value1, value2):
|
|
150
|
+
"""
|
|
151
|
+
Test if one value is the same object as another
|
|
152
|
+
|
|
153
|
+
:param value1: The first value
|
|
154
|
+
:param value2: The second value
|
|
155
|
+
:return: True if values are the same object, false otherwise
|
|
156
|
+
:rtype: bool
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
if isinstance(value1, (int, float)) and not isinstance(value1, bool) and \
|
|
160
|
+
isinstance(value2, (int, float)) and not isinstance(value2, bool):
|
|
161
|
+
return value1 == value2
|
|
162
|
+
|
|
163
|
+
return value1 is value2
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def value_compare(left, right):
|
|
167
|
+
"""
|
|
168
|
+
Compare two values
|
|
169
|
+
|
|
170
|
+
:param left: The left value
|
|
171
|
+
:param right: The right value
|
|
172
|
+
:return: -1 if the left value is less than the right value, 0 if equal, and 1 if greater than
|
|
173
|
+
:rtype: int
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
if left is None:
|
|
177
|
+
return 0 if right is None else -1
|
|
178
|
+
elif right is None:
|
|
179
|
+
return 1
|
|
180
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
181
|
+
return -1 if left < right else (0 if left == right else 1)
|
|
182
|
+
elif isinstance(left, bool) and isinstance(right, bool):
|
|
183
|
+
return -1 if left < right else (0 if left == right else 1)
|
|
184
|
+
elif isinstance(left, (int, float)) and not isinstance(left, bool) and \
|
|
185
|
+
isinstance(right, (int, float)) and not isinstance(right, bool):
|
|
186
|
+
return -1 if left < right else (0 if left == right else 1)
|
|
187
|
+
elif isinstance(left, datetime.datetime) and isinstance(right, datetime.datetime):
|
|
188
|
+
return -1 if left < right else (0 if left == right else 1)
|
|
189
|
+
elif isinstance(left, list) and isinstance(right, list):
|
|
190
|
+
for ix in range(min(len(left), len(right))):
|
|
191
|
+
item_compare = value_compare(left[ix], right[ix])
|
|
192
|
+
if item_compare != 0:
|
|
193
|
+
return item_compare
|
|
194
|
+
return -1 if len(left) < len(right) else (0 if len(left) == len(right) else 1)
|
|
195
|
+
|
|
196
|
+
# Invalid comparison - compare by type name
|
|
197
|
+
type1 = value_type(left) or 'unknown'
|
|
198
|
+
type2 = value_type(right) or 'unknown'
|
|
199
|
+
return -1 if type1 < type2 else (0 if type1 == type2 else 1)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Craig A. Hobbs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|