testomaton 0.2.2__py2.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.
testomaton/__init__.py ADDED
File without changes
testomaton/beaver.py ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+
3
+ #
4
+ # Copyright Testify AS
5
+ #
6
+ # This file is part of testomaton suite
7
+ #
8
+ # testomaton is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU Affero General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # For commercial license, please contact Testify AS.
14
+ #
15
+ # testomaton is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU Affero General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with testomaton. If not, see <http://www.gnu.org/licenses/>.
22
+ #
23
+ # See LICENSE file for the complete license text.
24
+ #
25
+
26
+ import sys
27
+ import argparse
28
+ import importlib
29
+ import importlib.metadata as metadata
30
+ import importlib.util
31
+ import csv
32
+
33
+ global_imports = {}
34
+ appended_columns = prepended_columns = replaced_columns = swapped_columns = []
35
+ columns_whitelist = []
36
+ columns_blacklist = []
37
+
38
+ def read_csv(file, separator=','):
39
+ csv_reader = None
40
+ try:
41
+ if file is None:
42
+ csv_reader = csv.reader(sys.stdin, delimiter=separator)
43
+ for row in csv_reader:
44
+ yield row
45
+ else:
46
+ with open(file) as f:
47
+ csv_reader = csv.reader(f, delimiter=separator)
48
+ for row in csv_reader:
49
+ yield row
50
+ except OSError:
51
+ print(f"Could not open file {file}")
52
+ sys.exit(1)
53
+
54
+ def parse_args():
55
+ global global_imports
56
+ global appended_columns, prepended_columns, replaced_columns, swapped_columns
57
+ global columns_whitelist, columns_blacklist
58
+
59
+ parser = argparse.ArgumentParser(description='Evaluate expressions in a csv file. Expressions are evaluated in the order they appear in the input list. Use @python <expression> to evaluate python expressions. In the expressions, use {column_name} to refer to a column value. Use {row id} to refer to the row number.')
60
+ parser.add_argument('input_file', type=str, nargs='?', default=None, help='Input file. If not provided, stdin is used')
61
+ parser.add_argument('-v', '--version', action='store_true', help='Print version')
62
+
63
+ parser.add_argument('-n', '--add-linenumber', nargs='?', type=str, default='', help='Add a column with the line number. Optional argument is the name of the column. Default name is empty. The column may be referenced in the expressions by its name.')
64
+ parser.add_argument('-H', '--remove-headrow', action='store_true', default=False, help='Do not output the first line that was read')
65
+ parser.add_argument("-s", "--input-separator", type=str, default=',', help="Separator used in the input")
66
+ parser.add_argument("-S", "--output-separator", type=str, default=None, help="Separator used in the output. If not provided, the input separator is used")
67
+ parser.add_argument("-i", "--imports", type=str, default=None, help="Comma separated list of packages to be imported and used in the expressions. Use 'as' to provide an alias.")
68
+ parser.add_argument("-m", "--modules", type=str, default=None, help="Comma separated list of custom modules (files with python code) to be imported and used in the expressions. Use 'as' to provide an alias. The file should contain a single module.")
69
+ parser.add_argument("-x", "--dont-evaluate", action='store_true', default=False, help="Do not evaluate expressions")
70
+
71
+ args = parser.parse_args()
72
+
73
+ if args.output_separator is None:
74
+ args.output_separator = args.input_separator
75
+
76
+ if args.version:
77
+ print(f"tomato {version()}")
78
+ sys.exit(0)
79
+
80
+ if args.imports is not None:
81
+ for imp in args.imports.split(','):
82
+ tokens = imp.split(' as ')
83
+ if len(tokens) == 1:
84
+ module_name = alias = tokens[0].strip()
85
+ else:
86
+ module_name = tokens[0].strip()
87
+ alias = tokens[1].strip()
88
+
89
+ module = importlib.import_module(module_name)
90
+ global_imports[alias] = module
91
+
92
+ if args.modules is not None:
93
+ for file_path in args.modules.split(','):
94
+ tokens = file_path.split(' as ')
95
+ file_name = tokens[0].strip().split('/')[-1]
96
+ module_name = file_name.split('.')[0]
97
+ if len(tokens) == 1:
98
+ alias = module_name
99
+ else:
100
+ file_path = tokens[0].strip()
101
+ alias = tokens[1].strip()
102
+ # print(f"Importing module {module_name} from file {file_name} as {alias}")
103
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
104
+ module = importlib.util.module_from_spec(spec)
105
+ sys.modules[alias] = module
106
+ spec.loader.exec_module(module)
107
+ global_imports[alias] = module
108
+ for attr in dir(module):
109
+ if not attr.startswith('__'):
110
+ global_imports[attr] = getattr(module, attr)
111
+
112
+ return args
113
+
114
+
115
+ def version():
116
+ return metadata.version('testomaton')
117
+
118
+ def evaluate_token(row_id, token, values):
119
+ stripped_token = token.strip()
120
+ values_copy = values.copy()
121
+ values_copy.update(global_imports)
122
+
123
+ def is_numeric(value):
124
+ try:
125
+ float(value)
126
+ return True
127
+ except ValueError:
128
+ return False
129
+
130
+ if token.startswith('@python '):
131
+ expr = stripped_token[8:]
132
+ expr = expr.replace('{row id}', str(row_id))
133
+ for key, value in values.items():
134
+ replaced_string = '{' + str(key) + '}'
135
+ #if value is not surrounded by quotes already:
136
+ if (str(value).strip()[0] != "'" and str(value).strip()[-1] != "'") and\
137
+ (str(value).strip()[0] != '"' and str(value).strip()[-1] != '"'):
138
+
139
+ #if value is not a number or bool literal then it should be enclosed in quotes
140
+ if not is_numeric(value) and value.lower() not in ['true', 'false']:
141
+ value = f"'{value}'"
142
+ expr = expr.replace(replaced_string, str(value))
143
+ try:
144
+ try:
145
+ result = eval(expr, {}, values_copy)
146
+ except Exception as e:
147
+ print(f'evaluating {expr} failed: {e}')
148
+ return result
149
+ except Exception as e:
150
+ exit(1)
151
+ else:
152
+ return token
153
+
154
+ def main():
155
+ args = parse_args()
156
+
157
+ input_columns = next(read_csv(args.input_file, separator=args.input_separator))
158
+
159
+ if not args.remove_headrow:
160
+ first_row = input_columns.copy()
161
+ if args.add_linenumber:
162
+ first_row.insert(0, args.add_linenumber)
163
+ print(args.output_separator.join(first_row))
164
+
165
+ csv_writer = csv.writer(sys.stdout, delimiter=args.output_separator)
166
+ row_id = 0
167
+ for row in read_csv(args.input_file, separator=args.input_separator):
168
+ row_id += 1
169
+ values = {}
170
+ for index, token in enumerate(row):
171
+ value = evaluate_token(row_id, token, values) if not args.dont_evaluate else token
172
+ values[input_columns[index]] = value
173
+ output = [str(v) for v in values.values()]
174
+ if args.add_linenumber:
175
+ output.insert(0, str(row_id))
176
+
177
+ csv_writer.writerow(output)
178
+
179
+ if __name__ == '__main__':
180
+ main()