pycli-trials 0.1.0__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.
- pycli_trials-0.1.0.dist-info/METADATA +78 -0
- pycli_trials-0.1.0.dist-info/RECORD +7 -0
- pycli_trials-0.1.0.dist-info/WHEEL +5 -0
- pycli_trials-0.1.0.dist-info/top_level.txt +1 -0
- pyct/__init__.py +6 -0
- pyct/client.py +193 -0
- pyct/utils.py +74 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycli-trials
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python wrapper for the ClinicalTrials.gov API (v2) with full pagination support and a few extra features
|
|
5
|
+
Home-page: https://github.com/YOUSSEFSAIDHOM/pyct
|
|
6
|
+
Author: Youssef Sidhom
|
|
7
|
+
Author-email: youssefsidhom8@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: pandas
|
|
17
|
+
Requires-Dist: tqdm
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# PyCT 🧬 (v0.1.0)
|
|
30
|
+
|
|
31
|
+
### About ðŸ’
|
|
32
|
+
|
|
33
|
+
PyCT is a python wrapper for the ClinicalTrials.org API (v2). Although a python-wrapper already exists for this API, it doesn't feature full pagination support, thus is unable to give you all the studies that match the search-terms. Moreover, PyCT enables you to search with more accuracy by specifying the conditions and the interventions separately
|
|
34
|
+
|
|
35
|
+
### Basic usage 🪛
|
|
36
|
+
Get started with:
|
|
37
|
+
```python
|
|
38
|
+
from pyct import ClinicalTrials
|
|
39
|
+
|
|
40
|
+
ct = ClinicalTrials()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Checking API info:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
print(ct)
|
|
47
|
+
```
|
|
48
|
+
Fetching studies:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
|
|
52
|
+
# Specify each field
|
|
53
|
+
|
|
54
|
+
df = ct.get_studies(
|
|
55
|
+
condition="Dementia",
|
|
56
|
+
intervention="Non-pharmacological",
|
|
57
|
+
status="RECRUITING"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Alternatively you can get one specific study using the NCT number
|
|
61
|
+
study = ct.get_study("NCT06210035")
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
and finally you can export with the following options below:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
ct.to_csv(df, "alzheimer_trials.csv") # CSV
|
|
68
|
+
|
|
69
|
+
ct.to_excel(df, "alzheimer_trials.xlsx") # Excel
|
|
70
|
+
|
|
71
|
+
ct.to_json(df, "alzheimer_trials.json") # JSON
|
|
72
|
+
```
|
|
73
|
+
It will automatically give a name if you don't specify
|
|
74
|
+
### Installation 📦
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
pip install pyct
|
|
78
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pyct/__init__.py,sha256=zzw8AnjqBlV_oNG3hItuGTmHg9E2obbEiRKyIbNvn9w,117
|
|
2
|
+
pyct/client.py,sha256=plssAearEgmshVS6OtYi7D56345heXynFQbTD01hp7I,5835
|
|
3
|
+
pyct/utils.py,sha256=-jhz_PLrD7J0jvQkbmS74EQ_sOgrEzN8lJMRjazhiVw,2077
|
|
4
|
+
pycli_trials-0.1.0.dist-info/METADATA,sha256=_HDUsThsPttz1KJWD7n4teAMtFaDM4F9I0-IwyfQy0Y,2016
|
|
5
|
+
pycli_trials-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
pycli_trials-0.1.0.dist-info/top_level.txt,sha256=ZtmUS758G8VBqkWk0n5b358fegRExYlWLm6suEQZfnQ,5
|
|
7
|
+
pycli_trials-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyct
|
pyct/__init__.py
ADDED
pyct/client.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
import datetime
|
|
5
|
+
from .utils import get_with_retry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClinicalTrials:
|
|
9
|
+
"""
|
|
10
|
+
PYCT is a Python-wrapper for the ClinicalTrials.gov API
|
|
11
|
+
which supports full pagination, mutliple query fields and built-in export options
|
|
12
|
+
|
|
13
|
+
Basic usage:
|
|
14
|
+
|
|
15
|
+
ct = ClinicalTrials()
|
|
16
|
+
df = ct.get_studies(conditions="Alzheimer", intervention="Non-pharmacological")
|
|
17
|
+
ct.to_csv(df, "trials.csv")
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Base URL for the ClinicalTrials API
|
|
22
|
+
BASE = "https://clinicaltrials.gov/api/v2"
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
info = self._get_api_info()
|
|
26
|
+
self.api_version = info.get("apiVersion")
|
|
27
|
+
self.last_updated = info.get("dataTimestamp")
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return (
|
|
31
|
+
f"PYCT | "
|
|
32
|
+
f"ClinicalTrials API v{self.api_version} | "
|
|
33
|
+
f"Data base last updated {self.last_updated}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ---------------------------- #
|
|
37
|
+
# Public methods #
|
|
38
|
+
# ---------------------------- #
|
|
39
|
+
|
|
40
|
+
def get_studies(self,
|
|
41
|
+
condition = None,
|
|
42
|
+
intervention = None,
|
|
43
|
+
term = None,
|
|
44
|
+
status = None,
|
|
45
|
+
page_size=1000):
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
Retrieves all the studies that match with a given query
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
condition (str) : e.g. "Dementia", "Covid"
|
|
52
|
+
|
|
53
|
+
intervention (str) : e.g. "excercise", "ECT"
|
|
54
|
+
|
|
55
|
+
term (str) : General keyword search across all fields
|
|
56
|
+
|
|
57
|
+
status (str) : Studies may have one of the following recuirtment statuses:
|
|
58
|
+
- RECRUITING, COMPLETED, NOT_YET_RECRUITING, ACTIVE_NOT_RECRUITING, and TERMINATED
|
|
59
|
+
|
|
60
|
+
page_size (int) : Number of results per page (max 1000). Defaults to 1000.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A requests.Response
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
params = {"format":"json", "pageSize":page_size}
|
|
67
|
+
|
|
68
|
+
params["query.cond"] = condition if condition is not None else None
|
|
69
|
+
params["query.intr"] = intervention if intervention is not None else None
|
|
70
|
+
params["query.term"] = term if term is not None else None
|
|
71
|
+
params["filter.status"] = status if status is not None else None
|
|
72
|
+
|
|
73
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
74
|
+
|
|
75
|
+
return self._paginate(params)
|
|
76
|
+
|
|
77
|
+
def get_study(self, nct):
|
|
78
|
+
"""
|
|
79
|
+
All studies can be identified with an NCT number. This method allows you to fetch one study
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
nct (str) : eg. NCT06210035
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
dict: the full study record as a dictionary
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
requests.HTTPError: A HTTP error code if something goes wrong, e.g. (404) If study is not found
|
|
89
|
+
"""
|
|
90
|
+
resp = get_with_retry(f"{self.BASE}/studies/{nct}")
|
|
91
|
+
return resp.json()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------- #
|
|
95
|
+
# Private methods #
|
|
96
|
+
# ---------------------------- #
|
|
97
|
+
|
|
98
|
+
def _get_api_info(self):
|
|
99
|
+
"""
|
|
100
|
+
Returns API version and date last updated.
|
|
101
|
+
"""
|
|
102
|
+
resp = requests.get(f"{self.BASE}/version")
|
|
103
|
+
resp.raise_for_status()
|
|
104
|
+
return resp.json()
|
|
105
|
+
|
|
106
|
+
def _paginate(self, params):
|
|
107
|
+
"""
|
|
108
|
+
Handle pagination automatically
|
|
109
|
+
|
|
110
|
+
It fetches all pages from the /studies endpoint, by passing the limit using a nextPageToken,
|
|
111
|
+
and returns a single combined DataFrame.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
params (dict) : Query parameters to send with each request
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A requests.Response
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
all_dfs = []
|
|
121
|
+
|
|
122
|
+
with tqdm(desc="Fetching studies", unit="page") as pbar:
|
|
123
|
+
while True:
|
|
124
|
+
resp = get_with_retry(f"{self.BASE}/studies", params=params)
|
|
125
|
+
data = resp.json()
|
|
126
|
+
|
|
127
|
+
studies = data.get("studies", [])
|
|
128
|
+
if not studies:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
all_dfs.append(pd.json_normalize(studies))
|
|
132
|
+
|
|
133
|
+
pbar.update(1)
|
|
134
|
+
pbar.set_postfix({"Found ": sum(len(d) for d in all_dfs)})
|
|
135
|
+
|
|
136
|
+
next_token = data.get("nextPageToken")
|
|
137
|
+
if not next_token:
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
params["pageToken"] = next_token
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if not all_dfs:
|
|
144
|
+
return pd.DataFrame()
|
|
145
|
+
|
|
146
|
+
return pd.concat(all_dfs, ignore_index=True)
|
|
147
|
+
|
|
148
|
+
# ---------------------------- #
|
|
149
|
+
# Export methods #
|
|
150
|
+
# ---------------------------- #
|
|
151
|
+
|
|
152
|
+
def to_csv(self, df, filename=None):
|
|
153
|
+
"""
|
|
154
|
+
Usage:
|
|
155
|
+
df = ClinicalTrials().get_studies(...
|
|
156
|
+
ClinicalTrials().to_csv(df, "name.csv")
|
|
157
|
+
|
|
158
|
+
Generates a name automatically if you can't come up with one
|
|
159
|
+
"""
|
|
160
|
+
if not filename:
|
|
161
|
+
filename = f"studies_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
|
162
|
+
|
|
163
|
+
df.to_csv(filename, index=False)
|
|
164
|
+
print(f"{len(df)} saved to {filename}")
|
|
165
|
+
|
|
166
|
+
def to_excel(self, df, filename=None):
|
|
167
|
+
"""
|
|
168
|
+
Usage:
|
|
169
|
+
df = ClinicalTrials().get_studies(...
|
|
170
|
+
ClinicalTrials().to_excel(df, "name.xlsx")
|
|
171
|
+
|
|
172
|
+
Generates a name automatically if you can't come up with one
|
|
173
|
+
"""
|
|
174
|
+
if not filename:
|
|
175
|
+
filename = f"studies_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
|
176
|
+
|
|
177
|
+
df.to_excel(filename, index=False)
|
|
178
|
+
print(f"{len(df)} saved to {filename}")
|
|
179
|
+
|
|
180
|
+
def to_json(self, df, filename=None, indent=2):
|
|
181
|
+
"""
|
|
182
|
+
Usage:
|
|
183
|
+
df = ClinicalTrials().get_studies(...
|
|
184
|
+
ClinicalTrials().to_json(df, "name.json")
|
|
185
|
+
|
|
186
|
+
Generates a name automatically if you can't come up with one
|
|
187
|
+
"""
|
|
188
|
+
if not filename:
|
|
189
|
+
filename = f"studies_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
190
|
+
|
|
191
|
+
df.to_json(filename, orient="records" ,indent=2)
|
|
192
|
+
print(f"{len(df)} saved to {filename}")
|
|
193
|
+
|
pyct/utils.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import requests
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
def get_with_retry(url, params=None, max_attempts=3, wait=2):
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Instead of attempting to retrieve studies and failing,
|
|
9
|
+
This enables you to retry until successful.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
url (str) : The base URL for the API
|
|
13
|
+
|
|
14
|
+
params (dict) : The parameters for the search may include condition, interventions etc
|
|
15
|
+
|
|
16
|
+
max_attempts (int)
|
|
17
|
+
|
|
18
|
+
wait (int) : how long you wait between each attempt in seconds
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
|
|
22
|
+
request.Response : can then be maniuplated as a pandas dataframe
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
If all else fails, a HTTPError is raised, with the last error printed to terminal
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
last_error = None
|
|
30
|
+
|
|
31
|
+
for attempt in range(1, max_attempts+1):
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
resp = requests.get(url, params=params)
|
|
35
|
+
resp.raise_for_status()
|
|
36
|
+
print(f"Succeeded in {attempt}/{max_attempts} attempts")
|
|
37
|
+
return resp
|
|
38
|
+
|
|
39
|
+
except requests.HTTPError as e:
|
|
40
|
+
last_error = e
|
|
41
|
+
status_code = e.response.status_code
|
|
42
|
+
|
|
43
|
+
# Retrying 4xx errors e.g. 404 or 400 won't succeed
|
|
44
|
+
# ...Except for error 429 which is 'too many requests'
|
|
45
|
+
|
|
46
|
+
if 400 <= status_code < 500 and status_code != 429:
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
print(f"Attempt {attempt}/{max_attempts} has failed with {status_code}")
|
|
50
|
+
|
|
51
|
+
waiting(wait)
|
|
52
|
+
|
|
53
|
+
except requests.ConnectionError as e:
|
|
54
|
+
last_error = e
|
|
55
|
+
print(f"Attempt {attempt}/{max_attempts} failed due to a connection error")
|
|
56
|
+
|
|
57
|
+
waiting(wait)
|
|
58
|
+
|
|
59
|
+
except requests.Timeout as e:
|
|
60
|
+
last_error = e
|
|
61
|
+
print(f"Attempt {attempt}/{max_attempts} failed, request timed out")
|
|
62
|
+
|
|
63
|
+
raise requests.HTTPError(f"{max_attempts}/{max_attempts} attemps failed :(\nLast Error:\n\t{last_error} ")
|
|
64
|
+
|
|
65
|
+
def waiting(wait):
|
|
66
|
+
"""
|
|
67
|
+
Procedure for the waiting text 'animation'
|
|
68
|
+
"""
|
|
69
|
+
while wait > 0:
|
|
70
|
+
sys.stdout.write("\r" + f"Retrying in {wait}s")
|
|
71
|
+
time.sleep(1)
|
|
72
|
+
wait -= 1
|
|
73
|
+
sys.stdout.flush()
|
|
74
|
+
print()
|