vantage6-algorithm-tools 5.0.2rc2__tar.gz → 5.0.3rc5__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.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/PKG-INFO +3 -3
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/pyproject.toml +2 -2
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/_test_preprocessing.py +5 -5
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/algorithm/test___init__.py +1 -5
- vantage6_algorithm_tools-5.0.3rc5/tests/tools/__init__.py +0 -0
- vantage6_algorithm_tools-5.0.3rc5/tests/tools/test_error_handling.py +36 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/client/__init__.py +6 -7
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/data_extraction/__init__.py +10 -10
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/data_extraction/mock_extract.py +3 -2
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/decorator/action.py +3 -2
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/decorator/algorithm_client.py +2 -1
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/decorator/data.py +5 -4
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/decorator/ohdsi.py +4 -3
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/mock/client.py +54 -53
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/mock/hq.py +4 -4
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/mock/network.py +3 -1
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/mock/node.py +5 -1
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/preprocessing/aggregation.py +3 -3
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/preprocessing/column.py +5 -5
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/preprocessing/datetime.py +7 -7
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/preprocessing/encoding.py +8 -8
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/preprocessing/filtering.py +8 -8
- vantage6_algorithm_tools-5.0.3rc5/vantage6/algorithm/tools/error_handling.py +48 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/tools/util.py +1 -1
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/tools/wrap.py +7 -6
- vantage6_algorithm_tools-5.0.2rc2/vantage6/algorithm/tools/error_handling.py +0 -59
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/.gitignore +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/Makefile +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/README.md +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/_test_deserialization.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/_test_docker_wrapper.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/_test_serialization.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/algorithm/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/algorithm_module.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/base/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_algorithm_client.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_network.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_node.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_server.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_user_client.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_util.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/decorator/__init__.py +3 -3
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/decorator/metadata.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/mock/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/mock/globals.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/mock/util.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/preprocessing/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/preprocessing/algorithm_json_data.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/tools/__init__.py +0 -0
- {vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/vantage6/algorithm/tools/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: vantage6-algorithm-tools
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.3rc5
|
|
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>
|
|
@@ -14,7 +14,7 @@ Requires-Dist: pyfiglet==1.0.4
|
|
|
14
14
|
Requires-Dist: pyjwt==2.13.0
|
|
15
15
|
Requires-Dist: sparqlwrapper>=2.0.0
|
|
16
16
|
Requires-Dist: sqlalchemy>=1.4.46
|
|
17
|
-
Requires-Dist: vantage6-common==5.0.
|
|
17
|
+
Requires-Dist: vantage6-common==5.0.3rc5
|
|
18
18
|
Provides-Extra: dev
|
|
19
19
|
Requires-Dist: black; extra == 'dev'
|
|
20
20
|
Requires-Dist: coverage==7.10.2; extra == 'dev'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vantage6-algorithm-tools"
|
|
7
|
-
version = "5.0.
|
|
7
|
+
version = "5.0.3rc5"
|
|
8
8
|
description = "Vantage6 algorithm tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
"pyfiglet==1.0.4",
|
|
28
28
|
"SPARQLWrapper>=2.0.0",
|
|
29
29
|
"sqlalchemy>=1.4.46",
|
|
30
|
-
"vantage6-common==5.0.
|
|
30
|
+
"vantage6-common==5.0.3rc5",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/_test_preprocessing.py
RENAMED
|
@@ -6,7 +6,7 @@ python -m unittest vantage6-algorithm-tools.tests.test_preprocessing
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import unittest
|
|
9
|
-
from datetime import datetime, timedelta
|
|
9
|
+
from datetime import UTC, datetime, timedelta
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import pandas as pd
|
|
@@ -16,7 +16,7 @@ from vantage6.algorithm.preprocessing.filtering import select_rows
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def random_date(
|
|
19
|
-
start: datetime, end: datetime, fmt: str = "%Y-%m-%d", seed: int = None
|
|
19
|
+
start: datetime, end: datetime, fmt: str = "%Y-%m-%d", seed: int | None = None
|
|
20
20
|
) -> str:
|
|
21
21
|
"""
|
|
22
22
|
Generate a random date between start and end.
|
|
@@ -537,8 +537,8 @@ class TestPreprocessing(unittest.TestCase):
|
|
|
537
537
|
df = get_test_dataframe()
|
|
538
538
|
|
|
539
539
|
# Set start and end dates for random date generation
|
|
540
|
-
start_date = datetime(2020, 1, 1)
|
|
541
|
-
end_date = datetime(2022, 12, 31)
|
|
540
|
+
start_date = datetime(2020, 1, 1, tzinfo=UTC)
|
|
541
|
+
end_date = datetime(2022, 12, 31, tzinfo=UTC)
|
|
542
542
|
|
|
543
543
|
# Generate a new column with random dates in string format
|
|
544
544
|
magic_fmt = "(%d)+(%m)=(%Y)"
|
|
@@ -637,7 +637,7 @@ class TestPreprocessing(unittest.TestCase):
|
|
|
637
637
|
'{"age":{"171":23,"342":23},"income":{"171":62026,"342":44074},'
|
|
638
638
|
'"education":{"171":"Master","342":"Master"},"color_preference":'
|
|
639
639
|
'{"171":"Green","342":"Green"},"purchased_product":{"171":1,"342":'
|
|
640
|
-
'0},"magic_date":{"171":"2022-11-12","342":"2020-01-20"},"income\/'
|
|
640
|
+
r'0},"magic_date":{"171":"2022-11-12","342":"2020-01-20"},"income\/'
|
|
641
641
|
'age":{"171":0.9737126326,"342":0.0147479299},"age_days":{"171":'
|
|
642
642
|
'8395,"342":8395},"age_days_td":{"171":"8395 days","342":"8395 '
|
|
643
643
|
'days"}}'
|
|
@@ -88,11 +88,7 @@ class TestAlgorithmClient(unittest.TestCase):
|
|
|
88
88
|
results = self.client.result.from_task(task_id=1)
|
|
89
89
|
self.assertEqual(results[0], {"foo": "bar"})
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
@patch("vantage6.common.client.client_base.ClientBase._download_run_data_from_hq")
|
|
93
|
-
def test_result_from_task_azure(
|
|
94
|
-
self, mock_download_run_data, mock_multi_page_request
|
|
95
|
-
):
|
|
91
|
+
def test_result_from_task_azure(self):
|
|
96
92
|
with (
|
|
97
93
|
patch.object(
|
|
98
94
|
self.client.result.parent, "_multi_page_request"
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
5
|
+
from vantage6.algorithm.tools.exceptions import AlgorithmRuntimeError, UserInputError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestHandleDataErrors(unittest.TestCase):
|
|
9
|
+
def test_non_pandas_error_is_sanitized(self):
|
|
10
|
+
"""Errors that are not raised by vantage6 should not leak any details."""
|
|
11
|
+
|
|
12
|
+
@handle_data_errors
|
|
13
|
+
def func():
|
|
14
|
+
raise ValueError("could not convert string to float: 'secret_42'")
|
|
15
|
+
|
|
16
|
+
with self.assertRaises(AlgorithmRuntimeError) as ctx:
|
|
17
|
+
func()
|
|
18
|
+
|
|
19
|
+
self.assertNotIn("secret_42", str(ctx.exception))
|
|
20
|
+
# the original error should not be printed when the traceback is logged
|
|
21
|
+
traceback_ = "".join(traceback.format_exception(ctx.exception))
|
|
22
|
+
self.assertNotIn("secret_42", traceback_)
|
|
23
|
+
|
|
24
|
+
def test_algorithm_errors_pass_through(self):
|
|
25
|
+
"""Vantage6 errors are meant for the user and should not be replaced."""
|
|
26
|
+
|
|
27
|
+
@handle_data_errors
|
|
28
|
+
def func():
|
|
29
|
+
raise UserInputError("old_names and new_names must have the same length")
|
|
30
|
+
|
|
31
|
+
with self.assertRaises(UserInputError):
|
|
32
|
+
func()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
unittest.main()
|
|
@@ -136,7 +136,7 @@ class AlgorithmClient(ClientBase):
|
|
|
136
136
|
|
|
137
137
|
return self.result.from_task(task_id)
|
|
138
138
|
|
|
139
|
-
def _multi_page_request(self, endpoint: str, params: dict = None) -> dict:
|
|
139
|
+
def _multi_page_request(self, endpoint: str, params: dict | None = None) -> dict:
|
|
140
140
|
"""
|
|
141
141
|
Make multiple requests to HQ to get all pages of a list of results.
|
|
142
142
|
|
|
@@ -247,9 +247,8 @@ class AlgorithmClient(ClientBase):
|
|
|
247
247
|
result = json.loads(
|
|
248
248
|
base64s_to_bytes(response.get("result")).decode()
|
|
249
249
|
)
|
|
250
|
-
except Exception
|
|
251
|
-
self.parent.log.
|
|
252
|
-
self.parent.log.exception(e)
|
|
250
|
+
except Exception:
|
|
251
|
+
self.parent.log.exception("Unable to load results")
|
|
253
252
|
return result
|
|
254
253
|
|
|
255
254
|
def from_task(self, task_id: int) -> list[Any]:
|
|
@@ -288,7 +287,7 @@ class AlgorithmClient(ClientBase):
|
|
|
288
287
|
return json.loads(run_data.decode(STRING_ENCODING))
|
|
289
288
|
else:
|
|
290
289
|
return json.loads(base64s_to_bytes(result_data).decode())
|
|
291
|
-
except Exception as e:
|
|
290
|
+
except Exception as e: # noqa: BLE001
|
|
292
291
|
self.parent.log.error(
|
|
293
292
|
f"Unable to load results for task {task_id}: {e}"
|
|
294
293
|
)
|
|
@@ -328,9 +327,9 @@ class AlgorithmClient(ClientBase):
|
|
|
328
327
|
self,
|
|
329
328
|
method: str,
|
|
330
329
|
arguments: dict | None = None,
|
|
331
|
-
organizations: list[int] = None,
|
|
330
|
+
organizations: list[int] | None = None,
|
|
332
331
|
name: str = "subtask",
|
|
333
|
-
description: str = None,
|
|
332
|
+
description: str | None = None,
|
|
334
333
|
) -> dict:
|
|
335
334
|
"""
|
|
336
335
|
Create a new (child) task at HQ.
|
|
@@ -7,7 +7,7 @@ from sqlalchemy import create_engine
|
|
|
7
7
|
|
|
8
8
|
from vantage6.common import error, info
|
|
9
9
|
|
|
10
|
-
from vantage6.algorithm.tools.error_handling import
|
|
10
|
+
from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
11
11
|
from vantage6.algorithm.tools.exceptions import DataReadError
|
|
12
12
|
|
|
13
13
|
from vantage6.algorithm.decorator.action import data_extraction
|
|
@@ -15,7 +15,7 @@ from vantage6.algorithm.decorator.action import data_extraction
|
|
|
15
15
|
_SPARQL_RETURN_FORMAT = CSV
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
@
|
|
18
|
+
@handle_data_errors
|
|
19
19
|
@data_extraction
|
|
20
20
|
def read_csv(connection_details: dict) -> pd.DataFrame:
|
|
21
21
|
"""
|
|
@@ -48,14 +48,14 @@ def _read_csv(connection_details: dict) -> pd.DataFrame:
|
|
|
48
48
|
error(f"File not found: {e}")
|
|
49
49
|
# pylint: disable=raise-missing-from
|
|
50
50
|
raise DataReadError(f"File not found: {e}")
|
|
51
|
-
except Exception as e:
|
|
51
|
+
except Exception as e: # noqa: BLE001
|
|
52
52
|
error(f"Error reading CSV file: {e}")
|
|
53
53
|
# pylint: disable=raise-missing-from
|
|
54
54
|
raise DataReadError(f"Error reading CSV file: {e}")
|
|
55
55
|
return df
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
@
|
|
58
|
+
@handle_data_errors
|
|
59
59
|
@data_extraction
|
|
60
60
|
def read_parquet(connection_details: dict) -> pd.DataFrame:
|
|
61
61
|
"""
|
|
@@ -87,14 +87,14 @@ def _read_parquet(connection_details: dict) -> pd.DataFrame:
|
|
|
87
87
|
error(f"File not found: {e}")
|
|
88
88
|
# pylint: disable=raise-missing-from
|
|
89
89
|
raise DataReadError(f"File not found: {e}")
|
|
90
|
-
except Exception as e:
|
|
90
|
+
except Exception as e: # noqa: BLE001
|
|
91
91
|
error(f"Error reading Parquet file: {e}")
|
|
92
92
|
# pylint: disable=raise-missing-from
|
|
93
93
|
raise DataReadError(f"Error reading Parquet file: {e}")
|
|
94
94
|
return df
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
@
|
|
97
|
+
@handle_data_errors
|
|
98
98
|
@data_extraction
|
|
99
99
|
def read_excel(connection_details: dict, sheet_name: str | None = None) -> pd.DataFrame:
|
|
100
100
|
"""
|
|
@@ -137,14 +137,14 @@ def _read_excel(
|
|
|
137
137
|
error(f"File not found: {e}")
|
|
138
138
|
# pylint: disable=raise-missing-from
|
|
139
139
|
raise DataReadError(f"File not found: {e}")
|
|
140
|
-
except Exception as e:
|
|
140
|
+
except Exception as e: # noqa: BLE001
|
|
141
141
|
error(f"Error reading Excel file: {e}")
|
|
142
142
|
# pylint: disable=raise-missing-from
|
|
143
143
|
raise DataReadError(f"Error reading Excel file: {e}")
|
|
144
144
|
return df
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
@
|
|
147
|
+
@handle_data_errors
|
|
148
148
|
@data_extraction
|
|
149
149
|
def read_sparql_database(connection_details: dict, query: str) -> pd.DataFrame:
|
|
150
150
|
"""
|
|
@@ -180,7 +180,7 @@ def _read_sparql_database(connection_details: dict, query: str) -> pd.DataFrame:
|
|
|
180
180
|
|
|
181
181
|
try:
|
|
182
182
|
df = pd.read_csv(io.StringIO(result))
|
|
183
|
-
except Exception as e:
|
|
183
|
+
except Exception as e: # noqa: BLE001
|
|
184
184
|
error(f"Error reading SPARQL data: {e}")
|
|
185
185
|
# pylint: disable=raise-missing-from
|
|
186
186
|
raise DataReadError(f"Error reading SPARQL data: {e}")
|
|
@@ -230,7 +230,7 @@ def _sqldb_uri_preprocess(database_uri: str) -> str:
|
|
|
230
230
|
return database_uri
|
|
231
231
|
|
|
232
232
|
|
|
233
|
-
@
|
|
233
|
+
@handle_data_errors
|
|
234
234
|
@data_extraction
|
|
235
235
|
def read_sql_database(connection_details: dict, query: str) -> pd.DataFrame:
|
|
236
236
|
"""
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from collections.abc import Callable
|
|
2
3
|
|
|
3
4
|
import pandas as pd
|
|
@@ -70,7 +71,7 @@ def load_mock_data(
|
|
|
70
71
|
" Please check the node configuration."
|
|
71
72
|
)
|
|
72
73
|
info(f"Available mock database types: {', '.join(MockDatabaseType.list())}")
|
|
73
|
-
exit(1)
|
|
74
|
+
sys.exit(1)
|
|
74
75
|
|
|
75
76
|
connection_details = {
|
|
76
77
|
"uri": database_uri,
|
|
@@ -84,7 +85,7 @@ def load_mock_data(
|
|
|
84
85
|
):
|
|
85
86
|
if not query:
|
|
86
87
|
error(f"Query is required for {database_type} databases.")
|
|
87
|
-
exit(1)
|
|
88
|
+
sys.exit(1)
|
|
88
89
|
return loader(connection_details, query)
|
|
89
90
|
else:
|
|
90
91
|
return loader(connection_details)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
from collections.abc import Callable
|
|
3
4
|
from functools import wraps
|
|
4
5
|
from typing import Any
|
|
@@ -181,10 +182,10 @@ def data_extraction(func: Callable) -> Callable:
|
|
|
181
182
|
type_ = get_env_var(ContainerEnvNames.DATABASE_TYPE.value, mock_type)
|
|
182
183
|
if not uri:
|
|
183
184
|
error("No database URI provided. Exiting...")
|
|
184
|
-
exit(1)
|
|
185
|
+
sys.exit(1)
|
|
185
186
|
if not type_:
|
|
186
187
|
error("No database type provided. Exiting...")
|
|
187
|
-
exit(1)
|
|
188
|
+
sys.exit(1)
|
|
188
189
|
|
|
189
190
|
connection_details["uri"] = uri
|
|
190
191
|
connection_details["type"] = type_
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
from collections.abc import Callable
|
|
3
4
|
from functools import wraps
|
|
4
5
|
from typing import TYPE_CHECKING, Optional
|
|
@@ -69,7 +70,7 @@ def _algorithm_client() -> Callable:
|
|
|
69
70
|
"Token not found. Is the method you called started as a "
|
|
70
71
|
"compute container? Exiting..."
|
|
71
72
|
)
|
|
72
|
-
exit(1)
|
|
73
|
+
sys.exit(1)
|
|
73
74
|
|
|
74
75
|
# read HQ address from the environment
|
|
75
76
|
host = os.environ[ContainerEnvNames.HOST.value]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
from collections.abc import Callable
|
|
3
4
|
from functools import wraps
|
|
4
5
|
|
|
@@ -90,7 +91,7 @@ def _validate_user_provided_dataframes(
|
|
|
90
91
|
f"but only {len(dataframes_grouped)} were provided. "
|
|
91
92
|
"Exiting..."
|
|
92
93
|
)
|
|
93
|
-
exit(1)
|
|
94
|
+
sys.exit(1)
|
|
94
95
|
elif len(dataframes_grouped) > number_of_expected_arguments:
|
|
95
96
|
warn(
|
|
96
97
|
f"Algorithm requires only {number_of_expected_arguments} databases"
|
|
@@ -143,7 +144,7 @@ def _read_dataframes_from_disk(
|
|
|
143
144
|
"for preprocessing functions. Use @preprocessing instead. "
|
|
144
145
|
"Exiting..."
|
|
145
146
|
)
|
|
146
|
-
exit(1)
|
|
147
|
+
sys.exit(1)
|
|
147
148
|
requested_dataframes = dataframes_grouped[idx_dataframes]
|
|
148
149
|
data_ = {
|
|
149
150
|
df_name: _read_df_from_disk(df_name) for df_name in requested_dataframes
|
|
@@ -243,13 +244,13 @@ def dataframe(*sources: str | int) -> Callable:
|
|
|
243
244
|
"The @dataframe decorator cannot be used for data extraction "
|
|
244
245
|
"functions. Exiting..."
|
|
245
246
|
)
|
|
246
|
-
exit(1)
|
|
247
|
+
sys.exit(1)
|
|
247
248
|
|
|
248
249
|
if getattr(func, "vantage6_dataframe_decorated", False):
|
|
249
250
|
error(
|
|
250
251
|
"The @dataframe decorator cannot be used multiple times. Exiting..."
|
|
251
252
|
)
|
|
252
|
-
exit(1)
|
|
253
|
+
sys.exit(1)
|
|
253
254
|
|
|
254
255
|
# read the data from the database(s)
|
|
255
256
|
dataframes = _read_dataframes_from_disk(dataframes_grouped, sources, action)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
from collections.abc import Callable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from functools import wraps
|
|
@@ -126,13 +127,13 @@ def _create_omop_database_connection(connection_details: dict) -> Callable:
|
|
|
126
127
|
if not OHDSI_AVAILABLE:
|
|
127
128
|
error("OHDSI/DatabaseConnector is not available.")
|
|
128
129
|
error("Did you use 'algorithm-ohdsi-base' image to build this algorithm?")
|
|
129
|
-
exit(1)
|
|
130
|
+
sys.exit(1)
|
|
130
131
|
|
|
131
132
|
expected_keys = ["dbms", "uri", "user", "password"]
|
|
132
133
|
if not set(expected_keys).issubset(connection_details):
|
|
133
134
|
missing_keys = set(expected_keys) - set(connection_details)
|
|
134
135
|
error(f"Missing connection details: {missing_keys}. Exiting...")
|
|
135
|
-
exit(1)
|
|
136
|
+
sys.exit(1)
|
|
136
137
|
|
|
137
138
|
info("Creating OHDSI database connection")
|
|
138
139
|
dbms = connection_details["dbms"]
|
|
@@ -176,7 +177,7 @@ def _get_ohdsi_metadata(connection_details: dict) -> OHDSIMetaData:
|
|
|
176
177
|
missing_env_vars = expected_env_vars - connection_details
|
|
177
178
|
error(f"Missing connection details: {missing_env_vars}")
|
|
178
179
|
error("Exiting...")
|
|
179
|
-
exit(1)
|
|
180
|
+
sys.exit(1)
|
|
180
181
|
|
|
181
182
|
tmp = Path(os.environ[ContainerEnvNames.SESSION_FOLDER.value])
|
|
182
183
|
metadata = OHDSIMetaData(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import sys
|
|
2
3
|
import traceback
|
|
3
4
|
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
@@ -29,49 +30,47 @@ class MockBaseClient:
|
|
|
29
30
|
# Which organization do I belong to?
|
|
30
31
|
self.organization_id = 0
|
|
31
32
|
# Store missing attributes in a set for __getattr__ to check
|
|
32
|
-
self._missing_attributes =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
]
|
|
68
|
-
)
|
|
33
|
+
self._missing_attributes = {
|
|
34
|
+
"_access_token",
|
|
35
|
+
"_ClientBase__auth_url",
|
|
36
|
+
"_ClientBase__check_algorithm_store_valid",
|
|
37
|
+
"_ClientBase__hq_url",
|
|
38
|
+
"_decrypt_run_data",
|
|
39
|
+
"_decrypt_field",
|
|
40
|
+
"_download_run_data_from_hq",
|
|
41
|
+
"_fetch_and_decrypt_run_data",
|
|
42
|
+
"_multi_page_request",
|
|
43
|
+
"_refresh_token",
|
|
44
|
+
"_upload_run_data_to_hq",
|
|
45
|
+
"auth_url",
|
|
46
|
+
"authenticate",
|
|
47
|
+
"check_if_blob_store_enabled",
|
|
48
|
+
"collaboration_id",
|
|
49
|
+
"cryptor",
|
|
50
|
+
"databases",
|
|
51
|
+
"generate_path_to",
|
|
52
|
+
"headers",
|
|
53
|
+
"image",
|
|
54
|
+
"log",
|
|
55
|
+
"name",
|
|
56
|
+
"node_id",
|
|
57
|
+
"obtain_new_token",
|
|
58
|
+
"request",
|
|
59
|
+
"hq_url",
|
|
60
|
+
"session_id",
|
|
61
|
+
"setup_encryption",
|
|
62
|
+
"store_id",
|
|
63
|
+
"study_id",
|
|
64
|
+
"token",
|
|
65
|
+
"wait_for_task_completion",
|
|
66
|
+
"whoami",
|
|
67
|
+
}
|
|
69
68
|
|
|
70
69
|
def __getattr__(self, name: str):
|
|
71
70
|
"""Handle access to missing attributes."""
|
|
72
71
|
if hasattr(self, "_missing_attributes") and name in self._missing_attributes:
|
|
73
72
|
warn(f"The attribute {name} is not available in the mock client.")
|
|
74
|
-
return
|
|
73
|
+
return
|
|
75
74
|
raise AttributeError(
|
|
76
75
|
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
77
76
|
)
|
|
@@ -113,7 +112,6 @@ class MockBaseClient:
|
|
|
113
112
|
|
|
114
113
|
def missing_method(self, method: str):
|
|
115
114
|
warn(f"The method {method} is not available in the mock client.")
|
|
116
|
-
return
|
|
117
115
|
|
|
118
116
|
def wait_for_results(self, task_id: int, interval: float = 1) -> list:
|
|
119
117
|
"""
|
|
@@ -135,7 +133,7 @@ class MockBaseClient:
|
|
|
135
133
|
return self.result.from_task(task_id)
|
|
136
134
|
|
|
137
135
|
class Study(SubClient):
|
|
138
|
-
""" """
|
|
136
|
+
"""Study subclient for the MockAlgorithmClient"""
|
|
139
137
|
|
|
140
138
|
def __init__(self, parent) -> None:
|
|
141
139
|
super().__init__(parent)
|
|
@@ -261,20 +259,20 @@ class MockBaseClient:
|
|
|
261
259
|
"sure that the method is available in the top level of your "
|
|
262
260
|
"algorithm module?"
|
|
263
261
|
)
|
|
264
|
-
exit(1)
|
|
262
|
+
sys.exit(1)
|
|
265
263
|
except SessionActionMismatchError:
|
|
266
264
|
error(
|
|
267
265
|
f"The {method} method is not a computation task, are you sure "
|
|
268
266
|
"you specified the correct method?"
|
|
269
267
|
)
|
|
270
|
-
exit(1)
|
|
268
|
+
sys.exit(1)
|
|
271
269
|
except DataFrameNotFound as e:
|
|
272
270
|
error(f"A dataframe you specified does not exist: {e}")
|
|
273
|
-
exit(1)
|
|
274
|
-
except Exception as e:
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
except Exception as e: # noqa: BLE001
|
|
275
273
|
error(f"Error simulating task run for organization {org_id}: {e}")
|
|
276
274
|
traceback.print_exc()
|
|
277
|
-
exit(1)
|
|
275
|
+
sys.exit(1)
|
|
278
276
|
|
|
279
277
|
result_response = self.parent.network.hq.save_result(result, task["id"])
|
|
280
278
|
self.parent.network.hq.save_run(
|
|
@@ -582,15 +580,15 @@ class MockUserClient(MockBaseClient):
|
|
|
582
580
|
except SessionActionMismatchError:
|
|
583
581
|
error(f"The function {method} is not a data extraction method.")
|
|
584
582
|
error("Exiting...")
|
|
585
|
-
exit(1)
|
|
586
|
-
except Exception as e:
|
|
583
|
+
sys.exit(1)
|
|
584
|
+
except Exception as e: # noqa: BLE001
|
|
587
585
|
error(
|
|
588
586
|
"Error simulating dataframe creation for organization "
|
|
589
587
|
f"{org_id}: {e}"
|
|
590
588
|
)
|
|
591
589
|
error("Exiting...")
|
|
592
590
|
traceback.print_exc()
|
|
593
|
-
exit(1)
|
|
591
|
+
sys.exit(1)
|
|
594
592
|
|
|
595
593
|
dataframes.append(df_response)
|
|
596
594
|
|
|
@@ -612,7 +610,10 @@ class MockUserClient(MockBaseClient):
|
|
|
612
610
|
def preprocess(
|
|
613
611
|
self, id_: int, image: str, method: str, arguments: dict
|
|
614
612
|
) -> dict:
|
|
615
|
-
"""
|
|
613
|
+
"""
|
|
614
|
+
Preprocess a dataframe by creating a task that runs the preprocessing
|
|
615
|
+
method.
|
|
616
|
+
"""
|
|
616
617
|
dataframe = self.parent.network.hq.get_dataframe(id_)
|
|
617
618
|
data_frame_name = dataframe.get("name")
|
|
618
619
|
if not dataframe or not data_frame_name:
|
|
@@ -635,15 +636,15 @@ class MockUserClient(MockBaseClient):
|
|
|
635
636
|
except SessionActionMismatchError:
|
|
636
637
|
error(f"The function {method} is not a preprocessing method.")
|
|
637
638
|
error("Exiting...")
|
|
638
|
-
exit(1)
|
|
639
|
-
except Exception as e:
|
|
639
|
+
sys.exit(1)
|
|
640
|
+
except Exception as e: # noqa: BLE001
|
|
640
641
|
error(
|
|
641
642
|
"Error simulating dataframe preprocessing for organization "
|
|
642
643
|
f"{org_id}: {e}"
|
|
643
644
|
)
|
|
644
645
|
error("Exiting...")
|
|
645
646
|
traceback.print_exc()
|
|
646
|
-
exit(1)
|
|
647
|
+
sys.exit(1)
|
|
647
648
|
dataframes.append(df)
|
|
648
649
|
|
|
649
650
|
result_response = self.parent.network.hq.save_result({}, task["id"])
|
|
@@ -687,7 +688,7 @@ class MockAlgorithmClient(MockBaseClient):
|
|
|
687
688
|
"parent task."
|
|
688
689
|
)
|
|
689
690
|
error("Exiting...")
|
|
690
|
-
exit(1)
|
|
691
|
+
sys.exit(1)
|
|
691
692
|
|
|
692
693
|
# inject the mock data into the arguments
|
|
693
694
|
kwargs["databases"] = self.parent.databases
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from datetime import
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
@@ -113,9 +113,9 @@ class MockHQ:
|
|
|
113
113
|
last_run_id = len(self.runs) + 1
|
|
114
114
|
run = {
|
|
115
115
|
"id": last_run_id,
|
|
116
|
-
"started_at": datetime.now(
|
|
117
|
-
"assigned_at": datetime.now(
|
|
118
|
-
"finished_at": datetime.now(
|
|
116
|
+
"started_at": datetime.now(UTC).isoformat(),
|
|
117
|
+
"assigned_at": datetime.now(UTC).isoformat(),
|
|
118
|
+
"finished_at": datetime.now(UTC).isoformat(),
|
|
119
119
|
"log": "mock_log",
|
|
120
120
|
"ports": [],
|
|
121
121
|
"status": "completed",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
1
3
|
import pandas as pd
|
|
2
4
|
|
|
3
5
|
from vantage6.common import info
|
|
@@ -148,7 +150,7 @@ class MockNetwork:
|
|
|
148
150
|
"install your algorithm package in this environment?"
|
|
149
151
|
)
|
|
150
152
|
error("Exiting...")
|
|
151
|
-
exit(1)
|
|
153
|
+
sys.exit(1)
|
|
152
154
|
|
|
153
155
|
# In the case the user provides a pandas DataFrame we need to create a dataframe
|
|
154
156
|
# record in HQ.
|
|
@@ -238,7 +238,9 @@ class MockNode:
|
|
|
238
238
|
self.dataframes[dataframe_name] = df
|
|
239
239
|
return df
|
|
240
240
|
|
|
241
|
-
def run(
|
|
241
|
+
def run(
|
|
242
|
+
self, method_fn: Callable, arguments: dict, task_env_vars: dict | None = None
|
|
243
|
+
):
|
|
242
244
|
"""
|
|
243
245
|
Run a method with the given arguments and task environment variables.
|
|
244
246
|
|
|
@@ -256,6 +258,8 @@ class MockNode:
|
|
|
256
258
|
Any
|
|
257
259
|
The result of the method run.
|
|
258
260
|
"""
|
|
261
|
+
if task_env_vars is None:
|
|
262
|
+
task_env_vars = {}
|
|
259
263
|
with env_vars(**task_env_vars):
|
|
260
264
|
return method_fn(**arguments)
|
|
261
265
|
|
|
@@ -7,13 +7,13 @@ DataFrame with statistical information based on a given grouping.
|
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
|
-
from vantage6.algorithm.tools.error_handling import
|
|
10
|
+
from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
11
11
|
from vantage6.algorithm.tools.exceptions import UserInputError
|
|
12
12
|
|
|
13
13
|
from vantage6.algorithm.decorator.action import preprocessing
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
@
|
|
16
|
+
@handle_data_errors
|
|
17
17
|
@preprocessing
|
|
18
18
|
def collapse(
|
|
19
19
|
df: pd.DataFrame,
|
|
@@ -160,7 +160,7 @@ def collapse(
|
|
|
160
160
|
return agg_df.reset_index()
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
@
|
|
163
|
+
@handle_data_errors
|
|
164
164
|
@preprocessing
|
|
165
165
|
def group_statistics(
|
|
166
166
|
df: pd.DataFrame,
|
|
@@ -6,13 +6,13 @@ expressions.
|
|
|
6
6
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
|
-
from vantage6.algorithm.tools.error_handling import
|
|
9
|
+
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
13
|
|
|
14
14
|
|
|
15
|
-
@
|
|
15
|
+
@handle_data_errors
|
|
16
16
|
@preprocessing
|
|
17
17
|
def rename_columns(
|
|
18
18
|
df: pd.DataFrame, old_names: list[str], new_names: list[str], strict: bool = False
|
|
@@ -55,7 +55,7 @@ def rename_columns(
|
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
@
|
|
58
|
+
@handle_data_errors
|
|
59
59
|
@preprocessing
|
|
60
60
|
def assign_column(df: pd.DataFrame, column_name: str, expression: str) -> pd.DataFrame:
|
|
61
61
|
"""
|
|
@@ -96,7 +96,7 @@ def assign_column(df: pd.DataFrame, column_name: str, expression: str) -> pd.Dat
|
|
|
96
96
|
return df
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
@
|
|
99
|
+
@handle_data_errors
|
|
100
100
|
@preprocessing
|
|
101
101
|
def redefine_column(
|
|
102
102
|
df: pd.DataFrame, column_name: str, expression: str
|
|
@@ -140,7 +140,7 @@ def redefine_column(
|
|
|
140
140
|
return df
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
@
|
|
143
|
+
@handle_data_errors
|
|
144
144
|
@preprocessing
|
|
145
145
|
def change_column_type(
|
|
146
146
|
df: pd.DataFrame, columns: list[str], target_type: str
|
|
@@ -5,17 +5,17 @@ for converting strings to datetime, adding fixed or variable timedeltas, and
|
|
|
5
5
|
calculating ages from birthdate columns.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import datetime
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
|
|
12
|
-
from vantage6.algorithm.tools.error_handling import
|
|
12
|
+
from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
13
13
|
from vantage6.algorithm.tools.exceptions import DataError, UserInputError
|
|
14
14
|
|
|
15
15
|
from vantage6.algorithm.decorator.action import preprocessing
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
@
|
|
18
|
+
@handle_data_errors
|
|
19
19
|
@preprocessing
|
|
20
20
|
def to_datetime(
|
|
21
21
|
df: pd.DataFrame,
|
|
@@ -81,7 +81,7 @@ def to_datetime(
|
|
|
81
81
|
return df
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
@
|
|
84
|
+
@handle_data_errors
|
|
85
85
|
@preprocessing
|
|
86
86
|
def to_timedelta(
|
|
87
87
|
df: pd.DataFrame,
|
|
@@ -179,7 +179,7 @@ def to_timedelta(
|
|
|
179
179
|
return df
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
@
|
|
182
|
+
@handle_data_errors
|
|
183
183
|
@preprocessing
|
|
184
184
|
def timedelta(
|
|
185
185
|
df: pd.DataFrame,
|
|
@@ -268,7 +268,7 @@ def timedelta(
|
|
|
268
268
|
return df
|
|
269
269
|
|
|
270
270
|
|
|
271
|
-
@
|
|
271
|
+
@handle_data_errors
|
|
272
272
|
@preprocessing
|
|
273
273
|
def calculate_age(
|
|
274
274
|
df: pd.DataFrame,
|
|
@@ -316,7 +316,7 @@ def calculate_age(
|
|
|
316
316
|
1 1980-05-15 44
|
|
317
317
|
"""
|
|
318
318
|
if reference_date is None:
|
|
319
|
-
reference_date =
|
|
319
|
+
reference_date = datetime.datetime.now(datetime.UTC).date()
|
|
320
320
|
else:
|
|
321
321
|
try:
|
|
322
322
|
reference_date = pd.to_datetime(reference_date)
|
|
@@ -9,13 +9,13 @@ standard deviations for scaling operations.
|
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
|
|
12
|
-
from vantage6.algorithm.tools.error_handling import
|
|
12
|
+
from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
13
13
|
from vantage6.algorithm.tools.exceptions import UserInputError
|
|
14
14
|
|
|
15
15
|
from vantage6.algorithm.decorator.action import preprocessing
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
@
|
|
18
|
+
@handle_data_errors
|
|
19
19
|
@preprocessing
|
|
20
20
|
def min_max_scale(
|
|
21
21
|
df: pd.DataFrame,
|
|
@@ -89,7 +89,7 @@ def min_max_scale(
|
|
|
89
89
|
return df_scaled
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
@
|
|
92
|
+
@handle_data_errors
|
|
93
93
|
@preprocessing
|
|
94
94
|
def standard_scale(
|
|
95
95
|
df: pd.DataFrame,
|
|
@@ -163,7 +163,7 @@ def standard_scale(
|
|
|
163
163
|
return df_scaled
|
|
164
164
|
|
|
165
165
|
|
|
166
|
-
@
|
|
166
|
+
@handle_data_errors
|
|
167
167
|
@preprocessing
|
|
168
168
|
def one_hot_encode(
|
|
169
169
|
df: pd.DataFrame,
|
|
@@ -251,7 +251,7 @@ def one_hot_encode(
|
|
|
251
251
|
return df_out
|
|
252
252
|
|
|
253
253
|
|
|
254
|
-
@
|
|
254
|
+
@handle_data_errors
|
|
255
255
|
@preprocessing
|
|
256
256
|
def encode(
|
|
257
257
|
df: pd.DataFrame,
|
|
@@ -335,7 +335,7 @@ def encode(
|
|
|
335
335
|
return encoded_df
|
|
336
336
|
|
|
337
337
|
|
|
338
|
-
@
|
|
338
|
+
@handle_data_errors
|
|
339
339
|
@preprocessing
|
|
340
340
|
def discretize_column(
|
|
341
341
|
df: pd.DataFrame,
|
|
@@ -401,7 +401,7 @@ def discretize_column(
|
|
|
401
401
|
return df
|
|
402
402
|
|
|
403
403
|
|
|
404
|
-
@
|
|
404
|
+
@handle_data_errors
|
|
405
405
|
@preprocessing
|
|
406
406
|
def extract_from_string(
|
|
407
407
|
df: pd.DataFrame,
|
|
@@ -457,7 +457,7 @@ def extract_from_string(
|
|
|
457
457
|
return df
|
|
458
458
|
|
|
459
459
|
|
|
460
|
-
@
|
|
460
|
+
@handle_data_errors
|
|
461
461
|
@preprocessing
|
|
462
462
|
def impute(
|
|
463
463
|
df: pd.DataFrame,
|
|
@@ -5,12 +5,12 @@ pandas DataFrames, such as selecting and filtering rows and columns.
|
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
|
-
from vantage6.algorithm.tools.error_handling import
|
|
8
|
+
from vantage6.algorithm.tools.error_handling import handle_data_errors
|
|
9
9
|
|
|
10
10
|
from vantage6.algorithm.decorator.action import preprocessing
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@handle_data_errors
|
|
14
14
|
@preprocessing
|
|
15
15
|
def select_rows(df: pd.DataFrame, query: str) -> pd.DataFrame:
|
|
16
16
|
"""
|
|
@@ -78,13 +78,13 @@ def select_rows(df: pd.DataFrame, query: str) -> pd.DataFrame:
|
|
|
78
78
|
return df.query(query)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
@
|
|
81
|
+
@handle_data_errors
|
|
82
82
|
@preprocessing
|
|
83
83
|
def filter_range(
|
|
84
84
|
df: pd.DataFrame,
|
|
85
85
|
column: str,
|
|
86
|
-
min_value: float = None,
|
|
87
|
-
max_value: float = None,
|
|
86
|
+
min_value: float | None = None,
|
|
87
|
+
max_value: float | None = None,
|
|
88
88
|
include_min: bool = True,
|
|
89
89
|
include_max: bool = True,
|
|
90
90
|
) -> pd.DataFrame:
|
|
@@ -162,7 +162,7 @@ def filter_range(
|
|
|
162
162
|
return df
|
|
163
163
|
|
|
164
164
|
|
|
165
|
-
@
|
|
165
|
+
@handle_data_errors
|
|
166
166
|
@preprocessing
|
|
167
167
|
def select_columns(df: pd.DataFrame, columns: list) -> pd.DataFrame:
|
|
168
168
|
"""
|
|
@@ -218,7 +218,7 @@ def select_columns(df: pd.DataFrame, columns: list) -> pd.DataFrame:
|
|
|
218
218
|
return df[columns]
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
@
|
|
221
|
+
@handle_data_errors
|
|
222
222
|
@preprocessing
|
|
223
223
|
def drop_columns(df: pd.DataFrame, columns: list) -> pd.DataFrame:
|
|
224
224
|
"""
|
|
@@ -266,7 +266,7 @@ def drop_columns(df: pd.DataFrame, columns: list) -> pd.DataFrame:
|
|
|
266
266
|
return df.drop(columns, axis=1)
|
|
267
267
|
|
|
268
268
|
|
|
269
|
-
@
|
|
269
|
+
@handle_data_errors
|
|
270
270
|
@preprocessing
|
|
271
271
|
def filter_by_date(
|
|
272
272
|
df: pd.DataFrame,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
from vantage6.common import logger_name
|
|
6
|
+
|
|
7
|
+
from vantage6.algorithm.tools.exceptions import AlgorithmError, AlgorithmRuntimeError
|
|
8
|
+
|
|
9
|
+
module_name = logger_name(__name__)
|
|
10
|
+
log = logging.getLogger(module_name)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_data_errors(func: Callable) -> Callable:
|
|
14
|
+
"""
|
|
15
|
+
Decorator to catch errors from data manipulation in algorithm functions and
|
|
16
|
+
prevent leaking privacy-sensitive data via error messages or tracebacks.
|
|
17
|
+
|
|
18
|
+
Any exception that is not a vantage6 ``AlgorithmError`` is replaced by a generic
|
|
19
|
+
``AlgorithmRuntimeError``. This includes non-pandas exceptions such as
|
|
20
|
+
``ValueError`` or ``KeyError``, which are commonly raised by pandas operations and
|
|
21
|
+
tend to contain data values in their message.
|
|
22
|
+
|
|
23
|
+
The vantage6 ``AlgorithmError`` exceptions are re-raised unchanged: their messages
|
|
24
|
+
are written by algorithm developers and are meant to be shown to the user.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@wraps(func)
|
|
28
|
+
def wrapper(*args, **kwargs):
|
|
29
|
+
try:
|
|
30
|
+
return func(*args, **kwargs)
|
|
31
|
+
except AlgorithmError:
|
|
32
|
+
raise
|
|
33
|
+
except Exception as e: # noqa: BLE001
|
|
34
|
+
msg = (
|
|
35
|
+
f"An error of type {type(e).__name__} occurred in "
|
|
36
|
+
f"'{func.__qualname__}'. Details have been omitted to protect privacy."
|
|
37
|
+
)
|
|
38
|
+
log.error(msg)
|
|
39
|
+
# `from None` suppresses the exception context: without it, the original
|
|
40
|
+
# exception, including its message, is still printed when the traceback
|
|
41
|
+
# is logged
|
|
42
|
+
raise AlgorithmRuntimeError(msg) from None
|
|
43
|
+
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Backwards-compatible alias
|
|
48
|
+
handle_pandas_errors = handle_data_errors
|
|
@@ -160,7 +160,7 @@ def _convert_envvar_to_int(envvar_name: str, envvar_value: str) -> int:
|
|
|
160
160
|
) from exc
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
def check_envvar_value_positive(envvar_name: str, envvar_value:
|
|
163
|
+
def check_envvar_value_positive(envvar_name: str, envvar_value: float) -> None:
|
|
164
164
|
"""
|
|
165
165
|
Check whether an environment variable is a positive integer.
|
|
166
166
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
import traceback
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
@@ -47,7 +48,7 @@ def wrap_algorithm(log_traceback: bool = True) -> None:
|
|
|
47
48
|
"No PKG_NAME specified! Make sure that the PKG_NAME environment "
|
|
48
49
|
"variable is specified in the Dockerfile. Exiting..."
|
|
49
50
|
)
|
|
50
|
-
exit(1)
|
|
51
|
+
sys.exit(1)
|
|
51
52
|
info(f"wrapper for {module}")
|
|
52
53
|
|
|
53
54
|
# Decode environment variables that are encoded by the node.
|
|
@@ -112,7 +113,7 @@ def _run_algorithm_method(
|
|
|
112
113
|
error(f"Module '{module}' can not be imported! Exiting...")
|
|
113
114
|
if log_traceback:
|
|
114
115
|
error(traceback.print_exc())
|
|
115
|
-
exit(1)
|
|
116
|
+
sys.exit(1)
|
|
116
117
|
|
|
117
118
|
# get algorithm method and attempt to load it
|
|
118
119
|
try:
|
|
@@ -121,7 +122,7 @@ def _run_algorithm_method(
|
|
|
121
122
|
error(f"Method '{method}' not found!\n")
|
|
122
123
|
if log_traceback:
|
|
123
124
|
error(traceback.print_exc())
|
|
124
|
-
exit(1)
|
|
125
|
+
sys.exit(1)
|
|
125
126
|
|
|
126
127
|
# check if the method is decorated with a vantage6 decorator. If it is not,
|
|
127
128
|
# we need to raise an error. It is important to check this, because the decorator
|
|
@@ -132,18 +133,18 @@ def _run_algorithm_method(
|
|
|
132
133
|
"algorithm functions should have a decorator such as @federated, "
|
|
133
134
|
"@central, @preprocessing, @data_extraction, etc."
|
|
134
135
|
)
|
|
135
|
-
exit(1)
|
|
136
|
+
sys.exit(1)
|
|
136
137
|
|
|
137
138
|
# try to run the method
|
|
138
139
|
if not arguments:
|
|
139
140
|
arguments = {}
|
|
140
141
|
try:
|
|
141
142
|
result = method_fn(**arguments)
|
|
142
|
-
except Exception as exc:
|
|
143
|
+
except Exception as exc: # noqa: BLE001
|
|
143
144
|
error(f"Error encountered while calling {method}: {exc}")
|
|
144
145
|
if log_traceback:
|
|
145
146
|
error(traceback.print_exc())
|
|
146
|
-
exit(1)
|
|
147
|
+
sys.exit(1)
|
|
147
148
|
|
|
148
149
|
return result
|
|
149
150
|
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from collections.abc import Callable
|
|
3
|
-
from functools import wraps
|
|
4
|
-
|
|
5
|
-
import pandas as pd
|
|
6
|
-
|
|
7
|
-
from vantage6.common import logger_name
|
|
8
|
-
|
|
9
|
-
from vantage6.algorithm.tools.exceptions import AlgorithmRuntimeError
|
|
10
|
-
|
|
11
|
-
module_name = logger_name(__name__)
|
|
12
|
-
log = logging.getLogger(module_name)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _collect_pandas_error_types() -> tuple[type, ...]:
|
|
16
|
-
"""
|
|
17
|
-
Collect all exception classes exported from pandas.errors.
|
|
18
|
-
|
|
19
|
-
Returns a tuple of exception types to be used in an except clause.
|
|
20
|
-
"""
|
|
21
|
-
exception_types = []
|
|
22
|
-
errors_module = pd.errors
|
|
23
|
-
for attr_name in errors_module.__all__:
|
|
24
|
-
attr = getattr(errors_module, attr_name, None)
|
|
25
|
-
if isinstance(attr, type) and issubclass(attr, Exception):
|
|
26
|
-
exception_types.append(attr)
|
|
27
|
-
|
|
28
|
-
return tuple(exception_types)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
PANDAS_ERROR_TYPES = _collect_pandas_error_types()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def handle_pandas_errors(func: Callable) -> Callable:
|
|
35
|
-
"""
|
|
36
|
-
Decorator to catch pandas-related errors from algorithm functions and
|
|
37
|
-
prevent leaking privacy-sensitive data via tracebacks.
|
|
38
|
-
|
|
39
|
-
- Catches pandas-specific exceptions and common data-manipulation errors.
|
|
40
|
-
- Logs a minimal, non-identifying message (no traceback, no exception str).
|
|
41
|
-
- Returns a safe, generic error response with 400 Bad Request.
|
|
42
|
-
- Other unexpected exceptions continue to log with traceback for debugging.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
@wraps(func)
|
|
46
|
-
def wrapper(*args, **kwargs):
|
|
47
|
-
try:
|
|
48
|
-
return func(*args, **kwargs)
|
|
49
|
-
except PANDAS_ERROR_TYPES as e:
|
|
50
|
-
# Avoid logging tracebacks or exception messages that may contain data
|
|
51
|
-
msg = (
|
|
52
|
-
f"Pandas-related error of type {type(e).__name__} occurred in "
|
|
53
|
-
"algorithm function. Details have been omitted to protect privacy."
|
|
54
|
-
)
|
|
55
|
-
log.error(msg)
|
|
56
|
-
# pylint: disable=raise-missing-from
|
|
57
|
-
raise AlgorithmRuntimeError(msg)
|
|
58
|
-
|
|
59
|
-
return wrapper
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/_test_serialization.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/algorithm/__init__.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/algorithm_module.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/base/__init__.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_network.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_node.py
RENAMED
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_server.py
RENAMED
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_tools-5.0.2rc2 → vantage6_algorithm_tools-5.0.3rc5}/tests/mock/test_util.py
RENAMED
|
File without changes
|
|
@@ -15,13 +15,13 @@ from vantage6.algorithm.decorator.metadata import metadata
|
|
|
15
15
|
from vantage6.algorithm.decorator.ohdsi import omop_data_extraction
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
|
+
"algorithm_client",
|
|
18
19
|
"central",
|
|
19
20
|
"data_extraction",
|
|
20
|
-
"federated",
|
|
21
|
-
"preprocessing",
|
|
22
|
-
"algorithm_client",
|
|
23
21
|
"dataframe",
|
|
24
22
|
"dataframes",
|
|
23
|
+
"federated",
|
|
25
24
|
"metadata",
|
|
26
25
|
"omop_data_extraction",
|
|
26
|
+
"preprocessing",
|
|
27
27
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|