discotoolkit 1.2.0__tar.gz → 1.2.2__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.
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/PKG-INFO +14 -8
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/README.md +8 -7
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/CELLiD.py +52 -37
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/DiscoClass.py +29 -17
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/DownloadDiscoData.py +9 -5
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/GeneSearch.py +5 -5
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/GetMetadata.py +14 -14
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/GlobalVariable.py +3 -2
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/__init__.py +1 -1
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/PKG-INFO +14 -8
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/SOURCES.txt +3 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/setup.py +8 -1
- discotoolkit-1.2.2/tests/test_docs.py +112 -0
- discotoolkit-1.2.2/tests/test_gene_search.py +61 -0
- discotoolkit-1.2.2/tests/test_reference_loading.py +98 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/tests/test_server_contract.py +14 -1
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/MANIFEST.in +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/CellMapper.py +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/Utilities.py +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/dependency_links.txt +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/requires.txt +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/top_level.txt +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/pyproject.toml +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/requirements.txt +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/setup.cfg +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/tests/test_settings.py +0 -0
- {discotoolkit-1.2.0 → discotoolkit-1.2.2}/tests/test_version.py +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: discotoolkit
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: DISCOtoolkit is an python package that allows users to access data and use the tools provided by the DISCO database.
|
|
5
5
|
Home-page: https://disco.bii.a-star.edu.sg/
|
|
6
6
|
Author: Li Mengwei, Rom Uddamvathanak
|
|
7
7
|
Author-email: uddamvathanak_rom@immunol.a-star.edu.sg
|
|
8
|
+
Project-URL: Documentation, https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/
|
|
9
|
+
Project-URL: Source, https://github.com/JinmiaoChenLab/DISCOtoolkit_py
|
|
10
|
+
Project-URL: Issues, https://github.com/JinmiaoChenLab/DISCOtoolkit_py/issues
|
|
11
|
+
Project-URL: DISCO, https://disco.bii.a-star.edu.sg/v1/
|
|
8
12
|
Requires-Python: >=3.9
|
|
9
13
|
Description-Content-Type: text/markdown
|
|
10
14
|
Requires-Dist: pandas>=1.4.2
|
|
@@ -24,6 +28,7 @@ Dynamic: author-email
|
|
|
24
28
|
Dynamic: description
|
|
25
29
|
Dynamic: description-content-type
|
|
26
30
|
Dynamic: home-page
|
|
31
|
+
Dynamic: project-url
|
|
27
32
|
Dynamic: requires-dist
|
|
28
33
|
Dynamic: requires-python
|
|
29
34
|
Dynamic: summary
|
|
@@ -36,18 +41,18 @@ Dynamic: summary
|
|
|
36
41
|
* @LastEditors: Mengwei Li
|
|
37
42
|
* @LastEditTime: 2023-04-16 21:22:03
|
|
38
43
|
-->
|
|
39
|
-
[](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/) [](https://pepy.tech/project/discotoolkit) [](https://pypi.org/project/discotoolkit)
|
|
40
45
|
|
|
41
|
-
# DISCOtoolkit 1.2.
|
|
46
|
+
# DISCOtoolkit 1.2.2
|
|
42
47
|
|
|
43
|
-
DISCOtoolkit is a Python package for accessing the data and tools of the [DISCO database](https://disco.bii.a-star.edu.sg/) (DISCO v1). Read the documentation at [
|
|
48
|
+
DISCOtoolkit is a Python package for accessing the data and tools of the [DISCO database](https://disco.bii.a-star.edu.sg/) (DISCO v1). Read the documentation, with tutorials and the API reference, at [disco.bii.a-star.edu.sg/v1/docs/toolkit/guide](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/).
|
|
44
49
|
|
|
45
50
|
- Filter and download DISCO data based on sample metadata and cell type information
|
|
46
51
|
- Gene search: a gene's expression across cell types and tissues
|
|
47
52
|
- CELLiD: cell type annotation
|
|
48
53
|
- scEnrichment: gene set enrichment using DISCO DEGs
|
|
49
54
|
|
|
50
|
-
> **DISCO v1
|
|
55
|
+
> **Use DISCO v1.** The toolkit is developed and tested against **DISCO v1**, which is the default, and that is the server we recommend. DISCO v2 has its own R package ([DISCOtoolkit](https://github.com/JinmiaoChenLab/DISCOtoolkit)). The Python package can be pointed at a v2 server, but that is not tested; see [Choosing a server](#choosing-a-server).
|
|
51
56
|
|
|
52
57
|
## Quickstart (Google Colab)
|
|
53
58
|
|
|
@@ -97,7 +102,7 @@ python -m pip install -U discotoolkit
|
|
|
97
102
|
import discotoolkit as dt
|
|
98
103
|
|
|
99
104
|
dt.get_server() # 'https://disco.bii.a-star.edu.sg/disco_v3_api/' (DISCO v1, the default)
|
|
100
|
-
dt.set_server("v2") # DISCO v2
|
|
105
|
+
dt.set_server("v2") # DISCO v2 (not tested; DISCO v1 is the supported server)
|
|
101
106
|
dt.set_server("https://my.server/disco_v3_api/") # any server with the same API
|
|
102
107
|
```
|
|
103
108
|
|
|
@@ -110,11 +115,12 @@ Two small test files need nothing beyond `requests`, so they run anywhere, inclu
|
|
|
110
115
|
```
|
|
111
116
|
python tests/test_settings.py # offline: server selection logic
|
|
112
117
|
python tests/test_version.py # offline: setup.py and __init__.py agree on the version
|
|
118
|
+
python tests/test_docs.py # offline: notebooks are clean, every public function is in the API reference
|
|
113
119
|
python tests/test_server_contract.py # online: does the server answer everything the toolkit needs?
|
|
114
|
-
python tests/test_server_contract.py
|
|
120
|
+
python tests/test_server_contract.py <api root> # check another server
|
|
115
121
|
```
|
|
116
122
|
|
|
117
|
-
GitHub Actions runs these, and installs the package on Python 3.9-3.12, on every push. Maintainers: see [RELEASING.md](RELEASING.md) for how a release is published.
|
|
123
|
+
GitHub Actions runs these, and installs the package on Python 3.9-3.12, on every push. Maintainers: see [RELEASING.md](RELEASING.md) for how a release, and its documentation, is published.
|
|
118
124
|
|
|
119
125
|
`test_server_contract.py` checks status codes and the shape of each response (the columns and file types the toolkit reads). It only samples the large reference files, it does not download them.
|
|
120
126
|
|
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
* @LastEditors: Mengwei Li
|
|
7
7
|
* @LastEditTime: 2023-04-16 21:22:03
|
|
8
8
|
-->
|
|
9
|
-
[](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/) [](https://pepy.tech/project/discotoolkit) [](https://pypi.org/project/discotoolkit)
|
|
10
10
|
|
|
11
|
-
# DISCOtoolkit 1.2.
|
|
11
|
+
# DISCOtoolkit 1.2.2
|
|
12
12
|
|
|
13
|
-
DISCOtoolkit is a Python package for accessing the data and tools of the [DISCO database](https://disco.bii.a-star.edu.sg/) (DISCO v1). Read the documentation at [
|
|
13
|
+
DISCOtoolkit is a Python package for accessing the data and tools of the [DISCO database](https://disco.bii.a-star.edu.sg/) (DISCO v1). Read the documentation, with tutorials and the API reference, at [disco.bii.a-star.edu.sg/v1/docs/toolkit/guide](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/).
|
|
14
14
|
|
|
15
15
|
- Filter and download DISCO data based on sample metadata and cell type information
|
|
16
16
|
- Gene search: a gene's expression across cell types and tissues
|
|
17
17
|
- CELLiD: cell type annotation
|
|
18
18
|
- scEnrichment: gene set enrichment using DISCO DEGs
|
|
19
19
|
|
|
20
|
-
> **DISCO v1
|
|
20
|
+
> **Use DISCO v1.** The toolkit is developed and tested against **DISCO v1**, which is the default, and that is the server we recommend. DISCO v2 has its own R package ([DISCOtoolkit](https://github.com/JinmiaoChenLab/DISCOtoolkit)). The Python package can be pointed at a v2 server, but that is not tested; see [Choosing a server](#choosing-a-server).
|
|
21
21
|
|
|
22
22
|
## Quickstart (Google Colab)
|
|
23
23
|
|
|
@@ -67,7 +67,7 @@ python -m pip install -U discotoolkit
|
|
|
67
67
|
import discotoolkit as dt
|
|
68
68
|
|
|
69
69
|
dt.get_server() # 'https://disco.bii.a-star.edu.sg/disco_v3_api/' (DISCO v1, the default)
|
|
70
|
-
dt.set_server("v2") # DISCO v2
|
|
70
|
+
dt.set_server("v2") # DISCO v2 (not tested; DISCO v1 is the supported server)
|
|
71
71
|
dt.set_server("https://my.server/disco_v3_api/") # any server with the same API
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -80,11 +80,12 @@ Two small test files need nothing beyond `requests`, so they run anywhere, inclu
|
|
|
80
80
|
```
|
|
81
81
|
python tests/test_settings.py # offline: server selection logic
|
|
82
82
|
python tests/test_version.py # offline: setup.py and __init__.py agree on the version
|
|
83
|
+
python tests/test_docs.py # offline: notebooks are clean, every public function is in the API reference
|
|
83
84
|
python tests/test_server_contract.py # online: does the server answer everything the toolkit needs?
|
|
84
|
-
python tests/test_server_contract.py
|
|
85
|
+
python tests/test_server_contract.py <api root> # check another server
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
GitHub Actions runs these, and installs the package on Python 3.9-3.12, on every push. Maintainers: see [RELEASING.md](RELEASING.md) for how a release is published.
|
|
88
|
+
GitHub Actions runs these, and installs the package on Python 3.9-3.12, on every push. Maintainers: see [RELEASING.md](RELEASING.md) for how a release, and its documentation, is published.
|
|
88
89
|
|
|
89
90
|
`test_server_contract.py` checks status codes and the shape of each response (the columns and file types the toolkit reads). It only samples the large reference files, it does not download them.
|
|
90
91
|
|
|
@@ -56,11 +56,11 @@ def generate_correlation_map(x, y):
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
# get the total atlas from DISCO website for the user to select which atlas to use
|
|
59
|
-
def get_atlas(ref_data=None, ref_path=None):
|
|
59
|
+
def get_atlas(ref_data=None, ref_path=None) -> list:
|
|
60
60
|
"""get the all the atlas string from the DISCO website and return to the user
|
|
61
61
|
|
|
62
62
|
Returns:
|
|
63
|
-
|
|
63
|
+
list: the atlas names, as a list of strings
|
|
64
64
|
"""
|
|
65
65
|
# Download reference data and ref_deg if missing
|
|
66
66
|
if ref_data is None:
|
|
@@ -72,23 +72,56 @@ def get_atlas(ref_data=None, ref_path=None):
|
|
|
72
72
|
if not os.path.exists(ref_path):
|
|
73
73
|
os.mkdir(ref_path)
|
|
74
74
|
|
|
75
|
-
# download reference data from the server
|
|
75
|
+
# download the reference data from the server (once) and read it as a pandas dataframe
|
|
76
76
|
if ref_data is None:
|
|
77
|
-
|
|
78
|
-
logging.info("Downloading reference dataset...")
|
|
79
|
-
response = requests.get(
|
|
80
|
-
url=api_url("toolkit/getRef?type=pkl"), timeout=timeout
|
|
81
|
-
)
|
|
82
|
-
open(ref_path + "/ref_data.pkl", "wb").write(response.content)
|
|
83
|
-
ref_data = pd.read_pickle(
|
|
84
|
-
ref_path + "/ref_data.pkl",
|
|
85
|
-
compression={"method": "gzip", "compresslevel": 6},
|
|
86
|
-
)
|
|
77
|
+
ref_data = _load_reference(ref_path, "ref_data", "getRef", index_col=0)
|
|
87
78
|
|
|
88
79
|
all_atlas = [each.split("--")[1] for each in ref_data.columns]
|
|
89
80
|
return list(set(all_atlas))
|
|
90
81
|
|
|
91
82
|
|
|
83
|
+
def _load_reference(ref_path: str, name: str, endpoint: str, **csv_options) -> pd.DataFrame:
|
|
84
|
+
"""Get a CELLiD reference table (`name` is "ref_data" or "ref_deg"), downloading it once.
|
|
85
|
+
|
|
86
|
+
DISCO v1 serves it as plain CSV (`toolkit/<endpoint>?type=csv`, gzip-compressed on the wire),
|
|
87
|
+
read with pandas.read_csv. A copy already in `ref_path` is used as it is (a CSV, or the
|
|
88
|
+
pickle that earlier versions saved). Older servers only offer a pickle; that is the fallback,
|
|
89
|
+
with a warning, because unpickling data from a network is only safe if the server is trusted.
|
|
90
|
+
"""
|
|
91
|
+
csv_file = "%s/%s.csv" % (ref_path, name)
|
|
92
|
+
pkl_file = "%s/%s.pkl" % (ref_path, name)
|
|
93
|
+
|
|
94
|
+
if os.path.exists(csv_file):
|
|
95
|
+
return pd.read_csv(csv_file, **csv_options)
|
|
96
|
+
if os.path.exists(pkl_file):
|
|
97
|
+
return pd.read_pickle(pkl_file, compression={"method": "gzip", "compresslevel": 6})
|
|
98
|
+
|
|
99
|
+
logging.info("Downloading reference dataset (%s)...", name)
|
|
100
|
+
with requests.get(
|
|
101
|
+
url=api_url("toolkit/" + endpoint), params={"type": "csv"}, stream=True, timeout=timeout
|
|
102
|
+
) as response:
|
|
103
|
+
if response.status_code == 200:
|
|
104
|
+
# write to a temporary name so an interrupted download is not mistaken for the file
|
|
105
|
+
with open(csv_file + ".part", "wb") as handle:
|
|
106
|
+
for chunk in response.iter_content(chunk_size=1 << 20):
|
|
107
|
+
handle.write(chunk)
|
|
108
|
+
os.replace(csv_file + ".part", csv_file)
|
|
109
|
+
return pd.read_csv(csv_file, **csv_options)
|
|
110
|
+
|
|
111
|
+
logging.warning(
|
|
112
|
+
"This server has no CSV for %s (HTTP %s); falling back to the pickle download.",
|
|
113
|
+
name,
|
|
114
|
+
response.status_code,
|
|
115
|
+
)
|
|
116
|
+
response = requests.get(
|
|
117
|
+
url=api_url("toolkit/" + endpoint), params={"type": "pkl"}, timeout=timeout
|
|
118
|
+
)
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
with open(pkl_file, "wb") as handle:
|
|
121
|
+
handle.write(response.content)
|
|
122
|
+
return pd.read_pickle(pkl_file, compression={"method": "gzip", "compresslevel": 6})
|
|
123
|
+
|
|
124
|
+
|
|
92
125
|
def _load_geneset(ref_path: str) -> pd.DataFrame:
|
|
93
126
|
"""Get the gene-set reference (columns logfc, gene, atlas, name), downloading it once.
|
|
94
127
|
|
|
@@ -133,7 +166,7 @@ def CELLiD_cluster(
|
|
|
133
166
|
n_predict: int = 1,
|
|
134
167
|
ref_path: str = None,
|
|
135
168
|
ncores: int = 10,
|
|
136
|
-
):
|
|
169
|
+
) -> pd.DataFrame:
|
|
137
170
|
"""Cell type annotation using reference data and compute the correlation between the user cell gene expression as compare
|
|
138
171
|
to the reference data. The celltype with highest correlation will be concluded as the celltype
|
|
139
172
|
|
|
@@ -175,31 +208,13 @@ def CELLiD_cluster(
|
|
|
175
208
|
if not os.path.exists(ref_path):
|
|
176
209
|
os.mkdir(ref_path)
|
|
177
210
|
|
|
178
|
-
# download reference data from the server
|
|
211
|
+
# download the reference data from the server (once) and read it as a pandas dataframe
|
|
179
212
|
if ref_data is None:
|
|
180
|
-
|
|
181
|
-
logging.info("Downloading reference dataset...")
|
|
182
|
-
response = requests.get(
|
|
183
|
-
url=api_url("toolkit/getRef?type=pkl"), timeout=timeout
|
|
184
|
-
)
|
|
185
|
-
open(ref_path + "/ref_data.pkl", "wb").write(response.content)
|
|
186
|
-
ref_data = pd.read_pickle(
|
|
187
|
-
ref_path + "/ref_data.pkl",
|
|
188
|
-
compression={"method": "gzip", "compresslevel": 6},
|
|
189
|
-
)
|
|
213
|
+
ref_data = _load_reference(ref_path, "ref_data", "getRef", index_col=0)
|
|
190
214
|
|
|
191
215
|
# similarly do the same for the DEG reference
|
|
192
216
|
if ref_deg is None:
|
|
193
|
-
|
|
194
|
-
logging.info("Downloading deg dataset...")
|
|
195
|
-
response = requests.get(
|
|
196
|
-
url=api_url("toolkit/getRefDeg?type=pkl"), timeout=timeout
|
|
197
|
-
)
|
|
198
|
-
open(ref_path + "/ref_deg.pkl", "wb").write(response.content)
|
|
199
|
-
ref_deg = pd.read_pickle(
|
|
200
|
-
ref_path + "/ref_deg.pkl",
|
|
201
|
-
compression={"method": "gzip", "compresslevel": 6},
|
|
202
|
-
)
|
|
217
|
+
ref_deg = _load_reference(ref_path, "ref_deg", "getRefDeg")
|
|
203
218
|
|
|
204
219
|
####
|
|
205
220
|
# write list of atlas function
|
|
@@ -290,7 +305,7 @@ def CELLiD_cluster(
|
|
|
290
305
|
predict.columns = ["cor"]
|
|
291
306
|
predict = predict.sort_values(["cor"], ascending=False)
|
|
292
307
|
res = [
|
|
293
|
-
[each.split("--")[0], each.split("--")[1], predict["cor"][index], i]
|
|
308
|
+
[each.split("--")[0], each.split("--")[1], predict["cor"].iloc[index], i]
|
|
294
309
|
for index, each in enumerate(predict.index)
|
|
295
310
|
if index < n_predict
|
|
296
311
|
]
|
|
@@ -341,7 +356,7 @@ def CELLiD_enrichment(
|
|
|
341
356
|
reference: pd.DataFrame = None,
|
|
342
357
|
ref_path: str = None,
|
|
343
358
|
ncores: int = 1,
|
|
344
|
-
):
|
|
359
|
+
) -> pd.DataFrame:
|
|
345
360
|
"""Function to generate enrichment analysis based on the reference gene sets and following the DISCO pipeline.
|
|
346
361
|
|
|
347
362
|
Args:
|
|
@@ -3,21 +3,25 @@ Class for filtering dataset
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
class Filter:
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
"""Which DISCO samples and cells to look for.
|
|
7
|
+
|
|
8
|
+
Every argument is optional; a filter with none of them matches everything. Give a string or a
|
|
9
|
+
list of strings. The list of values each field accepts is `list_metadata_item(field)`.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
sample_id (str or list, optional): sample identifier, e.g. "ERX2757110".
|
|
13
|
+
project_id (str or list, optional): project (a study or dataset), e.g. "GSE147520".
|
|
14
|
+
tissue (str or list, optional): e.g. "lung", "bladder".
|
|
15
|
+
disease (str or list, optional): e.g. "COVID-19".
|
|
16
|
+
platform (str or list, optional): sequencing platform, e.g. "10x3'".
|
|
17
|
+
sample_type (str or list, optional): e.g. "control".
|
|
18
|
+
cell_type (str or list, optional): keep samples that contain these cell types, and download
|
|
19
|
+
only those cells.
|
|
20
|
+
cell_type_confidence (str): how sure DISCO's annotation must be: "high", "medium" or "all".
|
|
21
|
+
Defaults to "medium".
|
|
22
|
+
include_cell_type_children (bool): also match the more specific cell types under
|
|
23
|
+
`cell_type`. Defaults to True.
|
|
24
|
+
min_cell_per_sample (int): drop samples with fewer matching cells than this. Defaults to 100.
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
def __init__(self, sample_id = None, project_id = None, tissue = None, disease = None, platform = None, sample_type = None,
|
|
@@ -43,8 +47,16 @@ class Filter:
|
|
|
43
47
|
|
|
44
48
|
class FilterData:
|
|
45
49
|
|
|
46
|
-
"""
|
|
47
|
-
|
|
50
|
+
"""The result of a filter: the matching samples, with their counts.
|
|
51
|
+
|
|
52
|
+
Returned by `filter_disco_metadata` and passed to `download_disco_data`.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
sample_metadata (pandas.DataFrame): one row per matching sample.
|
|
56
|
+
cell_type_metadata (pandas.DataFrame): the cell types found in each sample.
|
|
57
|
+
sample_count (int): number of matching samples.
|
|
58
|
+
cell_count (int): number of matching cells.
|
|
59
|
+
filter (Filter): the filter that produced this result.
|
|
48
60
|
"""
|
|
49
61
|
def __init__(self, sample_metadata = None, cell_type_metadata = None, sample_count = None, cell_count = None, filter = Filter()):
|
|
50
62
|
self.sample_metadata = sample_metadata
|
|
@@ -22,14 +22,18 @@ from .DiscoClass import FilterData, Filter
|
|
|
22
22
|
from .GetMetadata import check_in_list
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def download_disco_data(metadata, output_dir: str = "DISCOtmp"):
|
|
26
|
-
"""
|
|
25
|
+
def download_disco_data(metadata, output_dir: str = "DISCOtmp") -> None:
|
|
26
|
+
"""Download the samples of a filter result, one AnnData `.h5ad` file per sample.
|
|
27
|
+
|
|
28
|
+
Each file holds only the cells that matched the filter, with DISCO's annotation in
|
|
29
|
+
`obs["cell_type"]`.
|
|
30
|
+
|
|
27
31
|
Args:
|
|
28
|
-
metadata (FilterData)
|
|
29
|
-
output_dir (
|
|
32
|
+
metadata (FilterData): the result of `filter_disco_metadata`.
|
|
33
|
+
output_dir (str): directory to save the files in; created if missing. Defaults to "DISCOtmp".
|
|
30
34
|
|
|
31
35
|
Returns:
|
|
32
|
-
None:
|
|
36
|
+
None: the files are written to `output_dir`.
|
|
33
37
|
"""
|
|
34
38
|
|
|
35
39
|
# define a list to store the error sample that can not be download
|
|
@@ -10,7 +10,7 @@ import colorcet as cc
|
|
|
10
10
|
from .GlobalVariable import api_url
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def gene_search(gene : str, atlas : Union[str, list] = None, figsize : tuple = None, dpi : int = 300):
|
|
13
|
+
def gene_search(gene : str, atlas : Union[str, list] = None, figsize : tuple = None, dpi : int = 300) -> None:
|
|
14
14
|
|
|
15
15
|
"""Function to search for the gene expression level the same as the input gene search bar in DISCO website.
|
|
16
16
|
|
|
@@ -58,8 +58,8 @@ def gene_search(gene : str, atlas : Union[str, list] = None, figsize : tuple = N
|
|
|
58
58
|
if atlas is not None:
|
|
59
59
|
sample_df = sample_df[sample_df["atlases"].isin(atlas)].copy()
|
|
60
60
|
|
|
61
|
-
# Define the colorcet
|
|
62
|
-
custom_palette = cc.glasbey_hv
|
|
61
|
+
# Define the colorcet palette, one colour per atlas shown
|
|
62
|
+
custom_palette = cc.glasbey_hv[: max(sample_df["atlases"].nunique(), 1)]
|
|
63
63
|
|
|
64
64
|
# saturation for the plot color
|
|
65
65
|
saturation=0.8
|
|
@@ -80,7 +80,7 @@ def gene_search(gene : str, atlas : Union[str, list] = None, figsize : tuple = N
|
|
|
80
80
|
fig = plt.figure(figsize=figsize)
|
|
81
81
|
|
|
82
82
|
# Create the violin plot with different colors based on the 'Category' variable and adding boxplot to assist user with median value
|
|
83
|
-
ax = sns.violinplot(data=sample_df, x='cell types', y='value', hue="atlases", width=0.8, dodge=False, cut=1, saturation = saturation,
|
|
83
|
+
ax = sns.violinplot(data=sample_df, x='cell types', y='value', hue="atlases", width=0.8, dodge=False, cut=1, saturation = saturation,
|
|
84
84
|
inner=None, linewidth=0.0, palette=custom_palette)
|
|
85
85
|
|
|
86
86
|
ax_2 = sns.boxplot(x='cell types', y='value', hue="atlases", data=sample_df, palette=custom_palette, dodge=False, width=0.5, saturation = saturation,
|
|
@@ -113,7 +113,7 @@ def gene_search(gene : str, atlas : Union[str, list] = None, figsize : tuple = N
|
|
|
113
113
|
# Set labels and title
|
|
114
114
|
plt.xlabel('Cell Types')
|
|
115
115
|
plt.ylabel('Expression Value')
|
|
116
|
-
plt.title(gene, fontweight='bold')
|
|
116
|
+
plt.title(gene, fontweight='bold', loc='left') # left: the legend sits above the centre
|
|
117
117
|
|
|
118
118
|
# Show the plot
|
|
119
119
|
plt.show()
|
|
@@ -55,15 +55,15 @@ def get_sample_ct_info():
|
|
|
55
55
|
return temp # return Dataframe of the JSON data
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def find_celltype(term: str = "", cell_ontology: dict = None):
|
|
58
|
+
def find_celltype(term: str = "", cell_ontology: dict = None) -> list:
|
|
59
59
|
"""find the celltype within the disco dataset
|
|
60
60
|
|
|
61
61
|
Args:
|
|
62
62
|
term (String): term refer to string of the cell type
|
|
63
|
-
cell_ontology (
|
|
63
|
+
cell_ontology (dict, optional): cell_ontology can be provided by the user in the format of dictionary datatype in python. Defaults to None.
|
|
64
64
|
|
|
65
65
|
Returns:
|
|
66
|
-
|
|
66
|
+
list: the cell types that match the term
|
|
67
67
|
"""
|
|
68
68
|
|
|
69
69
|
# when the user do not have cell ontology, we get the default one from disco database
|
|
@@ -113,7 +113,7 @@ def get_disco_metadata():
|
|
|
113
113
|
return metadata
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def get_celltype_children(cell_type: Union[str, list], cell_ontology: dict = None):
|
|
116
|
+
def get_celltype_children(cell_type: Union[str, list], cell_ontology: dict = None) -> list:
|
|
117
117
|
"""get the children of the input celltype from the user
|
|
118
118
|
|
|
119
119
|
Args:
|
|
@@ -121,7 +121,7 @@ def get_celltype_children(cell_type: Union[str, list], cell_ontology: dict = Non
|
|
|
121
121
|
cell_ontology (dict, optional): cell_ontology can be provided by the user in the format of dictionary datatype in python. Defaults to None.
|
|
122
122
|
|
|
123
123
|
Returns:
|
|
124
|
-
|
|
124
|
+
list: the children of the defined cell type, as a list of strings
|
|
125
125
|
"""
|
|
126
126
|
if cell_ontology is None:
|
|
127
127
|
cell_ontology = pd.DataFrame(
|
|
@@ -159,14 +159,14 @@ def get_celltype_children(cell_type: Union[str, list], cell_ontology: dict = Non
|
|
|
159
159
|
return children
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def list_metadata_item(field: str):
|
|
162
|
+
def list_metadata_item(field: str) -> list:
|
|
163
163
|
"""List element inside the metadata columns
|
|
164
164
|
|
|
165
165
|
Args:
|
|
166
166
|
field (str): metadata columns or field from the disco database
|
|
167
167
|
|
|
168
168
|
Returns:
|
|
169
|
-
|
|
169
|
+
list: the unique elements of the metadata column, as a reference for filtering
|
|
170
170
|
"""
|
|
171
171
|
|
|
172
172
|
# first get the disco metadata
|
|
@@ -182,11 +182,11 @@ def list_metadata_item(field: str):
|
|
|
182
182
|
return None
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
def list_all_columns():
|
|
185
|
+
def list_all_columns() -> list:
|
|
186
186
|
"""list all the columns found in the metadata of the disco database
|
|
187
187
|
|
|
188
188
|
Returns:
|
|
189
|
-
|
|
189
|
+
list: the names of the metadata columns, as a list of strings
|
|
190
190
|
"""
|
|
191
191
|
|
|
192
192
|
# get the disco metadata
|
|
@@ -194,14 +194,14 @@ def list_all_columns():
|
|
|
194
194
|
return list(metadata.columns)
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
def filter_disco_metadata(filter: Filter = Filter()):
|
|
198
|
-
"""
|
|
197
|
+
def filter_disco_metadata(filter: Filter = Filter()) -> FilterData:
|
|
198
|
+
"""Find the DISCO samples and cells that match a filter.
|
|
199
|
+
|
|
199
200
|
Args:
|
|
200
|
-
|
|
201
|
-
FilterData.filter.cell_type_confidence (String): requires string to be in ["high", "medium", "all"]
|
|
201
|
+
filter (Filter): what to look for. With no argument, everything matches.
|
|
202
202
|
|
|
203
203
|
Returns:
|
|
204
|
-
FilterData
|
|
204
|
+
FilterData: the matching samples and counts, to pass to `download_disco_data`.
|
|
205
205
|
"""
|
|
206
206
|
|
|
207
207
|
# starting with defining variable
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Settings shared by the rest of the package: logging, the request timeout and --
|
|
3
3
|
most importantly -- which DISCO server the toolkit talks to.
|
|
4
4
|
|
|
5
|
-
By default the toolkit talks to DISCO v1 (https://disco.bii.a-star.edu.sg).
|
|
6
|
-
|
|
5
|
+
By default the toolkit talks to DISCO v1 (https://disco.bii.a-star.edu.sg). DISCO v1 is the
|
|
6
|
+
server the toolkit is developed and tested against, and the one we recommend. Other servers
|
|
7
|
+
are possible but untested (the "v2" preset in particular):
|
|
7
8
|
|
|
8
9
|
import discotoolkit as dt
|
|
9
10
|
dt.set_server("v2") # a named preset
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: discotoolkit
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: DISCOtoolkit is an python package that allows users to access data and use the tools provided by the DISCO database.
|
|
5
5
|
Home-page: https://disco.bii.a-star.edu.sg/
|
|
6
6
|
Author: Li Mengwei, Rom Uddamvathanak
|
|
7
7
|
Author-email: uddamvathanak_rom@immunol.a-star.edu.sg
|
|
8
|
+
Project-URL: Documentation, https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/
|
|
9
|
+
Project-URL: Source, https://github.com/JinmiaoChenLab/DISCOtoolkit_py
|
|
10
|
+
Project-URL: Issues, https://github.com/JinmiaoChenLab/DISCOtoolkit_py/issues
|
|
11
|
+
Project-URL: DISCO, https://disco.bii.a-star.edu.sg/v1/
|
|
8
12
|
Requires-Python: >=3.9
|
|
9
13
|
Description-Content-Type: text/markdown
|
|
10
14
|
Requires-Dist: pandas>=1.4.2
|
|
@@ -24,6 +28,7 @@ Dynamic: author-email
|
|
|
24
28
|
Dynamic: description
|
|
25
29
|
Dynamic: description-content-type
|
|
26
30
|
Dynamic: home-page
|
|
31
|
+
Dynamic: project-url
|
|
27
32
|
Dynamic: requires-dist
|
|
28
33
|
Dynamic: requires-python
|
|
29
34
|
Dynamic: summary
|
|
@@ -36,18 +41,18 @@ Dynamic: summary
|
|
|
36
41
|
* @LastEditors: Mengwei Li
|
|
37
42
|
* @LastEditTime: 2023-04-16 21:22:03
|
|
38
43
|
-->
|
|
39
|
-
[](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/) [](https://pepy.tech/project/discotoolkit) [](https://pypi.org/project/discotoolkit)
|
|
40
45
|
|
|
41
|
-
# DISCOtoolkit 1.2.
|
|
46
|
+
# DISCOtoolkit 1.2.2
|
|
42
47
|
|
|
43
|
-
DISCOtoolkit is a Python package for accessing the data and tools of the [DISCO database](https://disco.bii.a-star.edu.sg/) (DISCO v1). Read the documentation at [
|
|
48
|
+
DISCOtoolkit is a Python package for accessing the data and tools of the [DISCO database](https://disco.bii.a-star.edu.sg/) (DISCO v1). Read the documentation, with tutorials and the API reference, at [disco.bii.a-star.edu.sg/v1/docs/toolkit/guide](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/).
|
|
44
49
|
|
|
45
50
|
- Filter and download DISCO data based on sample metadata and cell type information
|
|
46
51
|
- Gene search: a gene's expression across cell types and tissues
|
|
47
52
|
- CELLiD: cell type annotation
|
|
48
53
|
- scEnrichment: gene set enrichment using DISCO DEGs
|
|
49
54
|
|
|
50
|
-
> **DISCO v1
|
|
55
|
+
> **Use DISCO v1.** The toolkit is developed and tested against **DISCO v1**, which is the default, and that is the server we recommend. DISCO v2 has its own R package ([DISCOtoolkit](https://github.com/JinmiaoChenLab/DISCOtoolkit)). The Python package can be pointed at a v2 server, but that is not tested; see [Choosing a server](#choosing-a-server).
|
|
51
56
|
|
|
52
57
|
## Quickstart (Google Colab)
|
|
53
58
|
|
|
@@ -97,7 +102,7 @@ python -m pip install -U discotoolkit
|
|
|
97
102
|
import discotoolkit as dt
|
|
98
103
|
|
|
99
104
|
dt.get_server() # 'https://disco.bii.a-star.edu.sg/disco_v3_api/' (DISCO v1, the default)
|
|
100
|
-
dt.set_server("v2") # DISCO v2
|
|
105
|
+
dt.set_server("v2") # DISCO v2 (not tested; DISCO v1 is the supported server)
|
|
101
106
|
dt.set_server("https://my.server/disco_v3_api/") # any server with the same API
|
|
102
107
|
```
|
|
103
108
|
|
|
@@ -110,11 +115,12 @@ Two small test files need nothing beyond `requests`, so they run anywhere, inclu
|
|
|
110
115
|
```
|
|
111
116
|
python tests/test_settings.py # offline: server selection logic
|
|
112
117
|
python tests/test_version.py # offline: setup.py and __init__.py agree on the version
|
|
118
|
+
python tests/test_docs.py # offline: notebooks are clean, every public function is in the API reference
|
|
113
119
|
python tests/test_server_contract.py # online: does the server answer everything the toolkit needs?
|
|
114
|
-
python tests/test_server_contract.py
|
|
120
|
+
python tests/test_server_contract.py <api root> # check another server
|
|
115
121
|
```
|
|
116
122
|
|
|
117
|
-
GitHub Actions runs these, and installs the package on Python 3.9-3.12, on every push. Maintainers: see [RELEASING.md](RELEASING.md) for how a release is published.
|
|
123
|
+
GitHub Actions runs these, and installs the package on Python 3.9-3.12, on every push. Maintainers: see [RELEASING.md](RELEASING.md) for how a release, and its documentation, is published.
|
|
118
124
|
|
|
119
125
|
`test_server_contract.py` checks status codes and the shape of each response (the columns and file types the toolkit reads). It only samples the large reference files, it does not download them.
|
|
120
126
|
|
|
@@ -17,6 +17,9 @@ discotoolkit.egg-info/SOURCES.txt
|
|
|
17
17
|
discotoolkit.egg-info/dependency_links.txt
|
|
18
18
|
discotoolkit.egg-info/requires.txt
|
|
19
19
|
discotoolkit.egg-info/top_level.txt
|
|
20
|
+
tests/test_docs.py
|
|
21
|
+
tests/test_gene_search.py
|
|
22
|
+
tests/test_reference_loading.py
|
|
20
23
|
tests/test_server_contract.py
|
|
21
24
|
tests/test_settings.py
|
|
22
25
|
tests/test_version.py
|
|
@@ -6,7 +6,7 @@ with open("requirements.txt", "r") as f:
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name='discotoolkit',
|
|
9
|
-
version="1.2.
|
|
9
|
+
version="1.2.2",
|
|
10
10
|
url='https://disco.bii.a-star.edu.sg/',
|
|
11
11
|
author='Li Mengwei, Rom Uddamvathanak',
|
|
12
12
|
author_email='uddamvathanak_rom@immunol.a-star.edu.sg',
|
|
@@ -17,4 +17,11 @@ setup(
|
|
|
17
17
|
include_package_data=True,
|
|
18
18
|
long_description=open("README.md", encoding="utf-8").read(),
|
|
19
19
|
long_description_content_type="text/markdown",
|
|
20
|
+
# shown on the PyPI page
|
|
21
|
+
project_urls={
|
|
22
|
+
"Documentation": "https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/",
|
|
23
|
+
"Source": "https://github.com/JinmiaoChenLab/DISCOtoolkit_py",
|
|
24
|
+
"Issues": "https://github.com/JinmiaoChenLab/DISCOtoolkit_py/issues",
|
|
25
|
+
"DISCO": "https://disco.bii.a-star.edu.sg/v1/",
|
|
26
|
+
},
|
|
20
27
|
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The documentation is built from this repository on every change. These checks keep it honest and
|
|
3
|
+
need no network and no third-party packages:
|
|
4
|
+
|
|
5
|
+
* the committed notebooks carry no saved output (the build re-runs them against the live
|
|
6
|
+
database, so a saved output could only ever be stale);
|
|
7
|
+
* every page in the navigation exists;
|
|
8
|
+
* every public function of the package appears in the API reference, so a new function cannot
|
|
9
|
+
ship undocumented.
|
|
10
|
+
|
|
11
|
+
python tests/test_docs.py
|
|
12
|
+
"""
|
|
13
|
+
import ast
|
|
14
|
+
import glob
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
|
21
|
+
|
|
22
|
+
# helpers that are not part of the public interface
|
|
23
|
+
INTERNAL = {
|
|
24
|
+
"api_url", "check_in_list", "generate_correlation_map", "get_disco_metadata", "get_json",
|
|
25
|
+
"get_sample_ct_info", "write_10X_h5",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read(path):
|
|
30
|
+
with open(os.path.join(ROOT, path), encoding="utf-8") as handle:
|
|
31
|
+
return handle.read()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_committed_notebooks_have_no_saved_output():
|
|
35
|
+
dirty = []
|
|
36
|
+
for path in sorted(glob.glob(os.path.join(ROOT, "docs", "*.ipynb"))):
|
|
37
|
+
for cell in json.load(open(path, encoding="utf-8"))["cells"]:
|
|
38
|
+
if cell["cell_type"] == "code" and (cell.get("outputs") or cell.get("execution_count")):
|
|
39
|
+
dirty.append(os.path.basename(path))
|
|
40
|
+
break
|
|
41
|
+
assert not dirty, "run `python tools/execute_notebooks.py --clear` on: %s" % ", ".join(dirty)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_every_page_in_the_navigation_exists():
|
|
45
|
+
nav = read("mkdocs.yml").split("nav:")[1].split("\nplugins:")[0]
|
|
46
|
+
pages = re.findall(r":\s+([\w./-]+\.(?:md|ipynb))\s*$", nav, re.M)
|
|
47
|
+
assert pages, "found no pages in mkdocs.yml"
|
|
48
|
+
missing = [p for p in pages if not os.path.exists(os.path.join(ROOT, "docs", p))]
|
|
49
|
+
assert not missing, "in mkdocs.yml but not in docs/: %s" % missing
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_every_public_function_is_in_the_api_reference():
|
|
53
|
+
api = read("docs/API.md")
|
|
54
|
+
documented = set(re.findall(r"^:::\s+discotoolkit\.\w+\.(\w+)", api, re.M))
|
|
55
|
+
public = set()
|
|
56
|
+
for path in glob.glob(os.path.join(ROOT, "discotoolkit", "*.py")):
|
|
57
|
+
tree = ast.parse(open(path, encoding="utf-8").read())
|
|
58
|
+
for node in tree.body:
|
|
59
|
+
if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and not node.name.startswith("_"):
|
|
60
|
+
public.add(node.name)
|
|
61
|
+
undocumented = sorted(public - documented - INTERNAL)
|
|
62
|
+
assert not undocumented, (
|
|
63
|
+
"not in docs/API.md (add `::: discotoolkit.<Module>.<name>`, or list it as internal in "
|
|
64
|
+
"tests/test_docs.py): %s" % undocumented
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_api_reference_names_only_things_that_exist():
|
|
69
|
+
api = read("docs/API.md")
|
|
70
|
+
for module, name in re.findall(r"^:::\s+discotoolkit\.(\w+)\.(\w+)", api, re.M):
|
|
71
|
+
source = read("discotoolkit/%s.py" % module)
|
|
72
|
+
assert re.search(r"^(def|class)\s+%s\b" % name, source, re.M), "%s.%s is gone" % (module, name)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_tutorials_link_to_pages_not_to_notebook_files_and_not_to_the_v2_site():
|
|
76
|
+
problems = []
|
|
77
|
+
for path in sorted(glob.glob(os.path.join(ROOT, "docs", "*.ipynb"))):
|
|
78
|
+
text = "\n".join("".join(c["source"]) for c in json.load(open(path, encoding="utf-8"))["cells"])
|
|
79
|
+
name = os.path.basename(path)
|
|
80
|
+
# a relative link to another .ipynb works on GitHub but is a dead link on the site
|
|
81
|
+
for target in re.findall(r"\]\(([^)\s]+)\)", text):
|
|
82
|
+
if target.endswith(".ipynb") and not target.startswith("http"):
|
|
83
|
+
problems.append("%s links to the file %s; link to the page on the docs site" % (name, target))
|
|
84
|
+
if "immunesinglecell.org" in text:
|
|
85
|
+
problems.append("%s sends readers to the DISCO V2 site; V1 is the supported server" % name)
|
|
86
|
+
assert not problems, "; ".join(problems)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_the_changelog_has_notes_for_the_current_version():
|
|
90
|
+
# the release workflow builds the GitHub Release text from it, and refuses a version without notes
|
|
91
|
+
version = re.search(r'__version__\s*=\s*"([^"]+)"', read("discotoolkit/__init__.py")).group(1)
|
|
92
|
+
assert re.search(r"^## %s\b" % re.escape(version), read("CHANGELOG.md"), re.M), (
|
|
93
|
+
"add a '## %s' section to CHANGELOG.md before releasing" % version
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _run_all():
|
|
98
|
+
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
|
|
99
|
+
failed = 0
|
|
100
|
+
for name, fn in tests:
|
|
101
|
+
try:
|
|
102
|
+
fn()
|
|
103
|
+
print(" PASS %s" % name)
|
|
104
|
+
except Exception as error:
|
|
105
|
+
failed += 1
|
|
106
|
+
print(" FAIL %s\n %s: %s" % (name, type(error).__name__, error))
|
|
107
|
+
print("\n%d/%d passed" % (len(tests) - failed, len(tests)))
|
|
108
|
+
return 1 if failed else 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(_run_all())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gene_search draws with seaborn and matplotlib, which change between releases (a plot argument
|
|
3
|
+
that was ignored in one release is an error in the next). This runs the real plotting code on a
|
|
4
|
+
fake server answer, so a library upgrade that breaks it fails here instead of in a user's notebook.
|
|
5
|
+
|
|
6
|
+
python tests/test_gene_search.py
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import matplotlib
|
|
12
|
+
|
|
13
|
+
matplotlib.use("Agg")
|
|
14
|
+
import matplotlib.pyplot as plt # noqa: E402
|
|
15
|
+
from unittest import mock # noqa: E402
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
18
|
+
from discotoolkit import GeneSearch # noqa: E402
|
|
19
|
+
|
|
20
|
+
# [semicolon-separated values, cell type, (unused), atlas]
|
|
21
|
+
ROWS = [
|
|
22
|
+
["10;12;11;13;9;14", "T cell", "x", "thymus"],
|
|
23
|
+
["30;28;31;29;33;27", "Cardiomyocyte", "x", "heart"],
|
|
24
|
+
["3;4;2;5;3;4", "Fibroblast", "x", "heart"],
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FakeResponse:
|
|
29
|
+
def raise_for_status(self):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def json(self):
|
|
33
|
+
return ROWS
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_gene_search_draws_a_figure_for_all_atlases_and_for_a_subset():
|
|
37
|
+
with mock.patch.object(GeneSearch.requests, "get", return_value=FakeResponse()):
|
|
38
|
+
for atlas in (None, ["heart"], "thymus"):
|
|
39
|
+
plt.close("all")
|
|
40
|
+
drawn = []
|
|
41
|
+
with mock.patch.object(plt, "show", lambda *a, **k: drawn.append(plt.gcf())):
|
|
42
|
+
assert GeneSearch.gene_search("CD4", atlas=atlas, figsize=(6, 4), dpi=60) is None
|
|
43
|
+
assert len(drawn) == 1 and drawn[0].axes, "no figure for atlas=%r" % (atlas,)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_all():
|
|
47
|
+
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
|
|
48
|
+
failed = 0
|
|
49
|
+
for name, fn in tests:
|
|
50
|
+
try:
|
|
51
|
+
fn()
|
|
52
|
+
print(" PASS %s" % name)
|
|
53
|
+
except Exception as error:
|
|
54
|
+
failed += 1
|
|
55
|
+
print(" FAIL %s\n %s: %s" % (name, type(error).__name__, error))
|
|
56
|
+
print("\n%d/%d passed" % (len(tests) - failed, len(tests)))
|
|
57
|
+
return 1 if failed else 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
sys.exit(_run_all())
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_load_reference: the CELLiD reference tables are downloaded once, as CSV when the server has it,
|
|
3
|
+
and fall back to the old pickle when it does not. No network: the server is faked.
|
|
4
|
+
|
|
5
|
+
python tests/test_reference_loading.py
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
15
|
+
from discotoolkit import CELLiD # noqa: E402
|
|
16
|
+
|
|
17
|
+
CSV = b'"","T cell--atlas_a","B cell--atlas_a"\n"CD4",1.5,0.0\n"MS4A1",0.0,2.5\n'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FakeResponse:
|
|
21
|
+
def __init__(self, status, body=b""):
|
|
22
|
+
self.status_code = status
|
|
23
|
+
self.content = body
|
|
24
|
+
|
|
25
|
+
def __enter__(self):
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, *args):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def iter_content(self, chunk_size=None):
|
|
32
|
+
yield self.content
|
|
33
|
+
|
|
34
|
+
def raise_for_status(self):
|
|
35
|
+
if self.status_code >= 400:
|
|
36
|
+
raise RuntimeError("HTTP %s" % self.status_code)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def server(csv_status, calls):
|
|
40
|
+
def get(url, params=None, **kwargs):
|
|
41
|
+
calls.append((url.rsplit("/", 1)[-1], (params or {}).get("type")))
|
|
42
|
+
if (params or {}).get("type") == "csv":
|
|
43
|
+
return FakeResponse(csv_status, CSV if csv_status == 200 else b"")
|
|
44
|
+
# the pickle fallback: a gzip pickle of a small table
|
|
45
|
+
path = os.path.join(tempfile.gettempdir(), "disco_fake.pkl")
|
|
46
|
+
pd.DataFrame({"gene": ["CD4"], "group": ["T cell--a"]}).to_pickle(
|
|
47
|
+
path, compression={"method": "gzip", "compresslevel": 6}
|
|
48
|
+
)
|
|
49
|
+
return FakeResponse(200, open(path, "rb").read())
|
|
50
|
+
|
|
51
|
+
return get
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_csv_is_downloaded_once_and_read_with_genes_as_index():
|
|
55
|
+
calls = []
|
|
56
|
+
with tempfile.TemporaryDirectory() as d, mock.patch.object(CELLiD.requests, "get", server(200, calls)):
|
|
57
|
+
first = CELLiD._load_reference(d, "ref_data", "getRef", index_col=0)
|
|
58
|
+
again = CELLiD._load_reference(d, "ref_data", "getRef", index_col=0)
|
|
59
|
+
assert list(first.index) == ["CD4", "MS4A1"] and list(first.columns)[0] == "T cell--atlas_a"
|
|
60
|
+
assert first.equals(again)
|
|
61
|
+
assert calls == [("getRef", "csv")], "second call must use the saved file: %s" % calls
|
|
62
|
+
assert not os.path.exists(os.path.join(d, "ref_data.csv.part"))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_old_server_without_csv_falls_back_to_the_pickle():
|
|
66
|
+
calls = []
|
|
67
|
+
with tempfile.TemporaryDirectory() as d, mock.patch.object(CELLiD.requests, "get", server(400, calls)):
|
|
68
|
+
deg = CELLiD._load_reference(d, "ref_deg", "getRefDeg")
|
|
69
|
+
assert list(deg.columns) == ["gene", "group"]
|
|
70
|
+
assert calls == [("getRefDeg", "csv"), ("getRefDeg", "pkl")], calls
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_a_pickle_saved_by_an_earlier_version_is_still_used():
|
|
74
|
+
calls = []
|
|
75
|
+
with tempfile.TemporaryDirectory() as d, mock.patch.object(CELLiD.requests, "get", server(200, calls)):
|
|
76
|
+
pd.DataFrame({"gene": ["X"], "group": ["g"]}).to_pickle(
|
|
77
|
+
os.path.join(d, "ref_deg.pkl"), compression={"method": "gzip", "compresslevel": 6}
|
|
78
|
+
)
|
|
79
|
+
deg = CELLiD._load_reference(d, "ref_deg", "getRefDeg")
|
|
80
|
+
assert list(deg["gene"]) == ["X"] and calls == [], "must not touch the network"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _run_all():
|
|
84
|
+
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
|
|
85
|
+
failed = 0
|
|
86
|
+
for name, fn in tests:
|
|
87
|
+
try:
|
|
88
|
+
fn()
|
|
89
|
+
print(" PASS %s" % name)
|
|
90
|
+
except Exception as error:
|
|
91
|
+
failed += 1
|
|
92
|
+
print(" FAIL %s\n %s: %s" % (name, type(error).__name__, error))
|
|
93
|
+
print("\n%d/%d passed" % (len(tests) - failed, len(tests)))
|
|
94
|
+
return 1 if failed else 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
sys.exit(_run_all())
|
|
@@ -5,7 +5,7 @@ Needs only `requests`, so it runs anywhere -- including a fresh Colab notebook:
|
|
|
5
5
|
|
|
6
6
|
!pip install requests
|
|
7
7
|
!python tests/test_server_contract.py # DISCO v1 (default)
|
|
8
|
-
!python tests/test_server_contract.py
|
|
8
|
+
!python tests/test_server_contract.py <api root> # another server
|
|
9
9
|
|
|
10
10
|
or with pytest: DISCO_API_URL=<api root> pytest tests/test_server_contract.py
|
|
11
11
|
|
|
@@ -114,6 +114,19 @@ def test_cellid_deg_reference_is_a_gzip_pickle():
|
|
|
114
114
|
assert head[:2] == b"\x1f\x8b", "getRefDeg is not gzip data: %r" % head
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def test_cellid_reference_is_available_as_plain_csv():
|
|
118
|
+
header, row = first_lines("toolkit/getRef", 2, type="csv")
|
|
119
|
+
cols = [c.strip('"') for c in header.split(",")]
|
|
120
|
+
assert cols[0] == "" and len(cols) > 1000, "expected genes x (cell type--atlas) columns"
|
|
121
|
+
assert all("--" in c for c in cols[1:5]), cols[1:5]
|
|
122
|
+
assert row.split(",")[0].strip('"'), "rows should start with a gene name"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_cellid_deg_reference_is_available_as_plain_csv():
|
|
126
|
+
header, row = first_lines("toolkit/getRefDeg", 2, type="csv")
|
|
127
|
+
assert [c.strip('"') for c in header.split(",")] == ["gene", "group"], header
|
|
128
|
+
|
|
129
|
+
|
|
117
130
|
def test_gene_set_reference_is_available_as_plain_csv():
|
|
118
131
|
header, row = first_lines("toolkit/getGeneSet", 2, format="csv")
|
|
119
132
|
cols = [c.strip('"') for c in header.split(",")]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|