resolutiontree 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- resolutiontree/__init__.py +14 -0
- resolutiontree/core.py +1331 -0
- resolutiontree/utils.py +377 -0
- resolutiontree-0.1.0.dist-info/METADATA +29 -0
- resolutiontree-0.1.0.dist-info/RECORD +8 -0
- resolutiontree-0.1.0.dist-info/WHEEL +5 -0
- resolutiontree-0.1.0.dist-info/licenses/LICENSE +0 -0
- resolutiontree-0.1.0.dist-info/top_level.txt +1 -0
resolutiontree/utils.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sys
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Literal
|
|
6
|
+
from anndata import AnnData
|
|
7
|
+
from scanpy.tools._rank_genes_groups import rank_genes_groups
|
|
8
|
+
|
|
9
|
+
def find_cluster_specific_genes(
|
|
10
|
+
adata: AnnData,
|
|
11
|
+
resolutions: Sequence[float],
|
|
12
|
+
*,
|
|
13
|
+
prefix: str = "leiden_res_",
|
|
14
|
+
method: Literal["wilcoxon"] = "wilcoxon",
|
|
15
|
+
n_top_genes: int = 3,
|
|
16
|
+
min_cells: int = 2,
|
|
17
|
+
deg_mode: Literal["within_parent", "per_resolution"] = "within_parent",
|
|
18
|
+
verbose: bool = False,
|
|
19
|
+
) -> dict[tuple[str, str], list[str]]:
|
|
20
|
+
"""Find differentially expressed genes for clusters in two modes."""
|
|
21
|
+
|
|
22
|
+
if deg_mode not in ["within_parent", "per_resolution"]:
|
|
23
|
+
msg = "deg_mode must be 'within_parent' or 'per_resolution'"
|
|
24
|
+
raise ValueError(msg)
|
|
25
|
+
|
|
26
|
+
# Validate resolutions and clustering columns
|
|
27
|
+
for res in resolutions:
|
|
28
|
+
col = f"{prefix}{res}"
|
|
29
|
+
if col not in adata.obs:
|
|
30
|
+
msg = f"Column {col} not found in adata.obs"
|
|
31
|
+
raise ValueError(msg)
|
|
32
|
+
|
|
33
|
+
top_genes_dict: dict[tuple[str, str], list[str]] = {}
|
|
34
|
+
|
|
35
|
+
if deg_mode == "within_parent":
|
|
36
|
+
top_genes_dict.update(
|
|
37
|
+
find_within_parent_degs(
|
|
38
|
+
adata,
|
|
39
|
+
resolutions,
|
|
40
|
+
prefix=prefix,
|
|
41
|
+
n_top_genes=n_top_genes,
|
|
42
|
+
min_cells=min_cells,
|
|
43
|
+
rank_genes_groups=rank_genes_groups,
|
|
44
|
+
verbose=verbose,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
elif deg_mode == "per_resolution":
|
|
48
|
+
top_genes_dict.update(
|
|
49
|
+
find_per_resolution_degs(
|
|
50
|
+
adata,
|
|
51
|
+
resolutions,
|
|
52
|
+
prefix=prefix,
|
|
53
|
+
n_top_genes=n_top_genes,
|
|
54
|
+
min_cells=min_cells,
|
|
55
|
+
rank_genes_groups=rank_genes_groups,
|
|
56
|
+
verbose=verbose,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return top_genes_dict
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_within_parent_degs(
|
|
64
|
+
adata: AnnData,
|
|
65
|
+
resolutions: Sequence[float],
|
|
66
|
+
*,
|
|
67
|
+
prefix: str,
|
|
68
|
+
n_top_genes: int,
|
|
69
|
+
min_cells: int,
|
|
70
|
+
rank_genes_groups,
|
|
71
|
+
verbose: bool = False,
|
|
72
|
+
) -> dict[tuple[str, str], list[str]]:
|
|
73
|
+
top_genes_dict = {}
|
|
74
|
+
|
|
75
|
+
for i, res in enumerate(resolutions[:-1]):
|
|
76
|
+
res_key = f"{prefix}{res}"
|
|
77
|
+
next_res_key = f"{prefix}{resolutions[i + 1]}"
|
|
78
|
+
clusters = adata.obs[res_key].cat.categories
|
|
79
|
+
|
|
80
|
+
for cluster in clusters:
|
|
81
|
+
cluster_mask = adata.obs[res_key] == cluster
|
|
82
|
+
cluster_adata = adata[cluster_mask, :]
|
|
83
|
+
|
|
84
|
+
subclusters = cluster_adata.obs[next_res_key].value_counts()
|
|
85
|
+
valid_subclusters = subclusters[subclusters >= min_cells].index
|
|
86
|
+
|
|
87
|
+
if len(valid_subclusters) < 2:
|
|
88
|
+
if verbose:
|
|
89
|
+
print(
|
|
90
|
+
f"Skipping res_{res}_C{cluster}: < 2 subclusters with >= {min_cells} cells."
|
|
91
|
+
)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
subcluster_mask = cluster_adata.obs[next_res_key].isin(valid_subclusters)
|
|
95
|
+
deg_adata = cluster_adata[subcluster_mask, :]
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
rank_genes_groups(deg_adata, groupby=next_res_key, method="wilcoxon")
|
|
99
|
+
for subcluster in valid_subclusters:
|
|
100
|
+
names = deg_adata.uns["rank_genes_groups"]["names"][subcluster]
|
|
101
|
+
scores = deg_adata.uns["rank_genes_groups"]["scores"][subcluster]
|
|
102
|
+
top_genes = [
|
|
103
|
+
name
|
|
104
|
+
for name, score in zip(names, scores, strict=False)
|
|
105
|
+
if score > 0
|
|
106
|
+
][:n_top_genes]
|
|
107
|
+
parent_node = f"res_{res}_C{cluster}"
|
|
108
|
+
child_node = f"res_{resolutions[i + 1]}_C{subcluster}"
|
|
109
|
+
top_genes_dict[(parent_node, child_node)] = top_genes
|
|
110
|
+
if verbose:
|
|
111
|
+
print(f"{parent_node} -> {child_node}: {top_genes}")
|
|
112
|
+
except KeyError as e:
|
|
113
|
+
print(f"Key error when processing {parent_node} -> {child_node}: {e}")
|
|
114
|
+
continue
|
|
115
|
+
except TypeError as e:
|
|
116
|
+
print(
|
|
117
|
+
f"Type error with the data when processing {parent_node} -> {child_node}: {e}"
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
return top_genes_dict
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def find_per_resolution_degs(
|
|
125
|
+
adata: AnnData,
|
|
126
|
+
resolutions: Sequence[float],
|
|
127
|
+
*,
|
|
128
|
+
prefix: str,
|
|
129
|
+
n_top_genes: int,
|
|
130
|
+
min_cells: int,
|
|
131
|
+
rank_genes_groups,
|
|
132
|
+
verbose: bool = False,
|
|
133
|
+
) -> dict[tuple[str, str], list[str]]:
|
|
134
|
+
top_genes_dict = {}
|
|
135
|
+
|
|
136
|
+
for i, res in enumerate(resolutions[1:], 1):
|
|
137
|
+
res_key = f"{prefix}{res}"
|
|
138
|
+
prev_res_key = f"{prefix}{resolutions[i - 1]}"
|
|
139
|
+
clusters = adata.obs[res_key].cat.categories
|
|
140
|
+
valid_clusters = [
|
|
141
|
+
c for c in clusters if (adata.obs[res_key] == c).sum() >= min_cells
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
if not valid_clusters:
|
|
145
|
+
if verbose:
|
|
146
|
+
print(
|
|
147
|
+
f"Skipping resolution {res}: no clusters with >= {min_cells} cells."
|
|
148
|
+
)
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
deg_adata = adata[adata.obs[res_key].isin(valid_clusters), :]
|
|
152
|
+
try:
|
|
153
|
+
rank_genes_groups(
|
|
154
|
+
deg_adata, groupby=res_key, method="wilcoxon", reference="rest"
|
|
155
|
+
)
|
|
156
|
+
for cluster in valid_clusters:
|
|
157
|
+
names = deg_adata.uns["rank_genes_groups"]["names"][cluster]
|
|
158
|
+
scores = deg_adata.uns["rank_genes_groups"]["scores"][cluster]
|
|
159
|
+
top_genes = [
|
|
160
|
+
name
|
|
161
|
+
for name, score in zip(names, scores, strict=False)
|
|
162
|
+
if score > 0
|
|
163
|
+
][:n_top_genes]
|
|
164
|
+
parent_cluster = adata.obs[deg_adata.obs[res_key] == cluster][
|
|
165
|
+
prev_res_key
|
|
166
|
+
].mode()[0]
|
|
167
|
+
parent_node = f"res_{resolutions[i - 1]}_C{parent_cluster}"
|
|
168
|
+
child_node = f"res_{res}_C{cluster}"
|
|
169
|
+
top_genes_dict[(parent_node, child_node)] = top_genes
|
|
170
|
+
if verbose:
|
|
171
|
+
print(f"{parent_node} -> {child_node}: {top_genes}")
|
|
172
|
+
except KeyError as e:
|
|
173
|
+
print(f"Key error when processing {parent_node} -> {child_node}: {e}")
|
|
174
|
+
continue
|
|
175
|
+
except TypeError as e:
|
|
176
|
+
print(
|
|
177
|
+
f"Type error with the data when processing {parent_node} -> {child_node}: {e}"
|
|
178
|
+
)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
return top_genes_dict
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cluster_resolution_finder(
|
|
185
|
+
adata: AnnData,
|
|
186
|
+
resolutions: list[float],
|
|
187
|
+
*,
|
|
188
|
+
prefix: str = "leiden_res_",
|
|
189
|
+
method: Literal["wilcoxon"] = "wilcoxon",
|
|
190
|
+
n_top_genes: int = 3,
|
|
191
|
+
min_cells: int = 2,
|
|
192
|
+
deg_mode: Literal["within_parent", "per_resolution"] = "within_parent",
|
|
193
|
+
flavor: Literal["igraph"] = "igraph",
|
|
194
|
+
n_iterations: int = 2,
|
|
195
|
+
inplace: bool = False,
|
|
196
|
+
verbose: bool = False,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Find clusters across multiple resolutions and identify cluster-specific genes.
|
|
200
|
+
|
|
201
|
+
This function performs Leiden clustering at specified resolutions, identifies
|
|
202
|
+
differentially expressed genes (DEGs) for clusters, and stores the results in `adata`.
|
|
203
|
+
|
|
204
|
+
Params
|
|
205
|
+
------
|
|
206
|
+
adata
|
|
207
|
+
The annotated data matrix.
|
|
208
|
+
resolutions
|
|
209
|
+
List of resolution values for Leiden clustering (e.g., [0.0, 0.2, 0.5]).
|
|
210
|
+
prefix
|
|
211
|
+
Prefix for clustering keys in `adata.obs` (e.g., "leiden_res_").
|
|
212
|
+
method
|
|
213
|
+
Method for differential expression analysis: only "wilcoxon" is supported.
|
|
214
|
+
n_top_genes
|
|
215
|
+
Number of top genes to identify per child cluster.
|
|
216
|
+
min_cells
|
|
217
|
+
Minimum number of cells required in a subcluster to include it.
|
|
218
|
+
deg_mode
|
|
219
|
+
Mode for DEG analysis: "within_parent" (compare child to parent cluster) or
|
|
220
|
+
"per_resolution" (compare within each resolution).
|
|
221
|
+
flavor
|
|
222
|
+
Flavor of Leiden clustering: only "igraph" is supported.
|
|
223
|
+
n_iterations
|
|
224
|
+
Number of iterations for Leiden clustering.
|
|
225
|
+
inplace
|
|
226
|
+
If True, modifies `adata` in place. If False, returns a new AnnData object.
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
adata
|
|
231
|
+
If `inplace` is False, returns a new AnnData object with clustering results.
|
|
232
|
+
|
|
233
|
+
The following annotations are added to `adata`:
|
|
234
|
+
|
|
235
|
+
leiden_res_{resolution}
|
|
236
|
+
Cluster assignments for each resolution in `adata.obs`.
|
|
237
|
+
cluster_resolution_top_genes
|
|
238
|
+
Dictionary mapping (parent_node, child_node) pairs to lists of top marker genes,
|
|
239
|
+
stored in `adata.uns`.
|
|
240
|
+
|
|
241
|
+
Notes
|
|
242
|
+
-----
|
|
243
|
+
This function requires the `igraph` library for Leiden clustering, which is included in the
|
|
244
|
+
`leiden` extra. Install it with: ``pip install scanpy[leiden]``.
|
|
245
|
+
|
|
246
|
+
Requires `sc.pp.neighbors` to be run on `adata` beforehand.
|
|
247
|
+
|
|
248
|
+
Examples
|
|
249
|
+
--------
|
|
250
|
+
>>> import scanpy as sc
|
|
251
|
+
>>> adata = sc.datasets.pbmc68k()
|
|
252
|
+
>>> sc.pp.neighbors(adata)
|
|
253
|
+
>>> sc.tl.find_cluster_resolution(adata, resolutions=[0.0, 0.5])
|
|
254
|
+
>>> sc.pl.cluster_decision_tree(adata, resolutions=[0.0, 0.5])
|
|
255
|
+
"""
|
|
256
|
+
import io
|
|
257
|
+
|
|
258
|
+
from scanpy.tools import leiden
|
|
259
|
+
|
|
260
|
+
# Suppress prints if pytest is running
|
|
261
|
+
if "pytest" in sys.modules:
|
|
262
|
+
sys.stdout = io.StringIO()
|
|
263
|
+
|
|
264
|
+
_validate_cluster_resolution_inputs(adata, resolutions, method, flavor)
|
|
265
|
+
|
|
266
|
+
# Run Leiden clustering
|
|
267
|
+
for resolution in resolutions:
|
|
268
|
+
res_key = f"{prefix}{resolution}"
|
|
269
|
+
try:
|
|
270
|
+
leiden(
|
|
271
|
+
adata,
|
|
272
|
+
resolution=resolution,
|
|
273
|
+
flavor="igraph",
|
|
274
|
+
n_iterations=n_iterations,
|
|
275
|
+
key_added=res_key,
|
|
276
|
+
)
|
|
277
|
+
if "pytest" not in sys.modules and not hasattr(
|
|
278
|
+
sys, "_called_from_test"
|
|
279
|
+
): # Suppress print in tests
|
|
280
|
+
print(f"Completed Leiden clustering for resolution {resolution}")
|
|
281
|
+
except ValueError as e:
|
|
282
|
+
msg = f"Leiden clustering failed at resolution {resolution} due to invalid value: {e}"
|
|
283
|
+
raise RuntimeError(msg) from None
|
|
284
|
+
except TypeError as e:
|
|
285
|
+
msg = f"Leiden clustering failed at resolution {resolution} due to incorrect type: {e}"
|
|
286
|
+
raise RuntimeError(msg) from None
|
|
287
|
+
except RuntimeError as e:
|
|
288
|
+
msg = f"Leiden clustering failed at resolution {resolution}: {e}"
|
|
289
|
+
raise RuntimeError(msg) from None
|
|
290
|
+
|
|
291
|
+
if not inplace:
|
|
292
|
+
adata = adata.copy()
|
|
293
|
+
|
|
294
|
+
# Find cluster-specific genes
|
|
295
|
+
top_genes_dict = find_cluster_specific_genes(
|
|
296
|
+
adata=adata,
|
|
297
|
+
resolutions=resolutions,
|
|
298
|
+
prefix=prefix,
|
|
299
|
+
method=method,
|
|
300
|
+
n_top_genes=n_top_genes,
|
|
301
|
+
min_cells=min_cells,
|
|
302
|
+
deg_mode=deg_mode,
|
|
303
|
+
verbose=verbose,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Create DataFrame for clusterDecisionTree
|
|
307
|
+
try:
|
|
308
|
+
cluster_data = pd.DataFrame(
|
|
309
|
+
{f"{prefix}{r}": adata.obs[f"{prefix}{r}"] for r in resolutions}
|
|
310
|
+
)
|
|
311
|
+
except KeyError as e:
|
|
312
|
+
msg = f"Failed to create cluster_data DataFrame: missing column {e}"
|
|
313
|
+
raise RuntimeError(msg) from None
|
|
314
|
+
except ValueError as e:
|
|
315
|
+
msg = f"Failed to create cluster_data DataFrame due to invalid value: {e}"
|
|
316
|
+
raise RuntimeError(msg) from None
|
|
317
|
+
except TypeError as e:
|
|
318
|
+
msg = f"Failed to create cluster_data DataFrame due to incorrect type: {e}"
|
|
319
|
+
raise RuntimeError(msg) from None
|
|
320
|
+
|
|
321
|
+
# Store the results in adata.uns
|
|
322
|
+
# adata.uns["cluster_resolution_top_genes"] = top_genes_dict
|
|
323
|
+
adata.uns["cluster_resolution_top_genes"] = _convert_tuple_keys(top_genes_dict)
|
|
324
|
+
adata.uns["cluster_resolution_cluster_data"] = cluster_data
|
|
325
|
+
|
|
326
|
+
return adata
|
|
327
|
+
|
|
328
|
+
def _validate_cluster_resolution_inputs(
|
|
329
|
+
adata: AnnData,
|
|
330
|
+
resolutions: Sequence[float],
|
|
331
|
+
method: str,
|
|
332
|
+
flavor: str,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Validate inputs for the find_cluster_resolution function."""
|
|
335
|
+
if not resolutions:
|
|
336
|
+
msg = "resolutions list cannot be empty"
|
|
337
|
+
raise ValueError(msg)
|
|
338
|
+
if not all(isinstance(r, int | float) and r >= 0 for r in resolutions):
|
|
339
|
+
msg = "All resolutions must be non-negative numbers"
|
|
340
|
+
raise ValueError(msg)
|
|
341
|
+
if method != "wilcoxon":
|
|
342
|
+
msg = "Only method='wilcoxon' is supported"
|
|
343
|
+
raise ValueError(msg)
|
|
344
|
+
if flavor != "igraph":
|
|
345
|
+
msg = "Only flavor='igraph' is supported"
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
if "neighbors" not in adata.uns:
|
|
348
|
+
msg = "adata must have precomputed neighbors (run sc.pp.neighbors first)."
|
|
349
|
+
raise ValueError(msg)
|
|
350
|
+
|
|
351
|
+
# Recursively convert tuple keys in .uns to strings
|
|
352
|
+
def _convert_tuple_keys(d, delimiter="_"):
|
|
353
|
+
if isinstance(d, dict):
|
|
354
|
+
new_dict = {}
|
|
355
|
+
for k, v in d.items():
|
|
356
|
+
if isinstance(k, tuple):
|
|
357
|
+
k = delimiter.join(k)
|
|
358
|
+
new_dict[k] = _convert_tuple_keys(v, delimiter=delimiter)
|
|
359
|
+
return new_dict
|
|
360
|
+
elif isinstance(d, list):
|
|
361
|
+
return [_convert_tuple_keys(i, delimiter=delimiter) for i in d]
|
|
362
|
+
else:
|
|
363
|
+
return d
|
|
364
|
+
|
|
365
|
+
# Recursively recover tuple keys in .uns from strings
|
|
366
|
+
def _recover_tuple_keys(d, delimiter="_"):
|
|
367
|
+
if isinstance(d, dict):
|
|
368
|
+
new_dict = {}
|
|
369
|
+
for k, v in d.items():
|
|
370
|
+
if isinstance(k, str) and delimiter in k:
|
|
371
|
+
k = tuple(k.split(delimiter))
|
|
372
|
+
new_dict[k] = _recover_tuple_keys(v, delimiter=delimiter)
|
|
373
|
+
return new_dict
|
|
374
|
+
elif isinstance(d, list):
|
|
375
|
+
return [_recover_tuple_keys(i, delimiter=delimiter) for i in d]
|
|
376
|
+
else:
|
|
377
|
+
return d
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: resolutiontree
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Systematic exploration of clustering resolutions in single-cell analysis
|
|
5
|
+
Home-page: https://github.com/joe-jhou2/resolutiontree
|
|
6
|
+
Author: Joe Hou
|
|
7
|
+
Author-email: Joe Hou <joseph.houjue@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/joe-jhou2/resolutiontree
|
|
10
|
+
Project-URL: Documentation, https://resolutiontree.readthedocs.io/
|
|
11
|
+
Project-URL: Repository, https://github.com/joe-jhou2/resolutiontree
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/joe-jhou2/resolutiontree/issues
|
|
13
|
+
Keywords: single-cell,clustering,resolution,scanpy,leiden,visualization
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
resolutiontree/__init__.py,sha256=Jjk3APrAMIyWHuchBXAH1dCN7gETacl81MacpkmwZ-0,305
|
|
2
|
+
resolutiontree/core.py,sha256=zrMzqvzyJwSS6kEfSIE1uRe3Spdufo1jkrvDYzb35h8,51597
|
|
3
|
+
resolutiontree/utils.py,sha256=pEzAghm0JFsuDyb13Fm8ncjBg0QhRy6wnuxZgDBhDNk,13339
|
|
4
|
+
resolutiontree-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
resolutiontree-0.1.0.dist-info/METADATA,sha256=v3O-wkDVaVsqHUk4Bl3h2uNGpieCkCLG3B5kyGznGsM,1248
|
|
6
|
+
resolutiontree-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
resolutiontree-0.1.0.dist-info/top_level.txt,sha256=E0usvI1QBYLUw4m1v4wY2UsZ6wRy-YD_RJxwzu-kO2U,15
|
|
8
|
+
resolutiontree-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
resolutiontree
|