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.
Files changed (27) hide show
  1. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/PKG-INFO +14 -8
  2. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/README.md +8 -7
  3. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/CELLiD.py +52 -37
  4. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/DiscoClass.py +29 -17
  5. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/DownloadDiscoData.py +9 -5
  6. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/GeneSearch.py +5 -5
  7. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/GetMetadata.py +14 -14
  8. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/GlobalVariable.py +3 -2
  9. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/__init__.py +1 -1
  10. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/PKG-INFO +14 -8
  11. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/SOURCES.txt +3 -0
  12. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/setup.py +8 -1
  13. discotoolkit-1.2.2/tests/test_docs.py +112 -0
  14. discotoolkit-1.2.2/tests/test_gene_search.py +61 -0
  15. discotoolkit-1.2.2/tests/test_reference_loading.py +98 -0
  16. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/tests/test_server_contract.py +14 -1
  17. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/MANIFEST.in +0 -0
  18. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/CellMapper.py +0 -0
  19. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit/Utilities.py +0 -0
  20. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/dependency_links.txt +0 -0
  21. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/requires.txt +0 -0
  22. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/discotoolkit.egg-info/top_level.txt +0 -0
  23. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/pyproject.toml +0 -0
  24. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/requirements.txt +0 -0
  25. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/setup.cfg +0 -0
  26. {discotoolkit-1.2.0 → discotoolkit-1.2.2}/tests/test_settings.py +0 -0
  27. {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.0
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
- [![Documentation Status](https://readthedocs.org/projects/discotoolkit-py/badge/?version=latest)](https://discotoolkit-py.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://static.pepy.tech/personalized-badge/discotoolkit?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/discotoolkit) [![PyPI version](https://img.shields.io/pypi/v/discotoolkit)](https://pypi.org/project/discotoolkit)
44
+ [![Documentation](https://img.shields.io/badge/docs-disco.bii.a--star.edu.sg-blue)](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/) [![Downloads](https://static.pepy.tech/personalized-badge/discotoolkit?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/discotoolkit) [![PyPI version](https://img.shields.io/pypi/v/discotoolkit)](https://pypi.org/project/discotoolkit)
40
45
 
41
- # DISCOtoolkit 1.2.0
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 [discotoolkit-py.readthedocs.io](https://discotoolkit-py.readthedocs.io/en/latest/).
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 and v2.** The toolkit talks to **DISCO v1** by default. DISCO v2 has its own R package ([DISCOtoolkit](https://github.com/JinmiaoChenLab/DISCOtoolkit)); the Python package can still be pointed at a v2 server, see [Choosing a server](#choosing-a-server).
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 https://immunesinglecell.org/disco_v3_api/ # check another server
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
- [![Documentation Status](https://readthedocs.org/projects/discotoolkit-py/badge/?version=latest)](https://discotoolkit-py.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://static.pepy.tech/personalized-badge/discotoolkit?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/discotoolkit) [![PyPI version](https://img.shields.io/pypi/v/discotoolkit)](https://pypi.org/project/discotoolkit)
9
+ [![Documentation](https://img.shields.io/badge/docs-disco.bii.a--star.edu.sg-blue)](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/) [![Downloads](https://static.pepy.tech/personalized-badge/discotoolkit?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/discotoolkit) [![PyPI version](https://img.shields.io/pypi/v/discotoolkit)](https://pypi.org/project/discotoolkit)
10
10
 
11
- # DISCOtoolkit 1.2.0
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 [discotoolkit-py.readthedocs.io](https://discotoolkit-py.readthedocs.io/en/latest/).
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 and v2.** The toolkit talks to **DISCO v1** by default. DISCO v2 has its own R package ([DISCOtoolkit](https://github.com/JinmiaoChenLab/DISCOtoolkit)); the Python package can still be pointed at a v2 server, see [Choosing a server](#choosing-a-server).
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 https://immunesinglecell.org/disco_v3_api/ # check another server
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
- List: return list of string
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 in pickle extension and read as pandas dataframe
75
+ # download the reference data from the server (once) and read it as a pandas dataframe
76
76
  if ref_data is None:
77
- if not (os.path.exists(ref_path + "/ref_data.pkl")):
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 in pickle extension and read as pandas dataframe
211
+ # download the reference data from the server (once) and read it as a pandas dataframe
179
212
  if ref_data is None:
180
- if not (os.path.exists(ref_path + "/ref_data.pkl")):
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
- if not (os.path.exists(ref_path + "/ref_deg.pkl")):
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
- Filter class object to save the attributes for filtering the dataset from DISCO
8
-
9
- sample_id String e.g. ERX2757110;
10
- project_id String;
11
- tissue String e.g. lung, bladder;
12
- disease String e.g. COVID-19;
13
- platform String e.g. 10x3';
14
- sample_type String;
15
- cell_type String;
16
- cell_type_confidence String e.g. high;
17
- include_cell_type_children Bool e.g. True;
18
- min_cell_per_sample Int e.g. 300;
19
-
20
- return Class object
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
- Wrapper class on top of the Filter object to get dataset's summary such as cell count and sample count and the metadata
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
- """function to download the data based on the given filter
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) : FilterData class to filters data from DISCO database
29
- output_dir (Sting) : directory for storing the downloaded data. Default DISCOtmp
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: This function does not return any object and instead download the data for the user
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 paletteasdasd
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, plot_kws={'alpha':0.1},
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 (Dict) = cell_ontology can be provided by the user in the format of dictionary datatype in python. Defaults to None.
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
- Celltype List: List of matched celltype
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
- List of String: return the children of the defined celltype in list of String
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
- List: return the list of unique element in the metadata columns to the users as reference
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
- List: return the name of the metadata in the form of list of string
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
- """filter function option for the disco data
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
- Filter (Class): predefined Filter class with default attribute to filter data for the user
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 (Class): return the FilterData object which will then be used to filter and download data
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). To use
6
- another server:
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
@@ -5,4 +5,4 @@ from .CELLiD import *
5
5
  from .GeneSearch import *
6
6
  from .GlobalVariable import set_server, get_server
7
7
 
8
- __version__ = "1.2.0"
8
+ __version__ = "1.2.2"
@@ -1,10 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: discotoolkit
3
- Version: 1.2.0
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
- [![Documentation Status](https://readthedocs.org/projects/discotoolkit-py/badge/?version=latest)](https://discotoolkit-py.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://static.pepy.tech/personalized-badge/discotoolkit?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/discotoolkit) [![PyPI version](https://img.shields.io/pypi/v/discotoolkit)](https://pypi.org/project/discotoolkit)
44
+ [![Documentation](https://img.shields.io/badge/docs-disco.bii.a--star.edu.sg-blue)](https://disco.bii.a-star.edu.sg/v1/docs/toolkit/guide/) [![Downloads](https://static.pepy.tech/personalized-badge/discotoolkit?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/discotoolkit) [![PyPI version](https://img.shields.io/pypi/v/discotoolkit)](https://pypi.org/project/discotoolkit)
40
45
 
41
- # DISCOtoolkit 1.2.0
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 [discotoolkit-py.readthedocs.io](https://discotoolkit-py.readthedocs.io/en/latest/).
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 and v2.** The toolkit talks to **DISCO v1** by default. DISCO v2 has its own R package ([DISCOtoolkit](https://github.com/JinmiaoChenLab/DISCOtoolkit)); the Python package can still be pointed at a v2 server, see [Choosing a server](#choosing-a-server).
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 https://immunesinglecell.org/disco_v3_api/ # check another server
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.0",
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 https://immunesinglecell.org/disco_v3_api/ # another server
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