ctp-client 1.0.1__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.
- ctp_client-1.0.1.dist-info/METADATA +45 -0
- ctp_client-1.0.1.dist-info/RECORD +18 -0
- ctp_client-1.0.1.dist-info/WHEEL +4 -0
- ctp_client-1.0.1.dist-info/entry_points.txt +2 -0
- ctp_client-1.0.1.dist-info/licenses/LICENSE +14 -0
- ctpy/__init__.py +43 -0
- ctpy/cli/__init__.py +17 -0
- ctpy/cli/cli.py +157 -0
- ctpy/client.py +265 -0
- ctpy/core/__init__.py +18 -0
- ctpy/core/pipeline.py +33 -0
- ctpy/core/server.py +50 -0
- ctpy/core/stages.py +153 -0
- ctpy/exceptions.py +45 -0
- ctpy/helpers.py +188 -0
- ctpy/tests/mock.py +32 -0
- ctpy/tests/test_init.py +18 -0
- ctpy/version.py +12 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ctp-client
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: ctp-client is a Python client for [Clinical Trials Processor](https://mircwiki.rsna.org/index.php?title=MIRC_CTP).
|
|
5
|
+
Project-URL: CI/CD, https://gitlab.com/radiology/infrastructure/data-curation-tools/ctp-client/-/pipelines
|
|
6
|
+
Project-URL: Changelog, https://ctp-client.readthedocs.io/en/latest/changelog.html
|
|
7
|
+
Project-URL: Documentation, https://ctp-client.readthedocs.io
|
|
8
|
+
Project-URL: Download, https://gitlab.com/radiology/infrastructure/data-curation-tools/ctp-client/-/archive/master/ctp-client-master.zip
|
|
9
|
+
Project-URL: Homepage, https://gitlab.com/radiology/infrastructure/data-curation-tools/ctp-client.git
|
|
10
|
+
Project-URL: Issues, https://gitlab.com/radiology/infrastructure/data-curation-tools/ctp-client/-/issues
|
|
11
|
+
Author-email: BIGR Imaging Infrastructure Group <imaging.infrastructure@erasmusmc.nl>
|
|
12
|
+
License: Apache 2.0
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Education
|
|
17
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
18
|
+
Classifier: Intended Audience :: Information Technology
|
|
19
|
+
Classifier: Intended Audience :: Science/Research
|
|
20
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
21
|
+
Classifier: Natural Language :: English
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
29
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
30
|
+
Classifier: Topic :: System :: Logging
|
|
31
|
+
Classifier: Topic :: Utilities
|
|
32
|
+
Requires-Python: >=3.9
|
|
33
|
+
Requires-Dist: beautifulsoup4==4.12.3
|
|
34
|
+
Requires-Dist: click-log==0.4.0
|
|
35
|
+
Requires-Dist: click-loglevel==0.5.1
|
|
36
|
+
Requires-Dist: click==8.1.3
|
|
37
|
+
Requires-Dist: pydantic==2.10.3
|
|
38
|
+
Requires-Dist: pyyaml==6.0.2
|
|
39
|
+
Requires-Dist: requests>=2.22.0
|
|
40
|
+
Requires-Dist: tabulate==0.9.0
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# CTPy
|
|
44
|
+
|
|
45
|
+
CTPy is a Python client for [Clinical Trials Processor](https://mircwiki.rsna.org/index.php?title=MIRC_CTP).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ctpy/__init__.py,sha256=EP3GZi8JGn1trSB9AoYk0mEaeKPepvt6y41TOKl3mCs,1446
|
|
2
|
+
ctpy/client.py,sha256=TystTq56n5U-i405s7aKg0saT4EauNDpiveM1mnyIVs,9964
|
|
3
|
+
ctpy/exceptions.py,sha256=MfOIIsysalSGq0Q5zjoFygEQT1Fxhl9_clad-k_TDAw,1144
|
|
4
|
+
ctpy/helpers.py,sha256=eU6Bapl7PZJSPmwE6bU3Bw1bGBbFsMWtEreb0d62R_Y,7390
|
|
5
|
+
ctpy/version.py,sha256=Zk7F83QsYfUU3p4V3lMkE5OwMPMEZAUNWBZQoj7REW0,455
|
|
6
|
+
ctpy/cli/__init__.py,sha256=F5erEag1v1NfNPQl2UFpuL49vmT2W4hXaTqkTCi-WsA,710
|
|
7
|
+
ctpy/cli/cli.py,sha256=RyCcXVZMwgMv2dxVgO5GCAwzfXHuZIYD-FlIWsHfGPU,7307
|
|
8
|
+
ctpy/core/__init__.py,sha256=ENrDRrf2Ur_jU1nEHhx_3P9Od4Sk6qRMjxUyxrmpyyk,782
|
|
9
|
+
ctpy/core/pipeline.py,sha256=CVWYMa6Wz6ps6Huj6d8ffxfD1qd8DbIZuoJ26YpykNg,1279
|
|
10
|
+
ctpy/core/server.py,sha256=GEpjcn25A2wGveJVQ7KsjvSysMeNbUFfu4LeVFKzFMk,1949
|
|
11
|
+
ctpy/core/stages.py,sha256=oDd3NOMBuuyrBiabJPN70m_Vp4mnz-lrPDz0xY5Quss,5865
|
|
12
|
+
ctpy/tests/mock.py,sha256=jFU2d9mM9IEtgjqtIpWouAypo3UsCM9c7hEdhKAb3DY,1189
|
|
13
|
+
ctpy/tests/test_init.py,sha256=iAc-l17rVRyhVlUxByHENGVRnZbkc_EXVDaVG5WE_I4,649
|
|
14
|
+
ctp_client-1.0.1.dist-info/METADATA,sha256=FQtuc_mv023nefnuDN1Qdc5I67YkAsCJo1tA4Q-JUxY,2228
|
|
15
|
+
ctp_client-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
ctp_client-1.0.1.dist-info/entry_points.txt,sha256=wKTrCeAZoSgYSpds8OJnxIjHki5cGIupxs6TJmmlt9s,38
|
|
17
|
+
ctp_client-1.0.1.dist-info/licenses/LICENSE,sha256=ohsbe-1180tqV0K-E81i4S5MMEHucR1lLirw3LvIpXw,665
|
|
18
|
+
ctp_client-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
ctpy/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from .import version
|
|
20
|
+
from .client import CTPYclient
|
|
21
|
+
|
|
22
|
+
__version__ = version.version
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def connect(host: str,
|
|
26
|
+
port: int,
|
|
27
|
+
username: str,
|
|
28
|
+
password: str,
|
|
29
|
+
rate_limit: Optional[int] = None) -> CTPYclient:
|
|
30
|
+
"""
|
|
31
|
+
Createa a connection to a CTP server
|
|
32
|
+
|
|
33
|
+
:param host: host to connect to
|
|
34
|
+
:param port: port to connect to
|
|
35
|
+
:param username: username to login as
|
|
36
|
+
:param password: password for user
|
|
37
|
+
:param rate_limit: optionally limit the time between requests, this is specified
|
|
38
|
+
as the maximum requests per second
|
|
39
|
+
|
|
40
|
+
:return: CTP client object
|
|
41
|
+
"""
|
|
42
|
+
ctpy_client = CTPYclient(host, port, username, password, rate_limit)
|
|
43
|
+
return ctpy_client
|
ctpy/cli/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from .cli import cli
|
ctpy/cli/cli.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import socket
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
import tabulate
|
|
23
|
+
from click_loglevel import LogLevel
|
|
24
|
+
import yaml
|
|
25
|
+
from yaml.loader import SafeLoader
|
|
26
|
+
|
|
27
|
+
from ctpy import CTPYclient
|
|
28
|
+
from ctpy import exceptions
|
|
29
|
+
from ctpy.helpers import poll_object_tracker_for_studyuid, summarize_object_tracker_content
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
@click.version_option()
|
|
33
|
+
@click.pass_context
|
|
34
|
+
def cli(ctx):
|
|
35
|
+
ctx.ensure_object(dict)
|
|
36
|
+
|
|
37
|
+
@cli.command()
|
|
38
|
+
@click.option('--lookup-table-filename', type=Path, required=True)
|
|
39
|
+
@click.option('--ctp-host', required=True)
|
|
40
|
+
@click.option('--ctp-port', type=int, default=1180, required=True)
|
|
41
|
+
@click.option('--dicom-anonymizer-id', required=True)
|
|
42
|
+
@click.option('--username', required=True)
|
|
43
|
+
@click.option('--password', required=True, prompt=True, hide_input=True)
|
|
44
|
+
@click.option('--log-level', type=LogLevel(), default=logging.INFO)
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def post_lookup_table(_, lookup_table_filename, ctp_host, ctp_port,
|
|
47
|
+
dicom_anonymizer_id, username, password, log_level):
|
|
48
|
+
logging.basicConfig(
|
|
49
|
+
format=f"%(asctime)s - {socket.gethostname()} - [%(levelname)-8s] %(message)s",
|
|
50
|
+
level=log_level,
|
|
51
|
+
)
|
|
52
|
+
ctpy_client = CTPYclient(ctp_host, ctp_port, username, password, rate_limit=0.2)
|
|
53
|
+
with open(lookup_table_filename, encoding='utf-8') as lookup_table_file:
|
|
54
|
+
lut = yaml.load(lookup_table_file, Loader=SafeLoader)
|
|
55
|
+
for key, value in lut.items():
|
|
56
|
+
try:
|
|
57
|
+
ctpy_client.add_to_lookup(dicom_anonymizer_id, key, value)
|
|
58
|
+
except exceptions.CTPYFailedAddingLUT:
|
|
59
|
+
pass
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
@cli.command()
|
|
63
|
+
@click.option('--ctp-host', required=True)
|
|
64
|
+
@click.option('--ctp-port', type=int, default=1180, required=True)
|
|
65
|
+
@click.option('--pipeline-id', type=int, default=0, required=True)
|
|
66
|
+
@click.option('--object-tracker-id', type=int, default=1, required=True)
|
|
67
|
+
@click.option('--username', required=True, default="king")
|
|
68
|
+
@click.option('--password', required=True, default="password", prompt=True, hide_input=True)
|
|
69
|
+
@click.option('--patient-id', required=True)
|
|
70
|
+
@click.option('--study-uid', 'study_uid_filter')
|
|
71
|
+
@click.option('--delay', type=int, default=5)
|
|
72
|
+
@click.option('--timeout', type=int, default=900)
|
|
73
|
+
@click.option('--log-level', type=LogLevel(), default=logging.INFO)
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def check_object_tracker(_, ctp_host, ctp_port, pipeline_id, object_tracker_id,
|
|
76
|
+
username, password, patient_id, study_uid_filter, delay,
|
|
77
|
+
timeout, log_level):
|
|
78
|
+
logging.basicConfig(
|
|
79
|
+
format=f"%(asctime)s - {socket.gethostname()} - [%(levelname)-8s] %(message)s",
|
|
80
|
+
level=log_level,
|
|
81
|
+
)
|
|
82
|
+
ctpy_client = CTPYclient(ctp_host, ctp_port, username, password, rate_limit=0.2)
|
|
83
|
+
object_tracker_data = poll_object_tracker_for_studyuid(ctpy_client,
|
|
84
|
+
pipeline_id,
|
|
85
|
+
object_tracker_id,
|
|
86
|
+
patient_id,
|
|
87
|
+
study_uid_filter,
|
|
88
|
+
delay,
|
|
89
|
+
timeout)
|
|
90
|
+
logging.debug('patient_data: %s', object_tracker_data)
|
|
91
|
+
if object_tracker_data and patient_id in object_tracker_data:
|
|
92
|
+
for study_uid, series_info in object_tracker_data[patient_id].items():
|
|
93
|
+
for series_uid, instances in series_info.items():
|
|
94
|
+
logging.info('%s|%s|%s', study_uid, series_uid, instances)
|
|
95
|
+
|
|
96
|
+
@cli.command()
|
|
97
|
+
@click.option('--ctp-host', required=True)
|
|
98
|
+
@click.option('--ctp-port', type=int, default=1180, required=True)
|
|
99
|
+
@click.option('--pipeline-id', type=int, default=0, required=True)
|
|
100
|
+
@click.option('--idmap-id', type=int, default=1, required=True)
|
|
101
|
+
@click.option('--username', required=True, default="king")
|
|
102
|
+
@click.option('--password', required=True, default="password", prompt=True, hide_input=True)
|
|
103
|
+
@click.option('--keytype', required=True)
|
|
104
|
+
@click.option('--key', required=True)
|
|
105
|
+
@click.option('--log-level', type=LogLevel(), default=logging.INFO)
|
|
106
|
+
@click.pass_context
|
|
107
|
+
def query_idmap(_, ctp_host, ctp_port, pipeline_id, idmap_id, keytype, key,
|
|
108
|
+
username, password, log_level):
|
|
109
|
+
logging.basicConfig(
|
|
110
|
+
format=f"%(asctime)s - {socket.gethostname()} - [%(levelname)-8s] %(message)s",
|
|
111
|
+
level=log_level,
|
|
112
|
+
)
|
|
113
|
+
ctpy_client = CTPYclient(ctp_host, ctp_port, username, password, rate_limit=0.2)
|
|
114
|
+
study_uid = ctpy_client.idmap_search(pipeline_id, idmap_id, key, keytype)
|
|
115
|
+
if study_uid:
|
|
116
|
+
logging.info(study_uid)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@cli.command()
|
|
120
|
+
@click.option('--ctp-host', required=True)
|
|
121
|
+
@click.option('--ctp-port', type=int, default=1180, required=True)
|
|
122
|
+
@click.option('--username', required=False)
|
|
123
|
+
@click.option('--password', required=False)
|
|
124
|
+
def get_summary(ctp_host, ctp_port, username, password):
|
|
125
|
+
logging.basicConfig(
|
|
126
|
+
format=f"%(asctime)s - {socket.gethostname()} - [%(levelname)-8s] %(message)s",
|
|
127
|
+
level='INFO',
|
|
128
|
+
)
|
|
129
|
+
ctpy_client = CTPYclient(ctp_host, ctp_port, username, password, rate_limit=0.2)
|
|
130
|
+
summary = ctpy_client.get_ctp_summary()
|
|
131
|
+
logging.info(f"\n{tabulate.tabulate([s.to_table_row() for s in summary], headers=['pipeline', 'import', 'export', 'quarantines'])}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@cli.command()
|
|
135
|
+
@click.option('--ctp-host', required=True)
|
|
136
|
+
@click.option('--ctp-port', type=int, default=1180, required=True)
|
|
137
|
+
@click.option('--username', required=True)
|
|
138
|
+
@click.option('--password', required=True)
|
|
139
|
+
@click.option('--pipeline-name', required=True)
|
|
140
|
+
def get_tracker_summary(ctp_host, ctp_port, username, password, pipeline_name):
|
|
141
|
+
logging.basicConfig(
|
|
142
|
+
format=f"%(asctime)s - {socket.gethostname()} - [%(levelname)-8s] %(message)s",
|
|
143
|
+
level='INFO',
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
ctpy_client = CTPYclient(ctp_host, ctp_port, username, password, rate_limit=0.2)
|
|
147
|
+
pipeline = ctpy_client.server.pipelines.get(pipeline_name)
|
|
148
|
+
if pipeline is None:
|
|
149
|
+
raise exceptions.CTPYPipelineNotFound(f"Pipeline {pipeline_name} not found!")
|
|
150
|
+
|
|
151
|
+
trackers_summary = list()
|
|
152
|
+
object_trackers = ctpy_client.find_object_tracker_stages(pipeline)
|
|
153
|
+
for tracker_id, object_tracker in object_trackers:
|
|
154
|
+
object_tracker_contents = ctpy_client.get_object_tracker_patient_info(pipeline_id=pipeline.pipeline_id, stage_id=tracker_id)
|
|
155
|
+
|
|
156
|
+
trackers_summary.append([object_tracker.name, *summarize_object_tracker_content(object_tracker_contents)])
|
|
157
|
+
logging.info(f"\n{tabulate.tabulate(trackers_summary, headers=['name', 'subjects', 'experiments', 'scans', 'instances'])}")
|
ctpy/client.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Copyright 2022 Biomedical Imaging Group Rotterdam, Department of
|
|
2
|
+
# Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import re
|
|
19
|
+
from time import time, sleep
|
|
20
|
+
from typing import Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
from requests.exceptions import ConnectionError as RequestsConnectionError
|
|
24
|
+
|
|
25
|
+
from ctpy import exceptions
|
|
26
|
+
from ctpy.core import CTPServer
|
|
27
|
+
from ctpy.core.stages import ObjectTracker
|
|
28
|
+
from ctpy.helpers import parse_idmap_results, parse_object_tracker_results, parse_summary, parse_step, CtpSummary
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CTPYclient():
|
|
32
|
+
|
|
33
|
+
def __init__(self, host: str, port: int, username: str, password: str, rate_limit: Optional[int] = None):
|
|
34
|
+
self.host: str = host
|
|
35
|
+
self.port: int = port
|
|
36
|
+
self.headers: Dict[str, str] = {}
|
|
37
|
+
|
|
38
|
+
if username is None:
|
|
39
|
+
logging.debug(f'No username information')
|
|
40
|
+
self.session: requests.Session = requests.Session()
|
|
41
|
+
else:
|
|
42
|
+
logging.debug(f'Log in as user: {username}')
|
|
43
|
+
self.session: requests.Session = self._login(username, password)
|
|
44
|
+
|
|
45
|
+
self.rate_limit: int = rate_limit
|
|
46
|
+
self.server: CTPServer = CTPServer(self)
|
|
47
|
+
|
|
48
|
+
def _login(self, username: str, password: str) -> requests.Session:
|
|
49
|
+
cookies = {
|
|
50
|
+
'CTP': 'session',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
headers = {
|
|
54
|
+
'Connection': 'keep-alive',
|
|
55
|
+
'Referer': f'{self.host}:{self.port}/',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
timestamp = int(time())
|
|
59
|
+
query = f'username={username}&password={password}&url=/×tamp={timestamp}'
|
|
60
|
+
login_uri = f'{self.host}:{self.port}/login?{query}'
|
|
61
|
+
logging.debug(f'Logging in using url: {login_uri}')
|
|
62
|
+
try:
|
|
63
|
+
response = requests.get(login_uri, cookies=cookies,
|
|
64
|
+
headers=headers, allow_redirects=False)
|
|
65
|
+
logging.debug(f'got response: {response}')
|
|
66
|
+
except RequestsConnectionError as esc:
|
|
67
|
+
raise exceptions.CTPYLoginException(esc)
|
|
68
|
+
|
|
69
|
+
if response.status_code == 302 and 'RSNASESSION' in response.cookies:
|
|
70
|
+
logging.debug('Succesfully connected to CTP')
|
|
71
|
+
session = requests.Session()
|
|
72
|
+
session.cookies.update({
|
|
73
|
+
'RSNASESSION': response.cookies['RSNASESSION']
|
|
74
|
+
})
|
|
75
|
+
else:
|
|
76
|
+
raise exceptions.CTPYLoginException
|
|
77
|
+
return session
|
|
78
|
+
|
|
79
|
+
def _rate_limit(self):
|
|
80
|
+
if self.rate_limit:
|
|
81
|
+
sleep(1 / self.rate_limit)
|
|
82
|
+
|
|
83
|
+
def format_uri(self, path: str, query: Optional[str] = None) -> str:
|
|
84
|
+
if path and path[0] != '/':
|
|
85
|
+
path = '/' + path
|
|
86
|
+
if query and query[0] != '?':
|
|
87
|
+
query = '?' + query
|
|
88
|
+
|
|
89
|
+
if not query:
|
|
90
|
+
query = ''
|
|
91
|
+
if not path:
|
|
92
|
+
path = ''
|
|
93
|
+
|
|
94
|
+
return f'{self.host}:{self.port}{path}{query}'
|
|
95
|
+
|
|
96
|
+
def get(self,
|
|
97
|
+
path: str,
|
|
98
|
+
query: Optional[str] = None,
|
|
99
|
+
timeout: Optional[float] = None,
|
|
100
|
+
headers: Dict[str, str] = None) -> requests.Response:
|
|
101
|
+
"""
|
|
102
|
+
Process a REST GET to the server
|
|
103
|
+
|
|
104
|
+
:param path: path for the request
|
|
105
|
+
:param query: query string to use for the request
|
|
106
|
+
:param timeout: timeout
|
|
107
|
+
:param headers: request headers
|
|
108
|
+
:return: requests response
|
|
109
|
+
"""
|
|
110
|
+
uri = self.format_uri(path, query)
|
|
111
|
+
logging.debug('GET: %s', uri)
|
|
112
|
+
self._rate_limit()
|
|
113
|
+
try:
|
|
114
|
+
response = self.session.get(uri, timeout=timeout, headers=headers)
|
|
115
|
+
except RequestsConnectionError as esc:
|
|
116
|
+
raise exceptions.CTPYConnectionAborted(esc)
|
|
117
|
+
return response
|
|
118
|
+
|
|
119
|
+
def put(self,
|
|
120
|
+
path: str,
|
|
121
|
+
query: Optional[str] = None,
|
|
122
|
+
timeout: Optional[float] = None,
|
|
123
|
+
headers: Dict[str, str] = None) -> requests.Response:
|
|
124
|
+
uri = self.format_uri(path, query)
|
|
125
|
+
logging.debug('PUT: %s', uri)
|
|
126
|
+
self._rate_limit()
|
|
127
|
+
try:
|
|
128
|
+
response = self.session.put(uri, timeout=timeout, headers=headers)
|
|
129
|
+
except RequestsConnectionError as esc:
|
|
130
|
+
raise exceptions.CTPYConnectionAborted(esc)
|
|
131
|
+
return response
|
|
132
|
+
|
|
133
|
+
def post(self,
|
|
134
|
+
path: str,
|
|
135
|
+
data,
|
|
136
|
+
query: Optional[str] = None,
|
|
137
|
+
timeout: Optional[float] = None,
|
|
138
|
+
headers: Dict[str, str] = None) -> requests.Response:
|
|
139
|
+
uri = self.format_uri(path, query)
|
|
140
|
+
logging.debug('POST: %s', uri)
|
|
141
|
+
self._rate_limit()
|
|
142
|
+
try:
|
|
143
|
+
response = self.session.post(uri, data=data, timeout=timeout, headers=headers)
|
|
144
|
+
except RequestsConnectionError as esc:
|
|
145
|
+
raise exceptions.CTPYConnectionAborted(esc)
|
|
146
|
+
return response
|
|
147
|
+
|
|
148
|
+
def get_ctp_summary(self) -> List[CtpSummary]:
|
|
149
|
+
response = self.get('/summary?suppress')
|
|
150
|
+
if response.status_code == 200:
|
|
151
|
+
logging.debug('Succesfully got html')
|
|
152
|
+
elif response.status_code == 403:
|
|
153
|
+
logging.error('Forbidden')
|
|
154
|
+
raise exceptions.CTPYAccessException
|
|
155
|
+
else:
|
|
156
|
+
raise exceptions.CTPYGeneralError
|
|
157
|
+
|
|
158
|
+
results = parse_summary(response.content)
|
|
159
|
+
return results
|
|
160
|
+
|
|
161
|
+
def get_ctp_step(self, pipeline: int, step: int) -> Dict[str, Dict[str, str]]:
|
|
162
|
+
response = self.get(f'/summary?p={pipeline}&s={step}&suppress')
|
|
163
|
+
print(f"{response.status_code}=")
|
|
164
|
+
|
|
165
|
+
if response.status_code == 200:
|
|
166
|
+
logging.debug('Succesfully got html')
|
|
167
|
+
elif response.status_code == 403:
|
|
168
|
+
logging.error('Forbidden')
|
|
169
|
+
raise exceptions.CTPYAccessException
|
|
170
|
+
else:
|
|
171
|
+
raise exceptions.CTPYGeneralError
|
|
172
|
+
|
|
173
|
+
if "<title>ERROR</title>" in response.text:
|
|
174
|
+
match = re.search("java.lang.IndexOutOfBoundsException: Index (\d+) out of bounds for length (\d+)", response.text)
|
|
175
|
+
|
|
176
|
+
if match:
|
|
177
|
+
index = match.group(1)
|
|
178
|
+
length = match.group(2)
|
|
179
|
+
|
|
180
|
+
raise IndexError(f"Index out of range {index=} {length=}")
|
|
181
|
+
else:
|
|
182
|
+
raise exceptions.CTPYGeneralError
|
|
183
|
+
|
|
184
|
+
results = parse_step(response.content)
|
|
185
|
+
return results
|
|
186
|
+
|
|
187
|
+
def add_to_lookup(self, stage_id, key, value):
|
|
188
|
+
query = f'id={stage_id}&key={key}&value={value}'
|
|
189
|
+
response = self.put('/lookup', query)
|
|
190
|
+
if response.status_code == 200:
|
|
191
|
+
logging.info('Succesfully added Key/Value')
|
|
192
|
+
elif response.status_code == 304:
|
|
193
|
+
logging.warning('Did not modified key/value')
|
|
194
|
+
raise exceptions.CTPYFailedAddingLUT
|
|
195
|
+
elif response.status_code == 403:
|
|
196
|
+
logging.error('Forbidden')
|
|
197
|
+
raise exceptions.CTPYAccessException
|
|
198
|
+
else:
|
|
199
|
+
raise exceptions.CTPYGeneralError
|
|
200
|
+
# logging.debug(response.content)
|
|
201
|
+
|
|
202
|
+
def get_object_tracker_patient_info(self, pipeline_id, stage_id, key='', patient_id=None, study_uid_filter=None):
|
|
203
|
+
# headers = self.headers.copy()
|
|
204
|
+
headers = {}
|
|
205
|
+
headers['Referer'] = f'{self.host}:{self.port}/objecttracker'
|
|
206
|
+
data = {
|
|
207
|
+
'p': str(pipeline_id),
|
|
208
|
+
's': str(stage_id),
|
|
209
|
+
'suppress': '',
|
|
210
|
+
'keytype': 'patient',
|
|
211
|
+
'keys': key,
|
|
212
|
+
'format': 'csv'
|
|
213
|
+
}
|
|
214
|
+
response = self.post('objecttracker',
|
|
215
|
+
headers=headers,
|
|
216
|
+
data=data)
|
|
217
|
+
if response.status_code != 200:
|
|
218
|
+
logging.debug(response.text)
|
|
219
|
+
return None
|
|
220
|
+
logging.debug(f'Results: {response.text}')
|
|
221
|
+
|
|
222
|
+
object_tracker_dict = parse_object_tracker_results(response.text, patient_id, study_uid_filter)
|
|
223
|
+
return object_tracker_dict
|
|
224
|
+
|
|
225
|
+
def idmap_search(self, pipeline_id, stage_id, key, keytype):
|
|
226
|
+
headers = self.headers.copy()
|
|
227
|
+
headers['Referer'] = f'{self.host}:{self.port}/idmap'
|
|
228
|
+
data ={
|
|
229
|
+
"p": str(pipeline_id),
|
|
230
|
+
"s": str(stage_id),
|
|
231
|
+
"suppress": "",
|
|
232
|
+
"keytype": keytype,
|
|
233
|
+
"keys": str(key),
|
|
234
|
+
"format": "csv"
|
|
235
|
+
}
|
|
236
|
+
logging.debug(f'Querying ID-map at {pipeline_id};{stage_id}')
|
|
237
|
+
response = self.session.post(f'{self.host}:{self.port}/idmap',
|
|
238
|
+
headers=headers,
|
|
239
|
+
data=data)
|
|
240
|
+
if response.status_code != 200:
|
|
241
|
+
logging.debug(f'Got {response.status_code}')
|
|
242
|
+
logging.debug(response.text)
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
new_id = parse_idmap_results(response.text)
|
|
246
|
+
return new_id
|
|
247
|
+
|
|
248
|
+
def idmap_search_uid(self, pipeline_id, stage_id, key):
|
|
249
|
+
return self.idmap_search(pipeline_id, stage_id, key, keytype="originalUID")
|
|
250
|
+
|
|
251
|
+
def idmap_reverse_search_uid(self, pipeline_id, stage_id, key):
|
|
252
|
+
return self.idmap_search(pipeline_id, stage_id, key, keytype="trialUID")
|
|
253
|
+
|
|
254
|
+
def idmap_search_patientid(self, pipeline_id, stage_id, key):
|
|
255
|
+
return self.idmap_search(pipeline_id, stage_id, key, keytype="originalPtID")
|
|
256
|
+
|
|
257
|
+
def idmap_reverse_search_patientid(self, pipeline_id, stage_id, key):
|
|
258
|
+
return self.idmap_search(pipeline_id, stage_id, key, keytype="trialPtID")
|
|
259
|
+
|
|
260
|
+
def find_object_tracker_stages(self, pipeline):
|
|
261
|
+
object_trackers = [stage for stage in pipeline.stages.items() if isinstance(stage[1], ObjectTracker)]
|
|
262
|
+
if object_trackers:
|
|
263
|
+
return object_trackers
|
|
264
|
+
else:
|
|
265
|
+
raise exceptions.CTPYNoObjectTrackerFound(f"No ObjectTracker stages found in pipeline {pipeline.pipeline_id}!")
|
ctpy/core/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from .pipeline import Pipeline
|
|
17
|
+
from .server import CTPServer
|
|
18
|
+
from .stages import StageFactory
|
ctpy/core/pipeline.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from ctpy.core.stages import StageFactory
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Pipeline():
|
|
23
|
+
def __init__(self, server, pipeline_id, pipeline):
|
|
24
|
+
self.server = server
|
|
25
|
+
self.pipeline_id = pipeline_id
|
|
26
|
+
self.stages = {}
|
|
27
|
+
for stage_id, stage in enumerate(pipeline):
|
|
28
|
+
logging.debug('Create stage:<%s> <%s>', stage_id, stage)
|
|
29
|
+
ctp_stage = StageFactory.create_stage(self, stage_id, stage.attrib)
|
|
30
|
+
if id in stage.attrib:
|
|
31
|
+
self.stages[stage.attrib['id']] = ctp_stage
|
|
32
|
+
else:
|
|
33
|
+
self.stages[stage_id] = ctp_stage
|
ctpy/core/server.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import xml.etree.ElementTree as ET
|
|
18
|
+
from xml.etree.ElementTree import ParseError
|
|
19
|
+
|
|
20
|
+
from ctpy import exceptions
|
|
21
|
+
from ctpy.core import Pipeline
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CTPServer():
|
|
25
|
+
def __init__(self, ctpy_session):
|
|
26
|
+
self.ctpy_session = ctpy_session
|
|
27
|
+
response = self.ctpy_session.get('server')
|
|
28
|
+
if response.status_code != 200:
|
|
29
|
+
raise exceptions.CTPYServerXMLParseError()
|
|
30
|
+
xml_string = response.text
|
|
31
|
+
try:
|
|
32
|
+
server = ET.fromstring(xml_string)
|
|
33
|
+
except ParseError as exc:
|
|
34
|
+
raise exceptions.CTPYServerXMLParseError(exc)
|
|
35
|
+
|
|
36
|
+
# {'build': '2021.04.01 at 11:04:40 CDT', 'ip': '10.42.0.83', 'java': '1.7', 'port': '1080'}
|
|
37
|
+
self.build = server.attrib['build']
|
|
38
|
+
self.ip = server.attrib['ip']
|
|
39
|
+
self.java = server.attrib['java']
|
|
40
|
+
self.port = server.attrib['port']
|
|
41
|
+
self.pipelines = {}
|
|
42
|
+
|
|
43
|
+
for configuration in server:
|
|
44
|
+
if configuration.tag != 'Configuration':
|
|
45
|
+
raise exceptions.CTPYServerXMLParseError
|
|
46
|
+
pipeline_id = 0
|
|
47
|
+
for child in configuration:
|
|
48
|
+
if child.tag == 'Pipeline':
|
|
49
|
+
self.pipelines[child.attrib['name']] = Pipeline(self, pipeline_id, child)
|
|
50
|
+
pipeline_id += 1
|
ctpy/core/stages.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from ctpy import exceptions
|
|
20
|
+
from ctpy.helpers import parse_idmap_results, parse_object_tracker_results
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StageFactory:
|
|
24
|
+
@classmethod
|
|
25
|
+
def create_stage(cls, pipeline, stage_id, stage_config):
|
|
26
|
+
stage_class = stage_config['class'].split('.')[-1]
|
|
27
|
+
|
|
28
|
+
sanitized_config = cls._sanitize_config(stage_config)
|
|
29
|
+
logging.debug('Creating stage: %s', stage_class)
|
|
30
|
+
logging.debug('Creating stage: %s', sanitized_config)
|
|
31
|
+
|
|
32
|
+
if stage_class == 'IDMap':
|
|
33
|
+
return IDMap(pipeline, stage_id, **sanitized_config)
|
|
34
|
+
if stage_class == 'DicomAnonymizer':
|
|
35
|
+
return DicomAnonymizer(pipeline, stage_id, **sanitized_config)
|
|
36
|
+
if stage_class == 'DicomFilter':
|
|
37
|
+
return DicomFilter(pipeline, stage_id, **sanitized_config)
|
|
38
|
+
if stage_class == 'DicomPixelAnonymizer':
|
|
39
|
+
return DicomPixelAnonymizer(pipeline, stage_id, **sanitized_config)
|
|
40
|
+
if stage_class == 'ObjectTracker':
|
|
41
|
+
return ObjectTracker(pipeline, stage_id, **sanitized_config)
|
|
42
|
+
|
|
43
|
+
return BaseStage(pipeline, stage_id, **sanitized_config)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def _sanitize_config(cls, stage_config):
|
|
47
|
+
sanitized_config = {}
|
|
48
|
+
if 'class' not in stage_config or 'root' not in stage_config:
|
|
49
|
+
raise exceptions.CTPYStageParseError
|
|
50
|
+
sanitized_config['stage_class'] = stage_config['class']
|
|
51
|
+
sanitized_config['root'] = stage_config['root']
|
|
52
|
+
|
|
53
|
+
accepted_keys = ['id', 'root', 'quarantine', 'lookup_table', 'script', 'name', 'script']
|
|
54
|
+
|
|
55
|
+
for key in accepted_keys:
|
|
56
|
+
if key in stage_config:
|
|
57
|
+
sanitized_config[key] = stage_config[key]
|
|
58
|
+
return sanitized_config
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BaseStage():
|
|
62
|
+
def __init__(self, pipeline, stage_id, stage_class, root, id=None, quarantine=None, name=None):
|
|
63
|
+
self.id = id
|
|
64
|
+
self.stage_id = stage_id
|
|
65
|
+
self.pipeline = pipeline
|
|
66
|
+
self.stage_class = stage_class
|
|
67
|
+
self.root = root
|
|
68
|
+
self.quarantine = quarantine
|
|
69
|
+
self.name = name
|
|
70
|
+
|
|
71
|
+
def __str__(self):
|
|
72
|
+
return f'This Stage is of type: {type(self).__name__}'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ObjectTracker(BaseStage):
|
|
76
|
+
def query_patientids(self, key, study_uid_filter=None):
|
|
77
|
+
headers = {}
|
|
78
|
+
headers['Referer'] = self.pipeline.server.ctpy_session.format_uri('/objecttracker')
|
|
79
|
+
data = {
|
|
80
|
+
'p': str(self.pipeline.pipeline_id),
|
|
81
|
+
's': str(self.id),
|
|
82
|
+
'suppress': '',
|
|
83
|
+
'keytype': 'patient',
|
|
84
|
+
'keys': key,
|
|
85
|
+
'format': 'csv'
|
|
86
|
+
}
|
|
87
|
+
response = self.pipeline.server.ctpy_session.post('objecttracker',
|
|
88
|
+
headers=headers,
|
|
89
|
+
data=data)
|
|
90
|
+
if response.status_code != 200:
|
|
91
|
+
logging.debug(response.text)
|
|
92
|
+
return None
|
|
93
|
+
object_tracker_dict = parse_object_tracker_results(response.text, study_uid_filter)
|
|
94
|
+
return object_tracker_dict
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class IDMap(BaseStage):
|
|
98
|
+
def search(self, key, keytype):
|
|
99
|
+
headers = {}
|
|
100
|
+
headers['Referer'] = self.pipeline.server.ctpy_session.format_uri('/idmap')
|
|
101
|
+
data ={
|
|
102
|
+
'p': str(self.pipeline.pipeline_id),
|
|
103
|
+
's': str(self.id),
|
|
104
|
+
"suppress": "",
|
|
105
|
+
"keytype": keytype,
|
|
106
|
+
"keys": str(key),
|
|
107
|
+
"format": "csv"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
response = self.pipeline.server.ctpy_session.post('idmap',
|
|
111
|
+
headers=headers,
|
|
112
|
+
data=data)
|
|
113
|
+
if response.status_code != 200:
|
|
114
|
+
logging.debug(response.text)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
new_id = parse_idmap_results(response.text)
|
|
118
|
+
return new_id
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DicomFilter(BaseStage):
|
|
122
|
+
def __init__(self, pipeline, stage_id, stage_class, root,
|
|
123
|
+
id=None, name=None, quarantine=None, script=None):
|
|
124
|
+
super().__init__(pipeline, stage_class, root, name, quarantine=quarantine, name=name, id=id)
|
|
125
|
+
self.script = script
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DicomAnonymizer(BaseStage):
|
|
129
|
+
def __init__(self, pipeline, stage_id, stage_class, root, id,
|
|
130
|
+
name=None, quarantine=None, script=None, lookup_table=None):
|
|
131
|
+
super().__init__(pipeline, stage_id, stage_class, root, quarantine=quarantine, name=name, id=id)
|
|
132
|
+
self.lookup_table = lookup_table
|
|
133
|
+
self.script = script
|
|
134
|
+
|
|
135
|
+
def add_key(self, key, value):
|
|
136
|
+
query = f'id={self.id}&key={key}&value={value}'
|
|
137
|
+
response = self.pipeline.server.ctpy_session.put('/lookup', query)
|
|
138
|
+
if response.status_code == 200:
|
|
139
|
+
logging.info('Succesfully added Key/Value')
|
|
140
|
+
elif response.status_code == 304:
|
|
141
|
+
logging.warning('Did not modified key/value')
|
|
142
|
+
raise exceptions.CTPYFailedAddingLUT
|
|
143
|
+
elif response.status_code == 403:
|
|
144
|
+
logging.error('Forbidden')
|
|
145
|
+
raise exceptions.CTPYAccessException
|
|
146
|
+
else:
|
|
147
|
+
raise exceptions.CTPYGeneralError
|
|
148
|
+
|
|
149
|
+
class DicomPixelAnonymizer(BaseStage):
|
|
150
|
+
def __init__(self, pipeline, stage_id, stage_class,
|
|
151
|
+
name=None, root=None, quarantine=None, script=None):
|
|
152
|
+
super().__init__(pipeline, stage_class, root, name)
|
|
153
|
+
self.script = script
|
ctpy/exceptions.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright 2022 Biomedical Imaging Group Rotterdam, Department of
|
|
2
|
+
# Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CTPYFailedAddingLUT(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class CTPYNoObjectTrackerFound(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
class CTPYPipelineNotFound(Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
class CTPYLoginException(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
class CTPYSSLError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
class CTPYAccessException(Exception):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
class CTPYServerXMLParseError(Exception):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
class CTPYConnectionAborted(Exception):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
class CTPYStageParseError(Exception):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
class CTPYGeneralError(Exception):
|
|
45
|
+
pass
|
ctpy/helpers.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Copyright 2017 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from time import sleep, time
|
|
17
|
+
import logging
|
|
18
|
+
import datetime
|
|
19
|
+
from typing import Dict, List, Optional, Union
|
|
20
|
+
|
|
21
|
+
import bs4
|
|
22
|
+
from pydantic import BaseModel, Field, AliasChoices, field_validator
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_object_tracker_results(object_tracker_string: str,
|
|
26
|
+
patient_id_filter: Optional[str] = None,
|
|
27
|
+
study_uid_filter: Optional[str] = None) -> Union[None, Dict[str, Dict[str, Dict[str, str]]]]:
|
|
28
|
+
try:
|
|
29
|
+
data = object_tracker_string.split()[1:]
|
|
30
|
+
except IndexError:
|
|
31
|
+
return None
|
|
32
|
+
object_tracker_dict = {}
|
|
33
|
+
for row in data:
|
|
34
|
+
row = row.split(',')
|
|
35
|
+
patient_id = row[0]
|
|
36
|
+
study_uid = row[1]
|
|
37
|
+
series_uid = row[2]
|
|
38
|
+
instances = int(row[3])
|
|
39
|
+
if patient_id_filter and not patient_id == patient_id_filter:
|
|
40
|
+
logging.debug('Filtering Patient: %s not equal: %s', patient_id, patient_id_filter)
|
|
41
|
+
continue
|
|
42
|
+
if study_uid_filter and not study_uid == study_uid_filter:
|
|
43
|
+
logging.debug('Filtering StudyUID: %s', study_uid, study_uid_filter)
|
|
44
|
+
continue
|
|
45
|
+
if patient_id in object_tracker_dict:
|
|
46
|
+
if study_uid in object_tracker_dict[patient_id]:
|
|
47
|
+
if series_uid in object_tracker_dict[patient_id][study_uid]:
|
|
48
|
+
object_tracker_dict[patient_id][study_uid][series_uid] = instances
|
|
49
|
+
logging.debug('Update %s to object_tracker_dict', instances)
|
|
50
|
+
else:
|
|
51
|
+
object_tracker_dict[patient_id][study_uid][series_uid] = instances
|
|
52
|
+
logging.debug('Add %s to object_tracker_dict', instances)
|
|
53
|
+
else:
|
|
54
|
+
object_tracker_dict[patient_id][study_uid] = {series_uid: instances}
|
|
55
|
+
logging.debug('Add %s|%s;%s to object_tracker_dict',
|
|
56
|
+
study_uid, series_uid, instances)
|
|
57
|
+
else:
|
|
58
|
+
logging.debug('Add %s|%s|%s;%s to object_tracker_dict',
|
|
59
|
+
patient_id, study_uid, series_uid, instances)
|
|
60
|
+
object_tracker_dict[patient_id] = {study_uid: {series_uid: instances}}
|
|
61
|
+
return object_tracker_dict
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_idmap_results(idmap_string: str) -> Union[None, str]:
|
|
65
|
+
logging.debug(f'idmap: {idmap_string}')
|
|
66
|
+
lines = idmap_string.split('\n')
|
|
67
|
+
_, new = lines[1].split(',')
|
|
68
|
+
|
|
69
|
+
if new.startswith('=("'):
|
|
70
|
+
trial_uid = new[3:-2]
|
|
71
|
+
return trial_uid
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def poll_object_tracker_for_studyuid(ctpy_session, pipeline_id,
|
|
76
|
+
object_tracker_id, patient_id,
|
|
77
|
+
study_uid, delay, timeout):
|
|
78
|
+
old_data = object_tracker_data = {}
|
|
79
|
+
last_update = int(time())
|
|
80
|
+
logging.debug(f'Querying for patient:{patient_id}, study_uid: {study_uid}')
|
|
81
|
+
while old_data == object_tracker_data:
|
|
82
|
+
object_tracker_data = ctpy_session.get_object_tracker_patient_info(pipeline_id,
|
|
83
|
+
object_tracker_id,
|
|
84
|
+
'',
|
|
85
|
+
patient_id,
|
|
86
|
+
study_uid)
|
|
87
|
+
logging.debug('Checking for new data...')
|
|
88
|
+
if old_data == object_tracker_data:
|
|
89
|
+
logging.debug('No incoming data check timeout')
|
|
90
|
+
if int(time() - last_update) > timeout:
|
|
91
|
+
logging.debug('Timeout reached')
|
|
92
|
+
break
|
|
93
|
+
else:
|
|
94
|
+
last_update = int(time())
|
|
95
|
+
logging.debug('Incoming data...')
|
|
96
|
+
logging.debug(f'Found: {object_tracker_data}')
|
|
97
|
+
|
|
98
|
+
old_data = object_tracker_data
|
|
99
|
+
sleep(delay)
|
|
100
|
+
|
|
101
|
+
return object_tracker_data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class CtpSummary(BaseModel):
|
|
105
|
+
pipeline: str = Field('', validation_alias=AliasChoices('Pipeline'))
|
|
106
|
+
import_queues: int = Field(0, validation_alias=AliasChoices('ImportQueues'))
|
|
107
|
+
export_queues: int = Field(0, validation_alias=AliasChoices('ExportQueues'))
|
|
108
|
+
quarantines: int = Field(0, validation_alias=AliasChoices('Quarantines'))
|
|
109
|
+
time_stamp: datetime.datetime = Field(
|
|
110
|
+
default_factory=lambda: datetime.datetime.now()) # pylint:disable=unnecessary-lambda
|
|
111
|
+
|
|
112
|
+
def __init__(self, row):
|
|
113
|
+
super().__init__()
|
|
114
|
+
values = row.find_all('td')
|
|
115
|
+
self.pipeline = values[0].text
|
|
116
|
+
self.import_queues = int(values[1].text)
|
|
117
|
+
self.export_queues = int(values[2].text)
|
|
118
|
+
self.quarantines = int(values[3].text)
|
|
119
|
+
|
|
120
|
+
@field_validator('import_queues', 'export_queues', 'quarantines', mode='before')
|
|
121
|
+
@classmethod
|
|
122
|
+
def remove_separators(cls, v: str) -> int:
|
|
123
|
+
if isinstance(v, int):
|
|
124
|
+
return v
|
|
125
|
+
return int(v.replace(',', ''))
|
|
126
|
+
|
|
127
|
+
def to_table_row(self) -> List:
|
|
128
|
+
return [self.pipeline, self.import_queues, self.export_queues, self.quarantines]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_summary(html: Union[str, bytes]) -> Union[None, List[CtpSummary]]:
|
|
132
|
+
soup = bs4.BeautifulSoup(html, 'html.parser')
|
|
133
|
+
tables = soup.find_all('table', attrs={'class': 'summary'})
|
|
134
|
+
if len(tables) != 1:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
rows = tables[0].find_all('tr')
|
|
138
|
+
results = [CtpSummary(row) for row in rows[1:]]
|
|
139
|
+
|
|
140
|
+
return results
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def parse_step(html: Union[str, bytes]) -> Dict[str, Dict[str, str]]:
|
|
144
|
+
soup = bs4.BeautifulSoup(html, 'html.parser')
|
|
145
|
+
|
|
146
|
+
status_table = soup.find('h2', string='Status').find_next_sibling('table')
|
|
147
|
+
config_table = soup.find('h2', string='Configuration').find_next_sibling('table')
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
'status': parse_vertical_table(status_table),
|
|
151
|
+
'config': parse_vertical_table(config_table),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def parse_vertical_table(table_element: bs4.Tag) -> Dict[str, str]:
|
|
156
|
+
"""
|
|
157
|
+
Parse the vertical tables that just have 2 columns with a key-value structure
|
|
158
|
+
|
|
159
|
+
:param table_element: table DOM element
|
|
160
|
+
:return: table parsed into a dict
|
|
161
|
+
"""
|
|
162
|
+
rows = table_element.find_all('tr')
|
|
163
|
+
|
|
164
|
+
result = {}
|
|
165
|
+
for row in rows:
|
|
166
|
+
elements = row.find_all('td')
|
|
167
|
+
if len(elements) != 2:
|
|
168
|
+
raise ValueError('Expected table to have 2 columns only!')
|
|
169
|
+
|
|
170
|
+
result[elements[0].text] = elements[1].text
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def summarize_object_tracker_content(object_tracker_content):
|
|
176
|
+
subjects = 0
|
|
177
|
+
experiments = 0
|
|
178
|
+
scans = 0
|
|
179
|
+
instances = 0
|
|
180
|
+
for _, subject_info in object_tracker_content.items():
|
|
181
|
+
subjects += 1
|
|
182
|
+
for _, experiment_info in subject_info.items():
|
|
183
|
+
experiments += 1
|
|
184
|
+
for _, scan_instances in experiment_info.items():
|
|
185
|
+
scans += 1
|
|
186
|
+
instances += scan_instances
|
|
187
|
+
|
|
188
|
+
return subjects, experiments, scans, instances
|
ctpy/tests/mock.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copyright 2024 Biomedical Imaging Group Rotterdam, Departments of
|
|
2
|
+
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from typing import Any, Pattern, Union
|
|
17
|
+
|
|
18
|
+
from requests import Response
|
|
19
|
+
from requests_mock import Mocker
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CTPRequestsMocker(Mocker):
|
|
23
|
+
uri: str = "http://ctp.example.com"
|
|
24
|
+
port: int = 1080
|
|
25
|
+
|
|
26
|
+
def request(self,
|
|
27
|
+
method: str,
|
|
28
|
+
url: Union[str, Pattern[str]],
|
|
29
|
+
**kwargs: Any) -> Response:
|
|
30
|
+
url = f"{self.uri}:{self.port}/{url.lstrip('/')}"
|
|
31
|
+
print(f'Mocking request for {url}')
|
|
32
|
+
return super().request(method, url, **kwargs)
|
ctpy/tests/test_init.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import ctpy
|
|
2
|
+
from ctpy import CTPYclient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_connect(ctpy_mock):
|
|
6
|
+
ctpy_mock.get('/login', cookies={'RSNASESSION': '123'}, status_code=302)
|
|
7
|
+
|
|
8
|
+
build = "2021.04.01 at 11:04:40 CDT"
|
|
9
|
+
ip = "10.42.0.83"
|
|
10
|
+
java = "1.7"
|
|
11
|
+
ctpy_mock.get('/server', text=f"""<?xml version="1.0" encoding="UTF-8" ?>\n<server build="{build}" ip="{ip}" java="{java}" port="1080" />""")
|
|
12
|
+
|
|
13
|
+
connection = ctpy.connect(ctpy_mock.uri, port=ctpy_mock.port, username="test", password="secret")
|
|
14
|
+
|
|
15
|
+
assert isinstance(connection, CTPYclient)
|
|
16
|
+
assert connection.server.build == build
|
|
17
|
+
assert connection.server.ip == ip
|
|
18
|
+
assert connection.server.java == java
|
ctpy/version.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# This file is automatically generated by versioningit during build stage. Do not edit it manually.
|
|
2
|
+
# Configuration is stored in the pyproject.toml file.
|
|
3
|
+
|
|
4
|
+
version = '1.0.1'
|
|
5
|
+
version_tuple = (1, 0, 1)
|
|
6
|
+
base_version = '1.0.1' # Latest released version known at build time
|
|
7
|
+
git_revision = '89d6adf'
|
|
8
|
+
git_revision_full = '89d6adf5e2ce060ebd334f67655bb8d278cfff22'
|
|
9
|
+
git_branch = 'None'
|
|
10
|
+
git_revision_date = '2025-01-16T12:32:55'
|
|
11
|
+
build_date = '2025-01-16T12:36:14'
|
|
12
|
+
|