vantage6-algorithm-tools 5.0.3rc5__tar.gz → 5.0.4rc1__tar.gz
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.
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/PKG-INFO +4 -8
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/pyproject.toml +4 -11
- vantage6_algorithm_tools-5.0.4rc1/tests/preprocessing/test_expression.py +207 -0
- vantage6_algorithm_tools-5.0.4rc1/tests/tools/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/mock/client.py +5 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/preprocessing/column.py +19 -4
- vantage6_algorithm_tools-5.0.4rc1/vantage6/algorithm/preprocessing/expression.py +259 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/preprocessing/filtering.py +19 -7
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/.gitignore +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/Makefile +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/README.md +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/_test_deserialization.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/_test_docker_wrapper.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/_test_preprocessing.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/_test_serialization.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/algorithm/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/algorithm/test___init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/algorithm_module.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/base/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_algorithm_client.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_network.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_node.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_server.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_user_client.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_util.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5/tests/tools → vantage6_algorithm_tools-5.0.4rc1/tests/preprocessing}/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/tools/test_error_handling.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/client/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/data_extraction/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/data_extraction/mock_extract.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/decorator/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/decorator/action.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/decorator/algorithm_client.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/decorator/data.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/decorator/metadata.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/decorator/ohdsi.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/mock/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/mock/globals.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/mock/hq.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/mock/network.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/mock/node.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/mock/util.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/preprocessing/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/preprocessing/aggregation.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/preprocessing/algorithm_json_data.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/preprocessing/datetime.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/preprocessing/encoding.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/tools/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/tools/error_handling.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/tools/exceptions.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/tools/util.py +0 -0
- {vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/vantage6/algorithm/tools/wrap.py +0 -0
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vantage6-algorithm-tools
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.4rc1
|
|
4
4
|
Summary: Vantage6 algorithm tools
|
|
5
5
|
Author: Vantage6 Team
|
|
6
6
|
Maintainer-email: Frank Martin <f.martin@iknl.nl>, Bart van Beusekom <b.vanbeusekom@iknl.nl>
|
|
7
|
-
License:
|
|
7
|
+
License: Apache-2.0
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Requires-Dist: openpyxl>=3.0.0
|
|
10
10
|
Requires-Dist: pandas>=2.2.3
|
|
11
11
|
Requires-Dist: psycopg2-binary==2.9.10
|
|
12
12
|
Requires-Dist: pyarrow>=16.1.0
|
|
13
13
|
Requires-Dist: pyfiglet==1.0.4
|
|
14
|
-
Requires-Dist: pyjwt==2.
|
|
14
|
+
Requires-Dist: pyjwt==2.15.1
|
|
15
15
|
Requires-Dist: sparqlwrapper>=2.0.0
|
|
16
16
|
Requires-Dist: sqlalchemy>=1.4.46
|
|
17
|
-
Requires-Dist: vantage6-common==5.0.
|
|
18
|
-
Provides-Extra: dev
|
|
19
|
-
Requires-Dist: black; extra == 'dev'
|
|
20
|
-
Requires-Dist: coverage==7.10.2; extra == 'dev'
|
|
21
|
-
Requires-Dist: pre-commit; extra == 'dev'
|
|
17
|
+
Requires-Dist: vantage6-common==5.0.4rc1
|
|
22
18
|
Description-Content-Type: text/markdown
|
|
23
19
|
|
|
24
20
|
<h1 align="center">
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vantage6-algorithm-tools"
|
|
7
|
-
version = "5.0.
|
|
7
|
+
version = "5.0.4rc1"
|
|
8
8
|
description = "Vantage6 algorithm tools"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license = {text = "
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
11
|
authors = [
|
|
12
12
|
{name = "Vantage6 Team"}
|
|
13
13
|
]
|
|
@@ -23,18 +23,11 @@ dependencies = [
|
|
|
23
23
|
# running SQL queries on a postgres database.
|
|
24
24
|
"psycopg2-binary==2.9.10",
|
|
25
25
|
"pyarrow>=16.1.0",
|
|
26
|
-
"pyjwt==2.
|
|
26
|
+
"pyjwt==2.15.1",
|
|
27
27
|
"pyfiglet==1.0.4",
|
|
28
28
|
"SPARQLWrapper>=2.0.0",
|
|
29
29
|
"sqlalchemy>=1.4.46",
|
|
30
|
-
"vantage6-common==5.0.
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
[project.optional-dependencies]
|
|
34
|
-
dev = [
|
|
35
|
-
"coverage==7.10.2",
|
|
36
|
-
"black",
|
|
37
|
-
"pre-commit",
|
|
30
|
+
"vantage6-common==5.0.4rc1",
|
|
38
31
|
]
|
|
39
32
|
|
|
40
33
|
[tool.hatch.build.targets.wheel]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from vantage6.algorithm.tools.exceptions import UserInputError
|
|
8
|
+
|
|
9
|
+
from vantage6.algorithm.preprocessing.column import assign_column, redefine_column
|
|
10
|
+
from vantage6.algorithm.preprocessing.expression import (
|
|
11
|
+
MAX_EXPRESSION_LENGTH,
|
|
12
|
+
evaluate_expression,
|
|
13
|
+
)
|
|
14
|
+
from vantage6.algorithm.preprocessing.filtering import select_rows
|
|
15
|
+
|
|
16
|
+
# The decorators require a running algorithm container, so test the wrapped functions
|
|
17
|
+
_assign_column = inspect.unwrap(assign_column)
|
|
18
|
+
_redefine_column = inspect.unwrap(redefine_column)
|
|
19
|
+
_select_rows = inspect.unwrap(select_rows)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _df() -> pd.DataFrame:
|
|
23
|
+
return pd.DataFrame(
|
|
24
|
+
{
|
|
25
|
+
"a": [1, 2, 3, 4, 5],
|
|
26
|
+
"b": [6, 7, 8, 9, 10],
|
|
27
|
+
"c": [11, 12, 13, 14, 15],
|
|
28
|
+
"s": ["x", "y", "x", "z", "y"],
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestEvaluateExpression(unittest.TestCase):
|
|
34
|
+
def assert_series_equal(self, expression: str, expected: pd.Series) -> None:
|
|
35
|
+
pd.testing.assert_series_equal(
|
|
36
|
+
evaluate_expression(_df(), expression), expected, check_names=False
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def test_arithmetic(self):
|
|
40
|
+
df = _df()
|
|
41
|
+
self.assert_series_equal("a + b * 2", df.a + df.b * 2)
|
|
42
|
+
self.assert_series_equal("(a - b) / c", (df.a - df.b) / df.c)
|
|
43
|
+
self.assert_series_equal("c // a % 3", df.c // df.a % 3)
|
|
44
|
+
self.assert_series_equal("-a", -df.a)
|
|
45
|
+
self.assert_series_equal("+a", df.a)
|
|
46
|
+
|
|
47
|
+
def test_comparisons(self):
|
|
48
|
+
df = _df()
|
|
49
|
+
self.assert_series_equal("a > 2", df.a > 2)
|
|
50
|
+
self.assert_series_equal("a == 3", df.a == 3)
|
|
51
|
+
self.assert_series_equal("a != 3", df.a != 3)
|
|
52
|
+
self.assert_series_equal("s == 'x'", df.s == "x")
|
|
53
|
+
self.assert_series_equal("2 <= a < 5", (df.a >= 2) & (df.a < 5))
|
|
54
|
+
|
|
55
|
+
def test_membership(self):
|
|
56
|
+
df = _df()
|
|
57
|
+
self.assert_series_equal("a in [1, 5]", df.a.isin([1, 5]))
|
|
58
|
+
self.assert_series_equal("s not in ('x', 'y')", ~df.s.isin(["x", "y"]))
|
|
59
|
+
|
|
60
|
+
def test_logical_operators(self):
|
|
61
|
+
df = _df()
|
|
62
|
+
self.assert_series_equal("a > 2 and b < 10", (df.a > 2) & (df.b < 10))
|
|
63
|
+
self.assert_series_equal("a > 4 or c < 12", (df.a > 4) | (df.c < 12))
|
|
64
|
+
self.assert_series_equal("not a > 2", ~(df.a > 2))
|
|
65
|
+
self.assert_series_equal("(a > 2) & (b < 10)", (df.a > 2) & (df.b < 10))
|
|
66
|
+
self.assert_series_equal("(a > 4) | (c < 12)", (df.a > 4) | (df.c < 12))
|
|
67
|
+
self.assert_series_equal("~(a > 2)", ~(df.a > 2))
|
|
68
|
+
|
|
69
|
+
def test_functions(self):
|
|
70
|
+
df = _df()
|
|
71
|
+
for name, function in [
|
|
72
|
+
("abs", np.abs),
|
|
73
|
+
("sqrt", np.sqrt),
|
|
74
|
+
("log", np.log),
|
|
75
|
+
("log2", np.log2),
|
|
76
|
+
("log10", np.log10),
|
|
77
|
+
("exp", np.exp),
|
|
78
|
+
("floor", np.floor),
|
|
79
|
+
("ceil", np.ceil),
|
|
80
|
+
]:
|
|
81
|
+
with self.subTest(function=name):
|
|
82
|
+
self.assert_series_equal(f"{name}(a / 3)", function(df.a / 3))
|
|
83
|
+
self.assert_series_equal("round(a / 3)", np.round(df.a / 3))
|
|
84
|
+
self.assert_series_equal("round(a / 3, 2)", np.round(df.a / 3, 2))
|
|
85
|
+
self.assert_series_equal("sqrt(log(b) + 1)", np.sqrt(np.log(df.b) + 1))
|
|
86
|
+
|
|
87
|
+
def test_text(self):
|
|
88
|
+
df = _df()
|
|
89
|
+
self.assert_series_equal("s + '_1'", df.s + "_1")
|
|
90
|
+
for expression in [
|
|
91
|
+
"s * 999999999",
|
|
92
|
+
"'x' * 999999999",
|
|
93
|
+
"999999999 * s",
|
|
94
|
+
"s - 'x'",
|
|
95
|
+
]:
|
|
96
|
+
with (
|
|
97
|
+
self.subTest(expression=expression),
|
|
98
|
+
self.assertRaises(UserInputError),
|
|
99
|
+
):
|
|
100
|
+
evaluate_expression(df, expression)
|
|
101
|
+
|
|
102
|
+
def test_constants(self):
|
|
103
|
+
self.assertEqual(evaluate_expression(_df(), "1 + 2"), 3)
|
|
104
|
+
self.assertEqual(evaluate_expression(_df(), "not True"), False)
|
|
105
|
+
self.assertEqual(evaluate_expression(_df(), " 1 "), 1)
|
|
106
|
+
|
|
107
|
+
def test_rejects_code_execution(self):
|
|
108
|
+
rejected = [
|
|
109
|
+
# attribute chain to reach the os module
|
|
110
|
+
"a.__class__.__init__.__globals__.get('sys').modules.get('os').getpid()",
|
|
111
|
+
"a.sum()",
|
|
112
|
+
"a.values",
|
|
113
|
+
"log(a).real",
|
|
114
|
+
"__import__('os')",
|
|
115
|
+
"eval('1')",
|
|
116
|
+
"exec('1')",
|
|
117
|
+
"open('/etc/passwd')",
|
|
118
|
+
"getattr(a, 'sum')()",
|
|
119
|
+
"(lambda: 1)()",
|
|
120
|
+
"log(a)(1)",
|
|
121
|
+
]
|
|
122
|
+
for expression in rejected:
|
|
123
|
+
with (
|
|
124
|
+
self.subTest(expression=expression),
|
|
125
|
+
self.assertRaises(UserInputError),
|
|
126
|
+
):
|
|
127
|
+
evaluate_expression(_df(), expression)
|
|
128
|
+
|
|
129
|
+
def test_rejects_unsupported_syntax(self):
|
|
130
|
+
rejected = [
|
|
131
|
+
"a ** 2",
|
|
132
|
+
"a @ b",
|
|
133
|
+
"a << 1",
|
|
134
|
+
"a[0]",
|
|
135
|
+
"a is None",
|
|
136
|
+
"[1, 2]",
|
|
137
|
+
"{'x': 1}",
|
|
138
|
+
"f'{a}'",
|
|
139
|
+
"a if b else c",
|
|
140
|
+
"b'bytes'",
|
|
141
|
+
"1j",
|
|
142
|
+
"...",
|
|
143
|
+
"a in b",
|
|
144
|
+
"a in [b]",
|
|
145
|
+
"x := 1",
|
|
146
|
+
"@a",
|
|
147
|
+
"`a`",
|
|
148
|
+
"log(a, 2)",
|
|
149
|
+
"log()",
|
|
150
|
+
"log(a=1)",
|
|
151
|
+
"round(a, 1.5)",
|
|
152
|
+
"round(a, True)",
|
|
153
|
+
"round(a, b)",
|
|
154
|
+
"round(a, 1, 2)",
|
|
155
|
+
"unknown_column + 1",
|
|
156
|
+
"",
|
|
157
|
+
"a +",
|
|
158
|
+
]
|
|
159
|
+
for expression in rejected:
|
|
160
|
+
with (
|
|
161
|
+
self.subTest(expression=expression),
|
|
162
|
+
self.assertRaises(UserInputError),
|
|
163
|
+
):
|
|
164
|
+
evaluate_expression(_df(), expression)
|
|
165
|
+
|
|
166
|
+
def test_rejects_non_string(self):
|
|
167
|
+
with self.assertRaises(UserInputError):
|
|
168
|
+
evaluate_expression(_df(), 1)
|
|
169
|
+
|
|
170
|
+
def test_maximum_length(self):
|
|
171
|
+
expression = "a" + " + a" * ((MAX_EXPRESSION_LENGTH - 1) // 4)
|
|
172
|
+
self.assertLessEqual(len(expression), MAX_EXPRESSION_LENGTH)
|
|
173
|
+
evaluate_expression(_df(), expression)
|
|
174
|
+
with self.assertRaises(UserInputError):
|
|
175
|
+
evaluate_expression(_df(), expression + " + a")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestPreprocessingFunctions(unittest.TestCase):
|
|
179
|
+
def test_assign_column(self):
|
|
180
|
+
df = _assign_column(_df(), "d", "a + b")
|
|
181
|
+
self.assertEqual(df["d"].tolist(), [7, 9, 11, 13, 15])
|
|
182
|
+
|
|
183
|
+
def test_assign_column_rejects_code(self):
|
|
184
|
+
with self.assertRaises(UserInputError):
|
|
185
|
+
_assign_column(_df(), "d", "a.sum()")
|
|
186
|
+
|
|
187
|
+
def test_redefine_column(self):
|
|
188
|
+
df = _redefine_column(_df(), "c", "a * b")
|
|
189
|
+
self.assertEqual(df["c"].tolist(), [6, 14, 24, 36, 50])
|
|
190
|
+
|
|
191
|
+
def test_redefine_column_rejects_code(self):
|
|
192
|
+
with self.assertRaises(UserInputError):
|
|
193
|
+
_redefine_column(_df(), "c", "a.sum()")
|
|
194
|
+
|
|
195
|
+
def test_select_rows(self):
|
|
196
|
+
df = _select_rows(_df(), "a > 2 and s != 'z'")
|
|
197
|
+
self.assertEqual(df["a"].tolist(), [3, 5])
|
|
198
|
+
self.assertEqual(df.index.tolist(), [2, 4])
|
|
199
|
+
|
|
200
|
+
def test_select_rows_rejects_code(self):
|
|
201
|
+
with self.assertRaises(UserInputError):
|
|
202
|
+
_select_rows(_df(), "a.sum() > 0")
|
|
203
|
+
|
|
204
|
+
def test_select_rows_requires_boolean(self):
|
|
205
|
+
for query in ["a + 1", "True", "s"]:
|
|
206
|
+
with self.subTest(query=query), self.assertRaises(UserInputError):
|
|
207
|
+
_select_rows(_df(), query)
|
|
File without changes
|
|
@@ -40,8 +40,11 @@ class MockBaseClient:
|
|
|
40
40
|
"_download_run_data_from_hq",
|
|
41
41
|
"_fetch_and_decrypt_run_data",
|
|
42
42
|
"_multi_page_request",
|
|
43
|
+
"_obtain_client_credentials_tokens",
|
|
43
44
|
"_refresh_token",
|
|
45
|
+
"_store_access_token",
|
|
44
46
|
"_upload_run_data_to_hq",
|
|
47
|
+
"AlgorithmStoreSubClientBase",
|
|
45
48
|
"auth_url",
|
|
46
49
|
"authenticate",
|
|
47
50
|
"check_if_blob_store_enabled",
|
|
@@ -59,6 +62,7 @@ class MockBaseClient:
|
|
|
59
62
|
"hq_url",
|
|
60
63
|
"session_id",
|
|
61
64
|
"setup_encryption",
|
|
65
|
+
"store_headers",
|
|
62
66
|
"store_id",
|
|
63
67
|
"study_id",
|
|
64
68
|
"token",
|
|
@@ -510,6 +514,7 @@ class MockUserClient(MockBaseClient):
|
|
|
510
514
|
self.set_missing_attributes(
|
|
511
515
|
[
|
|
512
516
|
"_get_logger",
|
|
517
|
+
"_obtain_tokens_per_audience",
|
|
513
518
|
"auth_client",
|
|
514
519
|
"auth_realm",
|
|
515
520
|
"authenticate_service_account",
|
|
@@ -10,6 +10,7 @@ from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
|
10
10
|
from vantage6.algorithm.tools.exceptions import UserInputError
|
|
11
11
|
|
|
12
12
|
from vantage6.algorithm.decorator.action import preprocessing
|
|
13
|
+
from vantage6.algorithm.preprocessing.expression import evaluate_expression
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@handle_data_errors
|
|
@@ -68,7 +69,14 @@ def assign_column(df: pd.DataFrame, column_name: str, expression: str) -> pd.Dat
|
|
|
68
69
|
column_name : str
|
|
69
70
|
The name of the new column.
|
|
70
71
|
expression : str
|
|
71
|
-
The expression used to create the new column.
|
|
72
|
+
The expression used to create the new column, e.g. ``weight / (height *
|
|
73
|
+
height)``. Supported are column names, numbers, strings, ``True``, ``False``,
|
|
74
|
+
``None``, the operators ``+ - * / // %`` (only ``+`` for text), comparisons
|
|
75
|
+
(``== != < <= > >=``), ``in``/``not in`` a list of constants,
|
|
76
|
+
``and or not & | ~`` and the functions
|
|
77
|
+
``abs, sqrt, log, log2, log10, exp, floor, ceil, round``. Other functions,
|
|
78
|
+
attribute access, indexing and ``**`` are not supported. The expression may
|
|
79
|
+
be at most 250 characters long.
|
|
72
80
|
|
|
73
81
|
Returns
|
|
74
82
|
-------
|
|
@@ -92,7 +100,7 @@ def assign_column(df: pd.DataFrame, column_name: str, expression: str) -> pd.Dat
|
|
|
92
100
|
if column_name in df.columns:
|
|
93
101
|
raise UserInputError(f"Column '{column_name}' already exists.")
|
|
94
102
|
|
|
95
|
-
df[column_name] = df
|
|
103
|
+
df[column_name] = evaluate_expression(df, expression)
|
|
96
104
|
return df
|
|
97
105
|
|
|
98
106
|
|
|
@@ -111,7 +119,14 @@ def redefine_column(
|
|
|
111
119
|
column_name : str
|
|
112
120
|
The name of the existing column to modify.
|
|
113
121
|
expression : str
|
|
114
|
-
The expression used to modify the existing column.
|
|
122
|
+
The expression used to modify the existing column, e.g. ``round(age / 10)``.
|
|
123
|
+
Supported are column names, numbers, strings, ``True``, ``False``, ``None``,
|
|
124
|
+
the operators ``+ - * / // %`` (only ``+`` for text), comparisons
|
|
125
|
+
(``== != < <= > >=``), ``in``/``not in`` a list of constants,
|
|
126
|
+
``and or not & | ~`` and the functions
|
|
127
|
+
``abs, sqrt, log, log2, log10, exp, floor, ceil, round``. Other functions,
|
|
128
|
+
attribute access, indexing and ``**`` are not supported. The expression may
|
|
129
|
+
be at most 250 characters long.
|
|
115
130
|
|
|
116
131
|
Returns
|
|
117
132
|
-------
|
|
@@ -136,7 +151,7 @@ def redefine_column(
|
|
|
136
151
|
if column_name not in df.columns:
|
|
137
152
|
raise UserInputError(f"Column '{column_name}' does not exist.")
|
|
138
153
|
|
|
139
|
-
df[column_name] = df
|
|
154
|
+
df[column_name] = evaluate_expression(df, expression)
|
|
140
155
|
return df
|
|
141
156
|
|
|
142
157
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe evaluation of expressions on pandas DataFrames.
|
|
3
|
+
|
|
4
|
+
Expressions are provided by users as task arguments, so they must not be evaluated with
|
|
5
|
+
``pandas.DataFrame.eval`` or ``pandas.DataFrame.query``: those allow attribute access and
|
|
6
|
+
method calls, and can be used to execute arbitrary code. Instead, the expression is
|
|
7
|
+
parsed with :mod:`ast` and evaluated by walking the syntax tree, supporting only:
|
|
8
|
+
|
|
9
|
+
- column names, e.g. ``age`` (columns must be valid Python identifiers)
|
|
10
|
+
- numbers, strings, ``True``, ``False`` and ``None``
|
|
11
|
+
- arithmetic: ``+``, ``-``, ``*``, ``/``, ``//``, ``%`` (only ``+`` for text)
|
|
12
|
+
- comparisons: ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, also chained (``1 < a < 5``)
|
|
13
|
+
- membership in a list of constants: ``a in [1, 2]``, ``a not in ['x', 'y']``
|
|
14
|
+
- logical operators: ``and``, ``or``, ``not``, ``&``, ``|``, ``~``
|
|
15
|
+
- the functions ``abs``, ``sqrt``, ``log``, ``log2``, ``log10``, ``exp``, ``floor``,
|
|
16
|
+
``ceil`` and ``round`` (``round(a)`` or ``round(a, 2)``)
|
|
17
|
+
|
|
18
|
+
Anything else, such as other functions, attribute access (``a.b``), indexing
|
|
19
|
+
(``a[0]``) and exponentiation (``**``), is rejected. Expressions may be at most
|
|
20
|
+
``MAX_EXPRESSION_LENGTH`` characters long.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
import operator
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import pandas as pd
|
|
30
|
+
|
|
31
|
+
from vantage6.algorithm.tools.exceptions import UserInputError
|
|
32
|
+
|
|
33
|
+
MAX_EXPRESSION_LENGTH = 250
|
|
34
|
+
|
|
35
|
+
_BINARY_OPERATORS: dict[type[ast.operator], Callable] = {
|
|
36
|
+
ast.Add: operator.add,
|
|
37
|
+
ast.Sub: operator.sub,
|
|
38
|
+
ast.Mult: operator.mul,
|
|
39
|
+
ast.Div: operator.truediv,
|
|
40
|
+
ast.FloorDiv: operator.floordiv,
|
|
41
|
+
ast.Mod: operator.mod,
|
|
42
|
+
ast.BitAnd: operator.and_,
|
|
43
|
+
ast.BitOr: operator.or_,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_COMPARISON_OPERATORS: dict[type[ast.cmpop], Callable] = {
|
|
47
|
+
ast.Eq: operator.eq,
|
|
48
|
+
ast.NotEq: operator.ne,
|
|
49
|
+
ast.Lt: operator.lt,
|
|
50
|
+
ast.LtE: operator.le,
|
|
51
|
+
ast.Gt: operator.gt,
|
|
52
|
+
ast.GtE: operator.ge,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Functions that may be called in expressions, with the number of arguments they take
|
|
56
|
+
_FUNCTIONS: dict[str, tuple[Callable, int]] = {
|
|
57
|
+
"abs": (np.abs, 1),
|
|
58
|
+
"sqrt": (np.sqrt, 1),
|
|
59
|
+
"log": (np.log, 1),
|
|
60
|
+
"log2": (np.log2, 1),
|
|
61
|
+
"log10": (np.log10, 1),
|
|
62
|
+
"exp": (np.exp, 1),
|
|
63
|
+
"floor": (np.floor, 1),
|
|
64
|
+
"ceil": (np.ceil, 1),
|
|
65
|
+
}
|
|
66
|
+
# `round` is handled separately, as its second argument must be an integer constant
|
|
67
|
+
_SUPPORTED_FUNCTIONS = sorted([*_FUNCTIONS, "round"])
|
|
68
|
+
|
|
69
|
+
_CONSTANT_TYPES = (int, float, str, bool, type(None))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def evaluate_expression(df: pd.DataFrame, expression: str) -> Any:
|
|
73
|
+
"""
|
|
74
|
+
Evaluate an expression on a DataFrame.
|
|
75
|
+
|
|
76
|
+
See the module docstring for the supported syntax.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
df : pandas.DataFrame
|
|
81
|
+
The DataFrame whose columns may be used in the expression.
|
|
82
|
+
expression : str
|
|
83
|
+
The expression to evaluate.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
Any
|
|
88
|
+
The result of the expression: usually a pandas Series, or a scalar if the
|
|
89
|
+
expression does not refer to any column.
|
|
90
|
+
|
|
91
|
+
Raises
|
|
92
|
+
------
|
|
93
|
+
UserInputError
|
|
94
|
+
If the expression is too long, cannot be parsed or uses unsupported syntax.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(expression, str):
|
|
97
|
+
raise UserInputError("Expression must be a string.")
|
|
98
|
+
if len(expression) > MAX_EXPRESSION_LENGTH:
|
|
99
|
+
raise UserInputError(
|
|
100
|
+
f"Expression is too long (maximum is {MAX_EXPRESSION_LENGTH} characters)."
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
tree = ast.parse(expression.strip(), mode="eval")
|
|
104
|
+
except (SyntaxError, ValueError, RecursionError, MemoryError):
|
|
105
|
+
raise UserInputError("Expression could not be parsed.") from None
|
|
106
|
+
return _Evaluator(df).visit(tree.body)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _Evaluator:
|
|
110
|
+
"""Evaluate the nodes of a parsed expression that are explicitly supported."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, df: pd.DataFrame) -> None:
|
|
113
|
+
self.df = df
|
|
114
|
+
|
|
115
|
+
def visit(self, node: ast.AST) -> Any:
|
|
116
|
+
if isinstance(node, ast.Name):
|
|
117
|
+
return self._visit_name(node)
|
|
118
|
+
if isinstance(node, ast.Constant):
|
|
119
|
+
return self._visit_constant(node)
|
|
120
|
+
if isinstance(node, ast.BinOp):
|
|
121
|
+
return self._visit_binop(node)
|
|
122
|
+
if isinstance(node, ast.UnaryOp):
|
|
123
|
+
return self._visit_unaryop(node)
|
|
124
|
+
if isinstance(node, ast.BoolOp):
|
|
125
|
+
return self._visit_boolop(node)
|
|
126
|
+
if isinstance(node, ast.Compare):
|
|
127
|
+
return self._visit_compare(node)
|
|
128
|
+
if isinstance(node, ast.Call):
|
|
129
|
+
return self._visit_call(node)
|
|
130
|
+
raise UserInputError(_unsupported(node))
|
|
131
|
+
|
|
132
|
+
def _visit_name(self, node: ast.Name) -> pd.Series:
|
|
133
|
+
if node.id not in self.df.columns:
|
|
134
|
+
raise UserInputError(f"Column '{node.id}' does not exist.")
|
|
135
|
+
return self.df[node.id]
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _visit_constant(node: ast.Constant) -> Any:
|
|
139
|
+
if not isinstance(node.value, _CONSTANT_TYPES):
|
|
140
|
+
raise UserInputError(_unsupported(node))
|
|
141
|
+
return node.value
|
|
142
|
+
|
|
143
|
+
def _visit_binop(self, node: ast.BinOp) -> Any:
|
|
144
|
+
op = _BINARY_OPERATORS.get(type(node.op))
|
|
145
|
+
if op is None:
|
|
146
|
+
raise UserInputError(_unsupported(node.op))
|
|
147
|
+
left, right = self.visit(node.left), self.visit(node.right)
|
|
148
|
+
# only allow joining text, as e.g. repeating text (`'x' * 999999999`) could
|
|
149
|
+
# use excessive memory
|
|
150
|
+
if not isinstance(node.op, ast.Add) and (_is_text(left) or _is_text(right)):
|
|
151
|
+
raise UserInputError("Only '+' is supported for text.")
|
|
152
|
+
return op(left, right)
|
|
153
|
+
|
|
154
|
+
def _visit_unaryop(self, node: ast.UnaryOp) -> Any:
|
|
155
|
+
operand = self.visit(node.operand)
|
|
156
|
+
if isinstance(node.op, ast.USub):
|
|
157
|
+
return -operand
|
|
158
|
+
if isinstance(node.op, ast.UAdd):
|
|
159
|
+
return +operand
|
|
160
|
+
if isinstance(node.op, (ast.Not, ast.Invert)):
|
|
161
|
+
# like pandas, `not` and `~` are element-wise negations of booleans
|
|
162
|
+
if isinstance(operand, (bool, np.bool_)):
|
|
163
|
+
return not operand
|
|
164
|
+
return ~operand
|
|
165
|
+
raise UserInputError(_unsupported(node.op))
|
|
166
|
+
|
|
167
|
+
def _visit_boolop(self, node: ast.BoolOp) -> Any:
|
|
168
|
+
# like pandas, `and` and `or` are evaluated element-wise
|
|
169
|
+
op = operator.and_ if isinstance(node.op, ast.And) else operator.or_
|
|
170
|
+
result = self.visit(node.values[0])
|
|
171
|
+
for value in node.values[1:]:
|
|
172
|
+
result = op(result, self.visit(value))
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
def _visit_compare(self, node: ast.Compare) -> Any:
|
|
176
|
+
result = None
|
|
177
|
+
left = self.visit(node.left)
|
|
178
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
179
|
+
if isinstance(op, (ast.In, ast.NotIn)):
|
|
180
|
+
right = self._visit_constant_list(comparator)
|
|
181
|
+
comparison = _isin(left, right)
|
|
182
|
+
if isinstance(op, ast.NotIn):
|
|
183
|
+
comparison = (
|
|
184
|
+
not comparison if isinstance(comparison, bool) else ~comparison
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
compare = _COMPARISON_OPERATORS.get(type(op))
|
|
188
|
+
if compare is None:
|
|
189
|
+
raise UserInputError(_unsupported(op))
|
|
190
|
+
right = self.visit(comparator)
|
|
191
|
+
comparison = compare(left, right)
|
|
192
|
+
# chained comparisons (e.g. `1 < a < 5`) must all be true
|
|
193
|
+
result = comparison if result is None else operator.and_(result, comparison)
|
|
194
|
+
left = right
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
def _visit_constant_list(self, node: ast.AST) -> list:
|
|
198
|
+
if not isinstance(node, (ast.List, ast.Tuple)) or not all(
|
|
199
|
+
isinstance(element, ast.Constant) for element in node.elts
|
|
200
|
+
):
|
|
201
|
+
raise UserInputError(
|
|
202
|
+
"Only a list of constants is supported after 'in' or 'not in'."
|
|
203
|
+
)
|
|
204
|
+
return [self._visit_constant(element) for element in node.elts]
|
|
205
|
+
|
|
206
|
+
def _visit_call(self, node: ast.Call) -> Any:
|
|
207
|
+
if (
|
|
208
|
+
not isinstance(node.func, ast.Name)
|
|
209
|
+
or node.func.id not in _SUPPORTED_FUNCTIONS
|
|
210
|
+
):
|
|
211
|
+
raise UserInputError(
|
|
212
|
+
f"Only the functions {', '.join(_SUPPORTED_FUNCTIONS)} are supported."
|
|
213
|
+
)
|
|
214
|
+
name = node.func.id
|
|
215
|
+
if node.keywords:
|
|
216
|
+
raise UserInputError(
|
|
217
|
+
f"Function '{name}' does not accept keyword arguments."
|
|
218
|
+
)
|
|
219
|
+
if name == "round":
|
|
220
|
+
return self._visit_round(node)
|
|
221
|
+
function, n_args = _FUNCTIONS[name]
|
|
222
|
+
if len(node.args) != n_args:
|
|
223
|
+
raise UserInputError(f"Function '{name}' takes {n_args} argument(s).")
|
|
224
|
+
return function(*(self.visit(arg) for arg in node.args))
|
|
225
|
+
|
|
226
|
+
def _visit_round(self, node: ast.Call) -> Any:
|
|
227
|
+
if len(node.args) not in (1, 2):
|
|
228
|
+
raise UserInputError("Function 'round' takes 1 or 2 arguments.")
|
|
229
|
+
decimals = 0
|
|
230
|
+
if len(node.args) == 2:
|
|
231
|
+
arg = node.args[1]
|
|
232
|
+
if not (
|
|
233
|
+
isinstance(arg, ast.Constant)
|
|
234
|
+
and isinstance(arg.value, int)
|
|
235
|
+
and not isinstance(arg.value, bool)
|
|
236
|
+
):
|
|
237
|
+
raise UserInputError(
|
|
238
|
+
"The number of decimals of 'round' must be an integer."
|
|
239
|
+
)
|
|
240
|
+
decimals = arg.value
|
|
241
|
+
return np.round(self.visit(node.args[0]), decimals)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _is_text(value: Any) -> bool:
|
|
245
|
+
if isinstance(value, pd.Series):
|
|
246
|
+
return pd.api.types.is_string_dtype(value) or pd.api.types.is_object_dtype(
|
|
247
|
+
value
|
|
248
|
+
)
|
|
249
|
+
return isinstance(value, str)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _isin(value: Any, options: list) -> Any:
|
|
253
|
+
if isinstance(value, pd.Series):
|
|
254
|
+
return value.isin(options)
|
|
255
|
+
return value in options
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _unsupported(node: ast.AST) -> str:
|
|
259
|
+
return f"'{type(node).__name__}' is not supported in expressions."
|
|
@@ -6,25 +6,31 @@ pandas DataFrames, such as selecting and filtering rows and columns.
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
8
|
from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
9
|
+
from vantage6.algorithm.tools.exceptions import UserInputError
|
|
9
10
|
|
|
10
11
|
from vantage6.algorithm.decorator.action import preprocessing
|
|
12
|
+
from vantage6.algorithm.preprocessing.expression import evaluate_expression
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@handle_data_errors
|
|
14
16
|
@preprocessing
|
|
15
17
|
def select_rows(df: pd.DataFrame, query: str) -> pd.DataFrame:
|
|
16
18
|
"""
|
|
17
|
-
Select rows
|
|
18
|
-
pandas.DataFrame.query function to filter the data.
|
|
19
|
-
|
|
20
|
-
See the documentation of that function for more information on the query syntax.
|
|
19
|
+
Select the rows of the data for which the query is true.
|
|
21
20
|
|
|
22
21
|
Parameters
|
|
23
22
|
----------
|
|
24
23
|
df : pd.DataFrame
|
|
25
24
|
The data to filter.
|
|
26
25
|
query : str
|
|
27
|
-
The
|
|
26
|
+
The condition that rows must meet, e.g. ``age >= 18 and sex == 'F'``.
|
|
27
|
+
Supported are column names, numbers, strings, ``True``, ``False``, ``None``,
|
|
28
|
+
the operators ``+ - * / // %`` (only ``+`` for text), comparisons
|
|
29
|
+
(``== != < <= > >=``), ``in``/``not in`` a list of constants,
|
|
30
|
+
``and or not & | ~`` and the functions
|
|
31
|
+
``abs, sqrt, log, log2, log10, exp, floor, ceil, round``. Other functions,
|
|
32
|
+
attribute access, indexing and ``**`` are not supported. The query may be at
|
|
33
|
+
most 250 characters long.
|
|
28
34
|
|
|
29
35
|
Returns
|
|
30
36
|
-------
|
|
@@ -73,9 +79,15 @@ def select_rows(df: pd.DataFrame, query: str) -> pd.DataFrame:
|
|
|
73
79
|
3 4 9 14
|
|
74
80
|
4 5 10 15
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
>>> select_rows(df, "a in [1, 5]")
|
|
83
|
+
a b c
|
|
84
|
+
0 1 6 11
|
|
85
|
+
4 5 10 15
|
|
77
86
|
"""
|
|
78
|
-
|
|
87
|
+
mask = evaluate_expression(df, query)
|
|
88
|
+
if not isinstance(mask, pd.Series) or not pd.api.types.is_bool_dtype(mask):
|
|
89
|
+
raise UserInputError("The query must evaluate to true or false for each row.")
|
|
90
|
+
return df[mask]
|
|
79
91
|
|
|
80
92
|
|
|
81
93
|
@handle_data_errors
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/_test_preprocessing.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/_test_serialization.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/algorithm/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/algorithm_module.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/base/__init__.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_network.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_node.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_server.py
RENAMED
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_tools-5.0.3rc5 → vantage6_algorithm_tools-5.0.4rc1}/tests/mock/test_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|