mimo-lang 1.1.1 → 2.0.6
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.
- package/.gitattributes +24 -0
- package/LICENSE +21 -0
- package/README.md +71 -39
- package/adapters/browserAdapter.js +86 -0
- package/adapters/nodeAdapter.js +101 -0
- package/bin/cli.js +80 -0
- package/bin/commands/convert.js +27 -0
- package/bin/commands/doctor.js +139 -0
- package/bin/commands/eval.js +39 -0
- package/bin/commands/fmt.js +109 -0
- package/bin/commands/help.js +72 -0
- package/bin/commands/lint.js +117 -0
- package/bin/commands/repl.js +24 -0
- package/bin/commands/run.js +64 -0
- package/bin/commands/test.js +126 -0
- package/bin/utils/colors.js +38 -0
- package/bin/utils/formatError.js +47 -0
- package/bin/utils/fs.js +57 -0
- package/bin/utils/version.js +8 -0
- package/build.js +18 -0
- package/bun.lock +74 -0
- package/index.js +48 -77
- package/index.web.js +364 -0
- package/interpreter/BuiltinFunction.js +32 -0
- package/interpreter/ErrorHandler.js +120 -0
- package/interpreter/ExpressionEvaluator.js +106 -0
- package/interpreter/Interpreter.js +172 -0
- package/interpreter/MimoError.js +112 -0
- package/interpreter/ModuleLoader.js +236 -0
- package/interpreter/StatementExecutor.js +107 -0
- package/interpreter/Utils.js +82 -0
- package/interpreter/Values.js +87 -0
- package/interpreter/coreBuiltins.js +490 -0
- package/interpreter/environment.js +99 -0
- package/interpreter/evaluators/binaryExpressionEvaluator.js +111 -0
- package/interpreter/evaluators/collectionEvaluator.js +151 -0
- package/interpreter/evaluators/functionCallEvaluator.js +76 -0
- package/interpreter/evaluators/literalEvaluator.js +27 -0
- package/interpreter/evaluators/moduleAccessEvaluator.js +25 -0
- package/interpreter/evaluators/templateLiteralEvaluator.js +20 -0
- package/interpreter/executors/BaseExecutor.js +37 -0
- package/interpreter/executors/ControlFlowExecutor.js +206 -0
- package/interpreter/executors/FunctionExecutor.js +126 -0
- package/interpreter/executors/PatternMatchExecutor.js +93 -0
- package/interpreter/executors/VariableExecutor.js +144 -0
- package/interpreter/index.js +8 -0
- package/interpreter/stdlib/array/accessFunctions.js +61 -0
- package/interpreter/stdlib/array/arrayUtils.js +36 -0
- package/interpreter/stdlib/array/higherOrderFunctions.js +285 -0
- package/interpreter/stdlib/array/searchFunctions.js +77 -0
- package/interpreter/stdlib/array/setFunctions.js +49 -0
- package/interpreter/stdlib/array/transformationFunctions.js +68 -0
- package/interpreter/stdlib/array.js +85 -0
- package/interpreter/stdlib/assert.js +143 -0
- package/interpreter/stdlib/datetime.js +170 -0
- package/interpreter/stdlib/env.js +54 -0
- package/interpreter/stdlib/fs.js +161 -0
- package/interpreter/stdlib/http.js +92 -0
- package/interpreter/stdlib/json.js +70 -0
- package/interpreter/stdlib/math.js +309 -0
- package/interpreter/stdlib/object.js +142 -0
- package/interpreter/stdlib/path.js +69 -0
- package/interpreter/stdlib/regex.js +134 -0
- package/interpreter/stdlib/string.js +260 -0
- package/interpreter/suggestions.js +46 -0
- package/lexer/Lexer.js +245 -0
- package/lexer/TokenTypes.js +131 -0
- package/lexer/createToken.js +11 -0
- package/lexer/tokenizers/commentTokenizer.js +45 -0
- package/lexer/tokenizers/literalTokenizer.js +163 -0
- package/lexer/tokenizers/symbolTokenizer.js +69 -0
- package/lexer/tokenizers/whitespaceTokenizer.js +36 -0
- package/package.json +29 -13
- package/parser/ASTNodes.js +448 -0
- package/parser/Parser.js +188 -0
- package/parser/expressions/atomicExpressions.js +165 -0
- package/parser/expressions/conditionalExpressions.js +0 -0
- package/parser/expressions/operatorExpressions.js +79 -0
- package/parser/expressions/primaryExpressions.js +77 -0
- package/parser/parseStatement.js +184 -0
- package/parser/parserExpressions.js +115 -0
- package/parser/parserUtils.js +19 -0
- package/parser/statements/controlFlowParsers.js +106 -0
- package/parser/statements/functionParsers.js +314 -0
- package/parser/statements/moduleParsers.js +57 -0
- package/parser/statements/patternMatchParsers.js +124 -0
- package/parser/statements/variableParsers.js +155 -0
- package/repl.js +325 -0
- package/test.js +47 -0
- package/tools/PrettyPrinter.js +3 -0
- package/tools/convert/Args.js +46 -0
- package/tools/convert/Registry.js +91 -0
- package/tools/convert/Transpiler.js +78 -0
- package/tools/convert/plugins/README.md +66 -0
- package/tools/convert/plugins/alya/index.js +10 -0
- package/tools/convert/plugins/alya/to_alya.js +289 -0
- package/tools/convert/plugins/alya/visitors/expressions.js +257 -0
- package/tools/convert/plugins/alya/visitors/statements.js +403 -0
- package/tools/convert/plugins/base_converter.js +228 -0
- package/tools/convert/plugins/javascript/index.js +10 -0
- package/tools/convert/plugins/javascript/mimo_runtime.js +265 -0
- package/tools/convert/plugins/javascript/to_js.js +155 -0
- package/tools/convert/plugins/javascript/visitors/expressions.js +197 -0
- package/tools/convert/plugins/javascript/visitors/patterns.js +102 -0
- package/tools/convert/plugins/javascript/visitors/statements.js +236 -0
- package/tools/convert/plugins/python/index.js +10 -0
- package/tools/convert/plugins/python/mimo_runtime.py +811 -0
- package/tools/convert/plugins/python/to_py.js +329 -0
- package/tools/convert/plugins/python/visitors/expressions.js +272 -0
- package/tools/convert/plugins/python/visitors/patterns.js +100 -0
- package/tools/convert/plugins/python/visitors/statements.js +257 -0
- package/tools/convert.js +102 -0
- package/tools/format/CommentAttacher.js +190 -0
- package/tools/format/CommentLexer.js +152 -0
- package/tools/format/Printer.js +849 -0
- package/tools/format/config.js +107 -0
- package/tools/formatter.js +169 -0
- package/tools/lint/Linter.js +391 -0
- package/tools/lint/config.js +114 -0
- package/tools/lint/rules/consistent-return.js +62 -0
- package/tools/lint/rules/max-depth.js +56 -0
- package/tools/lint/rules/no-empty-function.js +45 -0
- package/tools/lint/rules/no-magic-numbers.js +46 -0
- package/tools/lint/rules/no-shadow.js +113 -0
- package/tools/lint/rules/no-unused-vars.js +26 -0
- package/tools/lint/rules/prefer-const.js +19 -0
- package/tools/linter.js +261 -0
- package/tools/replFormatter.js +93 -0
- package/tools/stamp-version.js +32 -0
- package/web/index.js +9 -0
- package/bun.lockb +0 -0
- package/cli.js +0 -84
- package/compiler/execute/interpreter.js +0 -68
- package/compiler/execute/interpreters/binary.js +0 -12
- package/compiler/execute/interpreters/call.js +0 -10
- package/compiler/execute/interpreters/if.js +0 -10
- package/compiler/execute/interpreters/try-catch.js +0 -10
- package/compiler/execute/interpreters/while.js +0 -8
- package/compiler/execute/utils/createfunction.js +0 -11
- package/compiler/execute/utils/evaluate.js +0 -20
- package/compiler/execute/utils/operate.js +0 -23
- package/compiler/lexer/processToken.js +0 -40
- package/compiler/lexer/tokenTypes.js +0 -4
- package/compiler/lexer/tokenizer.js +0 -74
- package/compiler/parser/expression/comparison.js +0 -18
- package/compiler/parser/expression/identifier.js +0 -29
- package/compiler/parser/expression/number.js +0 -10
- package/compiler/parser/expression/operator.js +0 -21
- package/compiler/parser/expression/punctuation.js +0 -31
- package/compiler/parser/expression/string.js +0 -6
- package/compiler/parser/parseExpression.js +0 -27
- package/compiler/parser/parseStatement.js +0 -34
- package/compiler/parser/parser.js +0 -45
- package/compiler/parser/statement/call.js +0 -26
- package/compiler/parser/statement/function.js +0 -29
- package/compiler/parser/statement/if.js +0 -34
- package/compiler/parser/statement/return.js +0 -10
- package/compiler/parser/statement/set.js +0 -11
- package/compiler/parser/statement/show.js +0 -10
- package/compiler/parser/statement/try-catch.js +0 -25
- package/compiler/parser/statement/while.js +0 -22
- package/converter/go/convert.js +0 -110
- package/converter/js/convert.js +0 -107
- package/jsconfig.json +0 -27
- package/vite.config.js +0 -17
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Mimo Python runtime.
|
|
3
|
+
Provides Python implementations for Mimo's built-ins and standard library.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import re
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import random
|
|
11
|
+
import datetime
|
|
12
|
+
import urllib.request
|
|
13
|
+
import urllib.error
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, List, Dict, Callable, Optional, Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_equal(a: Any, b: Any) -> bool:
|
|
19
|
+
"""Deep equality check for Mimo values."""
|
|
20
|
+
if a is b:
|
|
21
|
+
return True
|
|
22
|
+
if type(a) != type(b):
|
|
23
|
+
return False
|
|
24
|
+
if isinstance(a, (list, tuple)):
|
|
25
|
+
if len(a) != len(b):
|
|
26
|
+
return False
|
|
27
|
+
return all(is_equal(x, y) for x, y in zip(a, b))
|
|
28
|
+
if isinstance(a, dict):
|
|
29
|
+
if set(a.keys()) != set(b.keys()):
|
|
30
|
+
return False
|
|
31
|
+
return all(is_equal(a[k], b[k]) for k in a.keys())
|
|
32
|
+
return a == b
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stringify(value: Any) -> str:
|
|
36
|
+
"""Convert a value to Mimo's string representation."""
|
|
37
|
+
if value is None:
|
|
38
|
+
return 'null'
|
|
39
|
+
if isinstance(value, bool):
|
|
40
|
+
return 'true' if value else 'false'
|
|
41
|
+
if isinstance(value, str):
|
|
42
|
+
return value
|
|
43
|
+
if isinstance(value, (list, tuple)):
|
|
44
|
+
elements = [stringify(v) for v in value]
|
|
45
|
+
return f"[{', '.join(elements)}]"
|
|
46
|
+
if isinstance(value, dict):
|
|
47
|
+
pairs = [f"{k}: {stringify(v)}" for k, v in value.items()]
|
|
48
|
+
return f"{{{', '.join(pairs)}}}"
|
|
49
|
+
if isinstance(value, datetime.datetime):
|
|
50
|
+
return f"datetime({value.isoformat()})"
|
|
51
|
+
return str(value)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MimoRuntime:
|
|
55
|
+
"""Main Mimo runtime class containing all built-ins and standard library modules."""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.setup_stdlib()
|
|
59
|
+
|
|
60
|
+
# --- Core IO & Utils ---
|
|
61
|
+
def show(self, *args):
|
|
62
|
+
"""Print values to stdout."""
|
|
63
|
+
output = ' '.join(stringify(arg) for arg in args)
|
|
64
|
+
print(output)
|
|
65
|
+
|
|
66
|
+
# --- Core Built-ins ---
|
|
67
|
+
def len(self, collection):
|
|
68
|
+
"""Get length of a collection."""
|
|
69
|
+
if collection is None:
|
|
70
|
+
return 0
|
|
71
|
+
if hasattr(collection, '__len__'):
|
|
72
|
+
return len(collection)
|
|
73
|
+
if isinstance(collection, dict):
|
|
74
|
+
return len(collection.keys())
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
def add(self, a, b):
|
|
78
|
+
"""Mimo + operator: concatenates if either operand is a string, otherwise adds."""
|
|
79
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
80
|
+
return stringify(a) + stringify(b)
|
|
81
|
+
return a + b
|
|
82
|
+
|
|
83
|
+
def get(self, collection, key):
|
|
84
|
+
"""Get value from collection by key, return None if not found."""
|
|
85
|
+
if collection is None:
|
|
86
|
+
return None
|
|
87
|
+
try:
|
|
88
|
+
if isinstance(collection, (list, tuple)):
|
|
89
|
+
return collection[key] if 0 <= key < len(collection) else None
|
|
90
|
+
elif isinstance(collection, dict):
|
|
91
|
+
return collection.get(key)
|
|
92
|
+
else:
|
|
93
|
+
return getattr(collection, key, None)
|
|
94
|
+
except (KeyError, IndexError, TypeError):
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def update(self, collection, key, value):
|
|
98
|
+
"""Update collection at key with value."""
|
|
99
|
+
if isinstance(collection, list) and isinstance(key, int):
|
|
100
|
+
if 0 <= key < len(collection):
|
|
101
|
+
collection[key] = value
|
|
102
|
+
elif isinstance(collection, dict):
|
|
103
|
+
collection[key] = value
|
|
104
|
+
else:
|
|
105
|
+
setattr(collection, key, value)
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
def type(self, value):
|
|
109
|
+
"""Get Mimo type of value."""
|
|
110
|
+
if value is None:
|
|
111
|
+
return 'null'
|
|
112
|
+
elif isinstance(value, bool):
|
|
113
|
+
return 'boolean'
|
|
114
|
+
elif isinstance(value, (int, float)):
|
|
115
|
+
return 'number'
|
|
116
|
+
elif isinstance(value, str):
|
|
117
|
+
return 'string'
|
|
118
|
+
elif isinstance(value, (list, tuple)):
|
|
119
|
+
return 'array'
|
|
120
|
+
elif isinstance(value, dict):
|
|
121
|
+
return 'object'
|
|
122
|
+
elif callable(value):
|
|
123
|
+
return 'function'
|
|
124
|
+
else:
|
|
125
|
+
return 'object'
|
|
126
|
+
|
|
127
|
+
def push(self, array, value):
|
|
128
|
+
"""Add value to end of array."""
|
|
129
|
+
if isinstance(array, list):
|
|
130
|
+
array.append(value)
|
|
131
|
+
return array
|
|
132
|
+
|
|
133
|
+
def pop(self, array):
|
|
134
|
+
"""Remove and return last element from array."""
|
|
135
|
+
if isinstance(array, list) and len(array) > 0:
|
|
136
|
+
return array.pop()
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def range(self, *args):
|
|
140
|
+
"""Generate range of numbers."""
|
|
141
|
+
if len(args) == 1:
|
|
142
|
+
return list(range(args[0]))
|
|
143
|
+
elif len(args) == 2:
|
|
144
|
+
return list(range(args[0], args[1]))
|
|
145
|
+
elif len(args) == 3:
|
|
146
|
+
return list(range(args[0], args[1], args[2]))
|
|
147
|
+
else:
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
def join(self, array, separator):
|
|
151
|
+
"""Join array elements with separator."""
|
|
152
|
+
if not isinstance(array, (list, tuple)):
|
|
153
|
+
return ""
|
|
154
|
+
return separator.join(stringify(item) for item in array)
|
|
155
|
+
|
|
156
|
+
def slice(self, collection, start, end=None):
|
|
157
|
+
"""Slice a collection."""
|
|
158
|
+
if end is None:
|
|
159
|
+
return collection[start:]
|
|
160
|
+
return collection[start:end]
|
|
161
|
+
|
|
162
|
+
# --- Logical Operators ---
|
|
163
|
+
def eq(self, a, b):
|
|
164
|
+
return is_equal(a, b)
|
|
165
|
+
|
|
166
|
+
def neq(self, a, b):
|
|
167
|
+
return not is_equal(a, b)
|
|
168
|
+
|
|
169
|
+
def and_(self, a, b):
|
|
170
|
+
return a and b
|
|
171
|
+
|
|
172
|
+
def or_(self, a, b):
|
|
173
|
+
return a or b
|
|
174
|
+
|
|
175
|
+
# --- Utility functions ---
|
|
176
|
+
def has_property(self, obj, prop):
|
|
177
|
+
if isinstance(obj, dict):
|
|
178
|
+
return prop in obj
|
|
179
|
+
else:
|
|
180
|
+
return hasattr(obj, prop)
|
|
181
|
+
|
|
182
|
+
def get_property_safe(self, obj, prop):
|
|
183
|
+
"""Safely get a property, returning None if not found."""
|
|
184
|
+
return self.get(obj, prop)
|
|
185
|
+
|
|
186
|
+
def keys(self, obj):
|
|
187
|
+
if isinstance(obj, dict):
|
|
188
|
+
return list(obj.keys())
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
def values(self, obj):
|
|
192
|
+
if isinstance(obj, dict):
|
|
193
|
+
return list(obj.values())
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
def entries(self, obj):
|
|
197
|
+
if isinstance(obj, dict):
|
|
198
|
+
return [[k, v] for k, v in obj.items()]
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
def get_arguments(self):
|
|
202
|
+
return sys.argv[1:]
|
|
203
|
+
|
|
204
|
+
def get_env(self, name):
|
|
205
|
+
return os.environ.get(name)
|
|
206
|
+
|
|
207
|
+
def coalesce(self, *args):
|
|
208
|
+
"""Return first non-None value."""
|
|
209
|
+
for arg in args:
|
|
210
|
+
if arg is not None:
|
|
211
|
+
return arg
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def if_else(self, condition, then_val, else_val):
|
|
215
|
+
"""Ternary if_else built-in."""
|
|
216
|
+
return then_val if condition else else_val
|
|
217
|
+
|
|
218
|
+
def exit_code(self, code=0):
|
|
219
|
+
"""Exit the program with the given code."""
|
|
220
|
+
sys.exit(code)
|
|
221
|
+
|
|
222
|
+
def setup_stdlib(self):
|
|
223
|
+
"""Setup standard library modules."""
|
|
224
|
+
|
|
225
|
+
# File system module
|
|
226
|
+
class FSModule:
|
|
227
|
+
@staticmethod
|
|
228
|
+
def read_file(path: str) -> str:
|
|
229
|
+
try:
|
|
230
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
231
|
+
return f.read()
|
|
232
|
+
except Exception as e:
|
|
233
|
+
raise Exception(f"Failed to read file {path}: {str(e)}")
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def write_file(path: str, data: str) -> None:
|
|
237
|
+
try:
|
|
238
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
239
|
+
f.write(data)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
raise Exception(f"Failed to write file {path}: {str(e)}")
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def exists(path: str) -> bool:
|
|
245
|
+
return Path(path).exists()
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def list_dir(path: str) -> List[str]:
|
|
249
|
+
try:
|
|
250
|
+
return list(os.listdir(path))
|
|
251
|
+
except Exception as e:
|
|
252
|
+
raise Exception(f"Failed to list directory {path}: {str(e)}")
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def make_dir(path: str, recursive: bool = False) -> None:
|
|
256
|
+
try:
|
|
257
|
+
if recursive:
|
|
258
|
+
Path(path).mkdir(parents=True, exist_ok=True)
|
|
259
|
+
else:
|
|
260
|
+
Path(path).mkdir(exist_ok=True)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
raise Exception(f"Failed to create directory {path}: {str(e)}")
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def remove_file(path: str) -> None:
|
|
266
|
+
try:
|
|
267
|
+
Path(path).unlink()
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise Exception(f"Failed to remove file {path}: {str(e)}")
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def remove_dir(path: str) -> None:
|
|
273
|
+
try:
|
|
274
|
+
Path(path).rmdir()
|
|
275
|
+
except Exception as e:
|
|
276
|
+
raise Exception(f"Failed to remove directory {path}: {str(e)}")
|
|
277
|
+
|
|
278
|
+
# JSON module
|
|
279
|
+
class JSONModule:
|
|
280
|
+
@staticmethod
|
|
281
|
+
def parse(text: str):
|
|
282
|
+
try:
|
|
283
|
+
return json.loads(text)
|
|
284
|
+
except json.JSONDecodeError as e:
|
|
285
|
+
raise Exception(f"Failed to parse JSON: {str(e)}")
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def stringify(obj, indent: Optional[int] = None):
|
|
289
|
+
try:
|
|
290
|
+
return json.dumps(obj, indent=indent, ensure_ascii=False)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise Exception(f"Failed to stringify JSON: {str(e)}")
|
|
293
|
+
|
|
294
|
+
# DateTime module
|
|
295
|
+
class DateTimeModule:
|
|
296
|
+
@staticmethod
|
|
297
|
+
def now():
|
|
298
|
+
return datetime.datetime.now()
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def get_timestamp(dt):
|
|
302
|
+
if isinstance(dt, datetime.datetime):
|
|
303
|
+
return int(dt.timestamp() * 1000)
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def from_timestamp(ts):
|
|
308
|
+
return datetime.datetime.fromtimestamp(ts / 1000)
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def to_iso_string(dt):
|
|
312
|
+
if isinstance(dt, datetime.datetime):
|
|
313
|
+
return dt.isoformat()
|
|
314
|
+
return ""
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def format(dt, fmt):
|
|
318
|
+
if not isinstance(dt, datetime.datetime):
|
|
319
|
+
return ""
|
|
320
|
+
result = fmt
|
|
321
|
+
result = result.replace('YYYY', str(dt.year))
|
|
322
|
+
result = result.replace('MM', f"{dt.month:02d}")
|
|
323
|
+
result = result.replace('DD', f"{dt.day:02d}")
|
|
324
|
+
result = result.replace('hh', f"{dt.hour:02d}")
|
|
325
|
+
result = result.replace('mm', f"{dt.minute:02d}")
|
|
326
|
+
result = result.replace('ss', f"{dt.second:02d}")
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
# Math module
|
|
330
|
+
class MathModule:
|
|
331
|
+
PI = math.pi
|
|
332
|
+
E = math.e
|
|
333
|
+
|
|
334
|
+
def __getattr__(self, name):
|
|
335
|
+
if hasattr(math, name):
|
|
336
|
+
return getattr(math, name)
|
|
337
|
+
raise AttributeError(f"Math module has no attribute '{name}'")
|
|
338
|
+
|
|
339
|
+
# String module
|
|
340
|
+
class StringModule:
|
|
341
|
+
@staticmethod
|
|
342
|
+
def to_upper(s: str) -> str:
|
|
343
|
+
return s.upper()
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def to_lower(s: str) -> str:
|
|
347
|
+
return s.lower()
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def to_title_case(s: str) -> str:
|
|
351
|
+
return ' '.join(w.capitalize() for w in s.lower().split(' '))
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def capitalize(s: str) -> str:
|
|
355
|
+
return s[0].upper() + s[1:] if s else s
|
|
356
|
+
|
|
357
|
+
@staticmethod
|
|
358
|
+
def trim(s: str) -> str:
|
|
359
|
+
return s.strip()
|
|
360
|
+
|
|
361
|
+
@staticmethod
|
|
362
|
+
def trim_start(s: str) -> str:
|
|
363
|
+
return s.lstrip()
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def trim_end(s: str) -> str:
|
|
367
|
+
return s.rstrip()
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def pad_start(s: str, length: int, pad: str = ' ') -> str:
|
|
371
|
+
return s.rjust(length, pad)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def pad_end(s: str, length: int, pad: str = ' ') -> str:
|
|
375
|
+
return s.ljust(length, pad)
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def contains(s: str, sub: str, pos: int = 0) -> bool:
|
|
379
|
+
return sub in s[pos:]
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def starts_with(s: str, sub: str, pos: int = 0) -> bool:
|
|
383
|
+
return s.startswith(sub, pos)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def ends_with(s: str, sub: str, end: Optional[int] = None) -> bool:
|
|
387
|
+
return s.endswith(sub, 0, end)
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def index_of(s: str, sub: str, from_idx: int = 0) -> int:
|
|
391
|
+
try:
|
|
392
|
+
return s.index(sub, from_idx)
|
|
393
|
+
except ValueError:
|
|
394
|
+
return -1
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def last_index_of(s: str, sub: str, from_idx: Optional[int] = None) -> int:
|
|
398
|
+
try:
|
|
399
|
+
if from_idx is not None:
|
|
400
|
+
return s.rindex(sub, 0, from_idx + 1)
|
|
401
|
+
return s.rindex(sub)
|
|
402
|
+
except ValueError:
|
|
403
|
+
return -1
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def substring(s: str, start: int, end: Optional[int] = None) -> str:
|
|
407
|
+
return s[start:end]
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def slice(s: str, start: int, end: Optional[int] = None) -> str:
|
|
411
|
+
return s[start:end]
|
|
412
|
+
|
|
413
|
+
@staticmethod
|
|
414
|
+
def split(s: str, sep=None, limit: Optional[int] = None) -> List[str]:
|
|
415
|
+
if limit is not None:
|
|
416
|
+
return s.split(sep, limit)
|
|
417
|
+
return s.split(sep)
|
|
418
|
+
|
|
419
|
+
@staticmethod
|
|
420
|
+
def replace(s: str, find: str, rep: str) -> str:
|
|
421
|
+
return s.replace(find, rep, 1)
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def replace_all(s: str, find: str, rep: str) -> str:
|
|
425
|
+
return s.replace(find, rep)
|
|
426
|
+
|
|
427
|
+
@staticmethod
|
|
428
|
+
def repeat(s: str, n: int) -> str:
|
|
429
|
+
return s * n
|
|
430
|
+
|
|
431
|
+
@staticmethod
|
|
432
|
+
def char_at(s: str, i: int) -> str:
|
|
433
|
+
return s[i] if 0 <= i < len(s) else ''
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def is_empty(s: str) -> bool:
|
|
437
|
+
return len(s) == 0
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def is_blank(s: str) -> bool:
|
|
441
|
+
return len(s.strip()) == 0
|
|
442
|
+
|
|
443
|
+
# Array module
|
|
444
|
+
class ArrayModule:
|
|
445
|
+
@staticmethod
|
|
446
|
+
def map(array: List, callback: Callable) -> List:
|
|
447
|
+
return [callback(item) for item in array]
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def filter(array: List, callback: Callable) -> List:
|
|
451
|
+
return [item for item in array if callback(item)]
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def reduce(array: List, callback: Callable, initial=None):
|
|
455
|
+
if initial is not None:
|
|
456
|
+
result = initial
|
|
457
|
+
start = 0
|
|
458
|
+
else:
|
|
459
|
+
result = array[0] if array else None
|
|
460
|
+
start = 1
|
|
461
|
+
for i in range(start, len(array)):
|
|
462
|
+
result = callback(result, array[i])
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def flat(array: List, depth: int = 1) -> List:
|
|
467
|
+
def _flat(arr, d):
|
|
468
|
+
result = []
|
|
469
|
+
for item in arr:
|
|
470
|
+
if isinstance(item, list) and d > 0:
|
|
471
|
+
result.extend(_flat(item, d - 1))
|
|
472
|
+
else:
|
|
473
|
+
result.append(item)
|
|
474
|
+
return result
|
|
475
|
+
return _flat(array, depth)
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def flat_map(array: List, callback: Callable) -> List:
|
|
479
|
+
result = []
|
|
480
|
+
for item in array:
|
|
481
|
+
mapped = callback(item)
|
|
482
|
+
if isinstance(mapped, list):
|
|
483
|
+
result.extend(mapped)
|
|
484
|
+
else:
|
|
485
|
+
result.append(mapped)
|
|
486
|
+
return result
|
|
487
|
+
|
|
488
|
+
@staticmethod
|
|
489
|
+
def group_by(array: List, callback: Callable) -> Dict:
|
|
490
|
+
result = {}
|
|
491
|
+
for item in array:
|
|
492
|
+
key = str(callback(item))
|
|
493
|
+
if key not in result:
|
|
494
|
+
result[key] = []
|
|
495
|
+
result[key].append(item)
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
def zip(*arrays) -> List:
|
|
500
|
+
return [list(group) for group in zip(*arrays)]
|
|
501
|
+
|
|
502
|
+
@staticmethod
|
|
503
|
+
def chunk(array: List, size: int) -> List:
|
|
504
|
+
return [array[i:i + size] for i in range(0, len(array), size)]
|
|
505
|
+
|
|
506
|
+
@staticmethod
|
|
507
|
+
def count(array: List, callback: Optional[Callable] = None) -> int:
|
|
508
|
+
if callback:
|
|
509
|
+
return sum(1 for item in array if callback(item))
|
|
510
|
+
return len(array)
|
|
511
|
+
|
|
512
|
+
@staticmethod
|
|
513
|
+
def for_each(array: List, callback: Callable) -> None:
|
|
514
|
+
for item in array:
|
|
515
|
+
callback(item)
|
|
516
|
+
|
|
517
|
+
@staticmethod
|
|
518
|
+
def find(array: List, callback: Callable):
|
|
519
|
+
for item in array:
|
|
520
|
+
if callback(item):
|
|
521
|
+
return item
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def find_index(array: List, callback: Callable) -> int:
|
|
526
|
+
for i, item in enumerate(array):
|
|
527
|
+
if callback(item):
|
|
528
|
+
return i
|
|
529
|
+
return -1
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def includes(array: List, value) -> bool:
|
|
533
|
+
return value in array
|
|
534
|
+
|
|
535
|
+
@staticmethod
|
|
536
|
+
def index_of(array: List, value, from_idx: int = 0) -> int:
|
|
537
|
+
try:
|
|
538
|
+
return array.index(value, from_idx)
|
|
539
|
+
except ValueError:
|
|
540
|
+
return -1
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def last_index_of(array: List, value, from_idx: Optional[int] = None) -> int:
|
|
544
|
+
search = array[:from_idx + 1] if from_idx is not None else array
|
|
545
|
+
for i in range(len(search) - 1, -1, -1):
|
|
546
|
+
if search[i] == value:
|
|
547
|
+
return i
|
|
548
|
+
return -1
|
|
549
|
+
|
|
550
|
+
@staticmethod
|
|
551
|
+
def slice(array: List, start: int, end: Optional[int] = None) -> List:
|
|
552
|
+
return array[start:end]
|
|
553
|
+
|
|
554
|
+
@staticmethod
|
|
555
|
+
def first(array: List):
|
|
556
|
+
return array[0] if array else None
|
|
557
|
+
|
|
558
|
+
@staticmethod
|
|
559
|
+
def last(array: List):
|
|
560
|
+
return array[-1] if array else None
|
|
561
|
+
|
|
562
|
+
@staticmethod
|
|
563
|
+
def is_empty(array: List) -> bool:
|
|
564
|
+
return len(array) == 0
|
|
565
|
+
|
|
566
|
+
@staticmethod
|
|
567
|
+
def sort(array: List, callback: Optional[Callable] = None) -> List:
|
|
568
|
+
if callback:
|
|
569
|
+
import functools
|
|
570
|
+
return sorted(array, key=functools.cmp_to_key(callback))
|
|
571
|
+
return sorted(array)
|
|
572
|
+
|
|
573
|
+
@staticmethod
|
|
574
|
+
def reverse(array: List) -> List:
|
|
575
|
+
return list(reversed(array))
|
|
576
|
+
|
|
577
|
+
@staticmethod
|
|
578
|
+
def shuffle(array: List) -> List:
|
|
579
|
+
result = list(array)
|
|
580
|
+
random.shuffle(result)
|
|
581
|
+
return result
|
|
582
|
+
|
|
583
|
+
@staticmethod
|
|
584
|
+
def concat(*arrays) -> List:
|
|
585
|
+
result = []
|
|
586
|
+
for arr in arrays:
|
|
587
|
+
result.extend(arr)
|
|
588
|
+
return result
|
|
589
|
+
|
|
590
|
+
@staticmethod
|
|
591
|
+
def unique(array: List) -> List:
|
|
592
|
+
seen = []
|
|
593
|
+
result = []
|
|
594
|
+
for item in array:
|
|
595
|
+
if item not in seen:
|
|
596
|
+
seen.append(item)
|
|
597
|
+
result.append(item)
|
|
598
|
+
return result
|
|
599
|
+
|
|
600
|
+
@staticmethod
|
|
601
|
+
def intersection(a: List, b: List) -> List:
|
|
602
|
+
return [v for v in a if v in b]
|
|
603
|
+
|
|
604
|
+
@staticmethod
|
|
605
|
+
def union(a: List, b: List) -> List:
|
|
606
|
+
result = list(a)
|
|
607
|
+
for item in b:
|
|
608
|
+
if item not in result:
|
|
609
|
+
result.append(item)
|
|
610
|
+
return result
|
|
611
|
+
|
|
612
|
+
@staticmethod
|
|
613
|
+
def difference(a: List, b: List) -> List:
|
|
614
|
+
return [v for v in a if v not in b]
|
|
615
|
+
|
|
616
|
+
# Path module
|
|
617
|
+
class PathModule:
|
|
618
|
+
@staticmethod
|
|
619
|
+
def join(*parts: str) -> str:
|
|
620
|
+
return os.path.join(*parts)
|
|
621
|
+
|
|
622
|
+
@staticmethod
|
|
623
|
+
def dirname(p: str) -> str:
|
|
624
|
+
return os.path.dirname(p)
|
|
625
|
+
|
|
626
|
+
@staticmethod
|
|
627
|
+
def basename(p: str, ext: Optional[str] = None) -> str:
|
|
628
|
+
base = os.path.basename(p)
|
|
629
|
+
if ext and base.endswith(ext):
|
|
630
|
+
base = base[:-len(ext)]
|
|
631
|
+
return base
|
|
632
|
+
|
|
633
|
+
@staticmethod
|
|
634
|
+
def extname(p: str) -> str:
|
|
635
|
+
return os.path.splitext(p)[1]
|
|
636
|
+
|
|
637
|
+
# Env module
|
|
638
|
+
class EnvModule:
|
|
639
|
+
@staticmethod
|
|
640
|
+
def get(name: str, fallback=None):
|
|
641
|
+
return os.environ.get(name, fallback)
|
|
642
|
+
|
|
643
|
+
@staticmethod
|
|
644
|
+
def has(name: str) -> bool:
|
|
645
|
+
return name in os.environ
|
|
646
|
+
|
|
647
|
+
@staticmethod
|
|
648
|
+
def all() -> Dict[str, str]:
|
|
649
|
+
return dict(os.environ)
|
|
650
|
+
|
|
651
|
+
# Regex module
|
|
652
|
+
class RegexModule:
|
|
653
|
+
@staticmethod
|
|
654
|
+
def find_matches(pattern: str, text: str, flags: str = '') -> Optional[List[str]]:
|
|
655
|
+
flag_map = {'i': re.IGNORECASE, 'm': re.MULTILINE, 's': re.DOTALL}
|
|
656
|
+
re_flags = 0
|
|
657
|
+
for ch in flags.replace('g', ''):
|
|
658
|
+
re_flags |= flag_map.get(ch, 0)
|
|
659
|
+
matches = re.findall(pattern, text, re_flags)
|
|
660
|
+
return matches if matches else None
|
|
661
|
+
|
|
662
|
+
@staticmethod
|
|
663
|
+
def is_match(pattern: str, text: str, flags: str = '') -> bool:
|
|
664
|
+
flag_map = {'i': re.IGNORECASE, 'm': re.MULTILINE, 's': re.DOTALL}
|
|
665
|
+
re_flags = 0
|
|
666
|
+
for ch in flags:
|
|
667
|
+
re_flags |= flag_map.get(ch, 0)
|
|
668
|
+
return bool(re.search(pattern, text, re_flags))
|
|
669
|
+
|
|
670
|
+
@staticmethod
|
|
671
|
+
def replace_all(text: str, pattern: str, replacement: str, flags: str = '') -> str:
|
|
672
|
+
flag_map = {'i': re.IGNORECASE, 'm': re.MULTILINE, 's': re.DOTALL}
|
|
673
|
+
re_flags = 0
|
|
674
|
+
for ch in flags.replace('g', ''):
|
|
675
|
+
re_flags |= flag_map.get(ch, 0)
|
|
676
|
+
return re.sub(pattern, replacement, text, flags=re_flags)
|
|
677
|
+
|
|
678
|
+
@staticmethod
|
|
679
|
+
def extract(pattern: str, text: str, flags: str = '') -> Optional[List[str]]:
|
|
680
|
+
flag_map = {'i': re.IGNORECASE, 'm': re.MULTILINE, 's': re.DOTALL}
|
|
681
|
+
re_flags = 0
|
|
682
|
+
for ch in flags:
|
|
683
|
+
re_flags |= flag_map.get(ch, 0)
|
|
684
|
+
m = re.search(pattern, text, re_flags)
|
|
685
|
+
if m is None:
|
|
686
|
+
return None
|
|
687
|
+
return [m.group(0)] + list(m.groups())
|
|
688
|
+
|
|
689
|
+
# HTTP module
|
|
690
|
+
class HTTPModule:
|
|
691
|
+
@staticmethod
|
|
692
|
+
def get(url: str) -> str:
|
|
693
|
+
try:
|
|
694
|
+
with urllib.request.urlopen(url) as response:
|
|
695
|
+
return response.read().decode('utf-8')
|
|
696
|
+
except urllib.error.URLError as e:
|
|
697
|
+
raise Exception(f"HTTP GET request failed: {str(e)}")
|
|
698
|
+
|
|
699
|
+
@staticmethod
|
|
700
|
+
def post(url: str, body: str, headers: Optional[Dict] = None) -> str:
|
|
701
|
+
req_headers = {'Content-Type': 'application/json'}
|
|
702
|
+
if headers:
|
|
703
|
+
req_headers.update(headers)
|
|
704
|
+
data = body.encode('utf-8')
|
|
705
|
+
req = urllib.request.Request(url, data=data, headers=req_headers, method='POST')
|
|
706
|
+
try:
|
|
707
|
+
with urllib.request.urlopen(req) as response:
|
|
708
|
+
return response.read().decode('utf-8')
|
|
709
|
+
except urllib.error.URLError as e:
|
|
710
|
+
raise Exception(f"HTTP POST request failed: {str(e)}")
|
|
711
|
+
|
|
712
|
+
# Object module
|
|
713
|
+
class ObjectModule:
|
|
714
|
+
@staticmethod
|
|
715
|
+
def merge(*objs: Dict) -> Dict:
|
|
716
|
+
result = {}
|
|
717
|
+
for obj in objs:
|
|
718
|
+
result.update(obj)
|
|
719
|
+
return result
|
|
720
|
+
|
|
721
|
+
@staticmethod
|
|
722
|
+
def pick(obj: Dict, keys: List[str]) -> Dict:
|
|
723
|
+
return {k: obj[k] for k in keys if k in obj}
|
|
724
|
+
|
|
725
|
+
@staticmethod
|
|
726
|
+
def omit(obj: Dict, keys: List[str]) -> Dict:
|
|
727
|
+
excluded = set(keys)
|
|
728
|
+
return {k: v for k, v in obj.items() if k not in excluded}
|
|
729
|
+
|
|
730
|
+
@staticmethod
|
|
731
|
+
def map_values(obj: Dict, callback: Callable) -> Dict:
|
|
732
|
+
return {k: callback(v, k, obj) for k, v in obj.items()}
|
|
733
|
+
|
|
734
|
+
@staticmethod
|
|
735
|
+
def from_entries(entries: List) -> Dict:
|
|
736
|
+
return {str(entry[0]): entry[1] for entry in entries}
|
|
737
|
+
|
|
738
|
+
@staticmethod
|
|
739
|
+
def is_empty(obj: Dict) -> bool:
|
|
740
|
+
return len(obj) == 0
|
|
741
|
+
|
|
742
|
+
@staticmethod
|
|
743
|
+
def keys(obj: Dict) -> List[str]:
|
|
744
|
+
return list(obj.keys())
|
|
745
|
+
|
|
746
|
+
@staticmethod
|
|
747
|
+
def values(obj: Dict) -> List:
|
|
748
|
+
return list(obj.values())
|
|
749
|
+
|
|
750
|
+
@staticmethod
|
|
751
|
+
def entries(obj: Dict) -> List:
|
|
752
|
+
return [[k, v] for k, v in obj.items()]
|
|
753
|
+
|
|
754
|
+
# Assert module
|
|
755
|
+
class AssertModule:
|
|
756
|
+
@staticmethod
|
|
757
|
+
def eq(actual, expected, message: Optional[str] = None) -> bool:
|
|
758
|
+
if not is_equal(actual, expected):
|
|
759
|
+
msg = f": {message}" if message else ""
|
|
760
|
+
raise AssertionError(
|
|
761
|
+
f"Assertion Failed{msg}.\n Expected: {json.dumps(expected)}\n Actual: {json.dumps(actual)}"
|
|
762
|
+
)
|
|
763
|
+
return True
|
|
764
|
+
|
|
765
|
+
@staticmethod
|
|
766
|
+
def neq(actual, expected, message: Optional[str] = None) -> bool:
|
|
767
|
+
if is_equal(actual, expected):
|
|
768
|
+
msg = f": {message}" if message else ""
|
|
769
|
+
raise AssertionError(f"Assertion Failed{msg}. Expected values to be different.")
|
|
770
|
+
return True
|
|
771
|
+
|
|
772
|
+
@staticmethod
|
|
773
|
+
def true(condition, message: Optional[str] = None) -> bool:
|
|
774
|
+
if condition is not True:
|
|
775
|
+
msg = f": {message}" if message else ""
|
|
776
|
+
raise AssertionError(f"Assertion Failed{msg}. Expected true, got {condition!r}")
|
|
777
|
+
return True
|
|
778
|
+
|
|
779
|
+
@staticmethod
|
|
780
|
+
def false(condition, message: Optional[str] = None) -> bool:
|
|
781
|
+
if condition is not False:
|
|
782
|
+
msg = f": {message}" if message else ""
|
|
783
|
+
raise AssertionError(f"Assertion Failed{msg}. Expected false, got {condition!r}")
|
|
784
|
+
return True
|
|
785
|
+
|
|
786
|
+
@staticmethod
|
|
787
|
+
def throws(fn: Callable, message: Optional[str] = None) -> bool:
|
|
788
|
+
try:
|
|
789
|
+
fn()
|
|
790
|
+
except Exception:
|
|
791
|
+
return True
|
|
792
|
+
msg = f": {message}" if message else ""
|
|
793
|
+
raise AssertionError(f"Assertion Failed{msg}. Expected function to throw, but it did not.")
|
|
794
|
+
|
|
795
|
+
# Assign all modules
|
|
796
|
+
self.fs = FSModule()
|
|
797
|
+
self.json = JSONModule()
|
|
798
|
+
self.datetime = DateTimeModule()
|
|
799
|
+
self.math = MathModule()
|
|
800
|
+
self.string = StringModule()
|
|
801
|
+
self.array = ArrayModule()
|
|
802
|
+
self.path = PathModule()
|
|
803
|
+
self.env = EnvModule()
|
|
804
|
+
self.regex = RegexModule()
|
|
805
|
+
self.http = HTTPModule()
|
|
806
|
+
self.object = ObjectModule()
|
|
807
|
+
self.assert_ = AssertModule()
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# Create global mimo instance
|
|
811
|
+
mimo = MimoRuntime()
|