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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyct
pyct/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .client import ClinicalTrials
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Youssef Sidhom"
5
+
6
+ __all__ = ["ClinicalTrials"]
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()