sherlock-tools 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.
|
File without changes
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from liftover import ChainFile
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class QualityControl:
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Object compiling all functions pertaining to GWAS/xQTL QC
|
|
9
|
+
- To do's:
|
|
10
|
+
- Make liftover allele-aware
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
sumstats: pl.DataFrame,
|
|
16
|
+
snp_col: str,
|
|
17
|
+
chr_col: str,
|
|
18
|
+
pos_col: str,
|
|
19
|
+
a1_col: str,
|
|
20
|
+
a2_col: str,
|
|
21
|
+
beta_col: str,
|
|
22
|
+
se_col: str,
|
|
23
|
+
pval_col: str,
|
|
24
|
+
maf_thresh: float,
|
|
25
|
+
genome_build: str,
|
|
26
|
+
target_genome_build: str,
|
|
27
|
+
trait_type: str,
|
|
28
|
+
n_col: str | None = None,
|
|
29
|
+
n: int | None = None,
|
|
30
|
+
n_cases: int | None = None,
|
|
31
|
+
n_controls: int | None = None,
|
|
32
|
+
info_col: str | None = None,
|
|
33
|
+
info_thresh: float | None = None,
|
|
34
|
+
freq_col: str | None = None,
|
|
35
|
+
chain_file: str | None = None, # although in params.yaml arg -> liftover_dir/
|
|
36
|
+
drop_mhc: bool = True,
|
|
37
|
+
drop_apoe: bool = False,
|
|
38
|
+
):
|
|
39
|
+
|
|
40
|
+
# data
|
|
41
|
+
self.sumstats = sumstats
|
|
42
|
+
|
|
43
|
+
# column mapping
|
|
44
|
+
self.snp_col = snp_col
|
|
45
|
+
self.chr_col = chr_col
|
|
46
|
+
self.pos_col = pos_col
|
|
47
|
+
self.a1_col = a1_col
|
|
48
|
+
self.a2_col = a2_col
|
|
49
|
+
self.beta_col = beta_col
|
|
50
|
+
self.se_col = se_col
|
|
51
|
+
self.pval_col = pval_col
|
|
52
|
+
self.info_col = info_col
|
|
53
|
+
self.freq_col = freq_col
|
|
54
|
+
|
|
55
|
+
# trait + sample size
|
|
56
|
+
self.trait_type = trait_type
|
|
57
|
+
self.n_col = n_col
|
|
58
|
+
self.n = n
|
|
59
|
+
self.n_cases = n_cases
|
|
60
|
+
self.n_controls = n_controls
|
|
61
|
+
|
|
62
|
+
# thresholds
|
|
63
|
+
self.maf_thresh = maf_thresh
|
|
64
|
+
self.info_thresh = info_thresh
|
|
65
|
+
|
|
66
|
+
# builds
|
|
67
|
+
self.genome_build = genome_build
|
|
68
|
+
self.target_genome_build = target_genome_build
|
|
69
|
+
self.chain_file = chain_file
|
|
70
|
+
|
|
71
|
+
# region removal
|
|
72
|
+
self.drop_mhc = drop_mhc
|
|
73
|
+
self.drop_apoe = drop_apoe
|
|
74
|
+
|
|
75
|
+
def _fix_data_types(self):
|
|
76
|
+
# strict=False -> values that can't be cast become null (caught by _correct_for_missingness)
|
|
77
|
+
df = self.sumstats.with_columns(
|
|
78
|
+
pl.col(self.snp_col).cast(pl.String),
|
|
79
|
+
pl.col(self.chr_col).cast(pl.String).str.replace(r"(?i)^chr", ""),
|
|
80
|
+
pl.col(self.pos_col).cast(pl.Int64, strict=False),
|
|
81
|
+
pl.col(self.a1_col).cast(pl.String).str.to_uppercase(),
|
|
82
|
+
pl.col(self.a2_col).cast(pl.String).str.to_uppercase(),
|
|
83
|
+
pl.col(self.beta_col).cast(pl.Float64, strict=False),
|
|
84
|
+
pl.col(self.se_col).cast(pl.Float64, strict=False),
|
|
85
|
+
pl.col(self.pval_col).cast(pl.Float64, strict=False),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# optional cols if given
|
|
89
|
+
optional_cols = [c for c in [self.n_col, self.freq_col, self.info_col] if c is not None]
|
|
90
|
+
df = df.with_columns(pl.col(c).cast(pl.Float64, strict=False) for c in optional_cols)
|
|
91
|
+
self.sumstats = df
|
|
92
|
+
return df
|
|
93
|
+
|
|
94
|
+
def _fix_pval(self):
|
|
95
|
+
# P == 0 (underflow) -> 1e-300, so downstream Z / -log10(P) stay finite
|
|
96
|
+
n_zero = self.sumstats.filter(pl.col(self.pval_col) == 0).height
|
|
97
|
+
df = self.sumstats.with_columns(
|
|
98
|
+
pl.when(pl.col(self.pval_col) == 0)
|
|
99
|
+
.then(1e-300)
|
|
100
|
+
.otherwise(pl.col(self.pval_col))
|
|
101
|
+
.alias(self.pval_col)
|
|
102
|
+
)
|
|
103
|
+
print(f"Replaced P == 0 with 1e-300: {n_zero}")
|
|
104
|
+
self.sumstats = df
|
|
105
|
+
return df
|
|
106
|
+
|
|
107
|
+
def _restrict_to_autosomal(self):
|
|
108
|
+
# keep chr 1-22 only ("chr" prefix already stripped in _fix_data_types)
|
|
109
|
+
autosomes = [str(i) for i in range(1, 23)]
|
|
110
|
+
n_before = self.sumstats.height
|
|
111
|
+
df = self.sumstats.filter(pl.col(self.chr_col).is_in(autosomes))
|
|
112
|
+
n_after = df.height
|
|
113
|
+
print(f"Removed non-autosomal variants: {n_before - n_after}")
|
|
114
|
+
print(f"Rows after autosome restriction: {n_after}")
|
|
115
|
+
self.sumstats = df
|
|
116
|
+
return df
|
|
117
|
+
|
|
118
|
+
def _correct_for_missingness(self):
|
|
119
|
+
n_before = self.sumstats.height
|
|
120
|
+
df = self.sumstats.filter(~pl.all_horizontal(pl.all().is_null()))
|
|
121
|
+
n_after = df.height
|
|
122
|
+
print(f"Removed empty rows: {n_before - n_after}")
|
|
123
|
+
print(f"Rows after empty-row removal: {n_after}")
|
|
124
|
+
|
|
125
|
+
# remove rows missing any key column (+ optional cols if given)
|
|
126
|
+
key_cols = [
|
|
127
|
+
self.snp_col, self.chr_col, self.pos_col,
|
|
128
|
+
self.a1_col, self.a2_col,
|
|
129
|
+
self.beta_col, self.se_col, self.pval_col,
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
key_cols += [c for c in [self.n_col, self.freq_col, self.info_col] if c is not None]
|
|
133
|
+
n_before = df.height
|
|
134
|
+
df = df.drop_nulls(subset=key_cols)
|
|
135
|
+
n_after = df.height
|
|
136
|
+
print(f"Removed rows with missing key values: {n_before - n_after}")
|
|
137
|
+
print(f"Rows after missing-value removal: {n_after}")
|
|
138
|
+
self.sumstats = df
|
|
139
|
+
return df
|
|
140
|
+
|
|
141
|
+
def _remove_duplicates(self):
|
|
142
|
+
# duplicated SNP IDs -> drop all copies (can't tell which one is right)
|
|
143
|
+
n_before = self.sumstats.height
|
|
144
|
+
df = self.sumstats.unique(subset=[self.snp_col], keep="none", maintain_order=True)
|
|
145
|
+
n_after = df.height
|
|
146
|
+
print(f"Removed duplicated SNPs: {n_before - n_after}")
|
|
147
|
+
print(f"Rows after duplicate removal: {n_after}")
|
|
148
|
+
self.sumstats = df
|
|
149
|
+
return df
|
|
150
|
+
|
|
151
|
+
def _remove_multiallelic(self):
|
|
152
|
+
# run after duplicate removal -> any repeated CHR:POS left = different alleles
|
|
153
|
+
n_before = self.sumstats.height
|
|
154
|
+
df = self.sumstats.filter(
|
|
155
|
+
pl.len().over([self.chr_col, self.pos_col]) == 1
|
|
156
|
+
)
|
|
157
|
+
n_after = df.height
|
|
158
|
+
print(f"Removed multiallelic variants: {n_before - n_after}")
|
|
159
|
+
print(f"Rows after multiallelic removal: {n_after}")
|
|
160
|
+
self.sumstats = df
|
|
161
|
+
return df
|
|
162
|
+
|
|
163
|
+
def _remove_indels(self):
|
|
164
|
+
bases = ["A", "T", "C", "G"]
|
|
165
|
+
a1 = pl.col(self.a1_col)
|
|
166
|
+
a2 = pl.col(self.a2_col)
|
|
167
|
+
ok_len = (a1.str.len_chars() == 1) & (a2.str.len_chars() == 1)
|
|
168
|
+
ok_bases = a1.is_in(bases) & a2.is_in(bases)
|
|
169
|
+
n_before = self.sumstats.height
|
|
170
|
+
df = self.sumstats.filter(ok_len & ok_bases)
|
|
171
|
+
n_after = df.height
|
|
172
|
+
print(f"Removed INDELs / non-ACGT: {n_before - n_after}")
|
|
173
|
+
print(f"Rows after INDEL removal: {n_after}")
|
|
174
|
+
self.sumstats = df
|
|
175
|
+
return df
|
|
176
|
+
|
|
177
|
+
def _remove_palindromes(self):
|
|
178
|
+
# A/T and C/G SNPs -> strand can't be resolved, always dropped
|
|
179
|
+
a1 = pl.col(self.a1_col)
|
|
180
|
+
a2 = pl.col(self.a2_col)
|
|
181
|
+
is_palindrome = (
|
|
182
|
+
((a1 == "A") & (a2 == "T")) |
|
|
183
|
+
((a1 == "T") & (a2 == "A")) |
|
|
184
|
+
((a1 == "C") & (a2 == "G")) |
|
|
185
|
+
((a1 == "G") & (a2 == "C"))
|
|
186
|
+
)
|
|
187
|
+
n_before = self.sumstats.height
|
|
188
|
+
df = self.sumstats.filter(~is_palindrome)
|
|
189
|
+
n_after = df.height
|
|
190
|
+
print(f"Removed palindromic SNPs: {n_before - n_after}")
|
|
191
|
+
print(f"Rows after palindrome removal: {n_after}")
|
|
192
|
+
self.sumstats = df
|
|
193
|
+
return df
|
|
194
|
+
|
|
195
|
+
def _maf_filter(self):
|
|
196
|
+
if self.freq_col is None:
|
|
197
|
+
print("Skipping MAF filter: no frequency column supplied.")
|
|
198
|
+
return self.sumstats
|
|
199
|
+
|
|
200
|
+
# FRQ = A1 freq -> MAF = min(FRQ, 1 - FRQ)
|
|
201
|
+
n_before = self.sumstats.height
|
|
202
|
+
df = self.sumstats.filter(
|
|
203
|
+
pl.min_horizontal(pl.col(self.freq_col), 1 - pl.col(self.freq_col)) >= self.maf_thresh
|
|
204
|
+
)
|
|
205
|
+
n_after = df.height
|
|
206
|
+
print(f"Removed low-MAF variants: {n_before - n_after}")
|
|
207
|
+
print(f"Rows after MAF filter: {n_after}")
|
|
208
|
+
self.sumstats = df
|
|
209
|
+
return df
|
|
210
|
+
|
|
211
|
+
def _info_filter(self):
|
|
212
|
+
if self.info_col is None:
|
|
213
|
+
print("Skipping INFO filter: no INFO column supplied.")
|
|
214
|
+
return self.sumstats
|
|
215
|
+
|
|
216
|
+
n_before = self.sumstats.height
|
|
217
|
+
df = self.sumstats.filter(
|
|
218
|
+
pl.col(self.info_col) >= self.info_thresh
|
|
219
|
+
)
|
|
220
|
+
n_after = df.height
|
|
221
|
+
print(f"Removed low-INFO variants: {n_before - n_after}")
|
|
222
|
+
print(f"Rows after INFO filter: {n_after}")
|
|
223
|
+
self.sumstats = df
|
|
224
|
+
return df
|
|
225
|
+
|
|
226
|
+
def _impute_effective_n(self):
|
|
227
|
+
# per-SNP N given -> keep it (_standardise_shape renames n_col -> N)
|
|
228
|
+
if self.n_col is not None:
|
|
229
|
+
print(f"Using per-SNP sample size from column: {self.n_col}")
|
|
230
|
+
return self.sumstats
|
|
231
|
+
|
|
232
|
+
# depends on whether type == cc / quant
|
|
233
|
+
if self.trait_type == "quant":
|
|
234
|
+
if self.n is None:
|
|
235
|
+
raise ValueError("trait_type 'quant' needs n_col or n")
|
|
236
|
+
n_eff = int(round(self.n))
|
|
237
|
+
elif self.trait_type == "cc":
|
|
238
|
+
if self.n_cases is None or self.n_controls is None:
|
|
239
|
+
raise ValueError("trait_type 'cc' needs n_col or n_cases + n_controls")
|
|
240
|
+
n_eff = int(round(4 * self.n_cases * self.n_controls / (self.n_cases + self.n_controls)))
|
|
241
|
+
else:
|
|
242
|
+
raise ValueError(f"Unsupported trait_type: {self.trait_type}")
|
|
243
|
+
|
|
244
|
+
df = self.sumstats.with_columns(pl.lit(n_eff, dtype=pl.Int64).alias("N"))
|
|
245
|
+
print(f"N set to: {n_eff} ({self.trait_type})")
|
|
246
|
+
self.sumstats = df
|
|
247
|
+
return df
|
|
248
|
+
|
|
249
|
+
def _liftover(self):
|
|
250
|
+
# same build -> nothing to do
|
|
251
|
+
if self.genome_build == self.target_genome_build:
|
|
252
|
+
print(f"Skipping liftover: already {self.target_genome_build}")
|
|
253
|
+
return self.sumstats
|
|
254
|
+
|
|
255
|
+
converter = ChainFile(self.chain_file, one_based=True)
|
|
256
|
+
chrs = self.sumstats[self.chr_col].to_list()
|
|
257
|
+
poss = self.sumstats[self.pos_col].to_list()
|
|
258
|
+
lifted_chrs = []
|
|
259
|
+
lifted_poss = []
|
|
260
|
+
|
|
261
|
+
for chrom, pos in zip(chrs, poss):
|
|
262
|
+
hits = converter[f"chr{chrom}"][int(pos)]
|
|
263
|
+
|
|
264
|
+
# no hit, or lands on another chr / alt contig -> drop
|
|
265
|
+
if len(hits) == 0 or hits[0][0].replace("chr", "") != chrom:
|
|
266
|
+
lifted_chrs.append(None)
|
|
267
|
+
lifted_poss.append(None)
|
|
268
|
+
else:
|
|
269
|
+
lifted_chrs.append(chrom)
|
|
270
|
+
lifted_poss.append(hits[0][1])
|
|
271
|
+
|
|
272
|
+
df = self.sumstats.with_columns([
|
|
273
|
+
pl.Series("CHR_LIFTED", lifted_chrs),
|
|
274
|
+
pl.Series("POS_LIFTED", lifted_poss),
|
|
275
|
+
])
|
|
276
|
+
|
|
277
|
+
n_before = df.height
|
|
278
|
+
df = df.drop_nulls(["CHR_LIFTED", "POS_LIFTED"])
|
|
279
|
+
n_after = df.height
|
|
280
|
+
print(f"Removed failed liftover variants: {n_before - n_after}")
|
|
281
|
+
print(f"Rows after liftover: {n_after}")
|
|
282
|
+
df = df.with_columns([
|
|
283
|
+
pl.col("CHR_LIFTED").alias(self.chr_col),
|
|
284
|
+
pl.col("POS_LIFTED").cast(pl.Int64).alias(self.pos_col),
|
|
285
|
+
])
|
|
286
|
+
df = df.drop(["CHR_LIFTED", "POS_LIFTED"])
|
|
287
|
+
self.sumstats = df
|
|
288
|
+
return df
|
|
289
|
+
|
|
290
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
291
|
+
# --- These bottom ones -> after _liftover() functs
|
|
292
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
293
|
+
|
|
294
|
+
def _align_betas_to_risk_allele(self):
|
|
295
|
+
# align to risk allele: BETA < 0 -> flip sign + swap A1/A2 (+ FRQ -> 1 - FRQ)
|
|
296
|
+
flip = pl.col(self.beta_col) < 0
|
|
297
|
+
n_flip = self.sumstats.filter(flip).height
|
|
298
|
+
|
|
299
|
+
cols = [
|
|
300
|
+
pl.when(flip).then(-pl.col(self.beta_col)).otherwise(pl.col(self.beta_col)).alias(self.beta_col),
|
|
301
|
+
pl.when(flip).then(pl.col(self.a2_col)).otherwise(pl.col(self.a1_col)).alias(self.a1_col),
|
|
302
|
+
pl.when(flip).then(pl.col(self.a1_col)).otherwise(pl.col(self.a2_col)).alias(self.a2_col),
|
|
303
|
+
]
|
|
304
|
+
if self.freq_col is not None:
|
|
305
|
+
cols.append(
|
|
306
|
+
pl.when(flip).then(1 - pl.col(self.freq_col)).otherwise(pl.col(self.freq_col)).alias(self.freq_col)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
df = self.sumstats.with_columns(cols)
|
|
310
|
+
print(f"Flipped to risk allele: {n_flip}")
|
|
311
|
+
self.sumstats = df
|
|
312
|
+
return df
|
|
313
|
+
|
|
314
|
+
# removing specific regions
|
|
315
|
+
def _remove_mhc(self):
|
|
316
|
+
|
|
317
|
+
# to run after liftover
|
|
318
|
+
if not self.drop_mhc:
|
|
319
|
+
print("Keeping MHC region!")
|
|
320
|
+
return self.sumstats
|
|
321
|
+
|
|
322
|
+
if self.target_genome_build == "GRCh38":
|
|
323
|
+
mhc_start = 24999772
|
|
324
|
+
mhc_end = 34032223
|
|
325
|
+
elif self.target_genome_build == "GRCh37":
|
|
326
|
+
mhc_start = 25000000
|
|
327
|
+
mhc_end = 34000000
|
|
328
|
+
else:
|
|
329
|
+
raise ValueError(f"Unsupported target_genome_build: {self.target_genome_build}")
|
|
330
|
+
|
|
331
|
+
n_before = self.sumstats.height
|
|
332
|
+
df = self.sumstats.filter(
|
|
333
|
+
~(
|
|
334
|
+
(pl.col(self.chr_col) == "6") &
|
|
335
|
+
(pl.col(self.pos_col) >= mhc_start) &
|
|
336
|
+
(pl.col(self.pos_col) <= mhc_end)
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
n_after = df.height
|
|
340
|
+
print(f"Removed MHC variants: {n_before - n_after} ({self.target_genome_build})!")
|
|
341
|
+
self.sumstats = df
|
|
342
|
+
return df
|
|
343
|
+
|
|
344
|
+
def _remove_apoe(self):
|
|
345
|
+
|
|
346
|
+
if not self.drop_apoe:
|
|
347
|
+
print("Keeping APOE region!")
|
|
348
|
+
return self.sumstats
|
|
349
|
+
|
|
350
|
+
if self.target_genome_build == "GRCh38":
|
|
351
|
+
apoe_start = 43495848
|
|
352
|
+
apoe_end = 45996742
|
|
353
|
+
elif self.target_genome_build == "GRCh37":
|
|
354
|
+
apoe_start = 44000000
|
|
355
|
+
apoe_end = 46500000
|
|
356
|
+
else:
|
|
357
|
+
raise ValueError(f"Unsupported target_genome_build: {self.target_genome_build}")
|
|
358
|
+
|
|
359
|
+
n_before = self.sumstats.height
|
|
360
|
+
df = self.sumstats.filter(
|
|
361
|
+
~(
|
|
362
|
+
(pl.col(self.chr_col) == "19") &
|
|
363
|
+
(pl.col(self.pos_col) >= apoe_start) &
|
|
364
|
+
(pl.col(self.pos_col) <= apoe_end)
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
n_after = df.height
|
|
368
|
+
print(f"Removed APOE variants: {n_before - n_after} ({self.target_genome_build})!")
|
|
369
|
+
self.sumstats = df
|
|
370
|
+
return df
|
|
371
|
+
|
|
372
|
+
def _standardise_shape(self):
|
|
373
|
+
# rename cols to the sherlock format (A1 = effect allele)
|
|
374
|
+
cols = [
|
|
375
|
+
pl.col(self.snp_col).alias("SNP"),
|
|
376
|
+
pl.col(self.chr_col).alias("CHR"),
|
|
377
|
+
pl.col(self.pos_col).alias("BP"),
|
|
378
|
+
pl.col(self.a1_col).alias("A1"),
|
|
379
|
+
pl.col(self.a2_col).alias("A2"),
|
|
380
|
+
pl.col(self.beta_col).alias("BETA"),
|
|
381
|
+
pl.col(self.se_col).alias("SE"),
|
|
382
|
+
pl.col(self.pval_col).alias("P"),
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
# sample size: always one N column
|
|
386
|
+
if self.n_col is not None:
|
|
387
|
+
cols.append(pl.col(self.n_col).alias("N"))
|
|
388
|
+
elif "N" in self.sumstats.columns:
|
|
389
|
+
cols.append(pl.col("N"))
|
|
390
|
+
|
|
391
|
+
# optional cols if given
|
|
392
|
+
if self.freq_col is not None:
|
|
393
|
+
cols.append(pl.col(self.freq_col).alias("FRQ"))
|
|
394
|
+
if self.info_col is not None:
|
|
395
|
+
cols.append(pl.col(self.info_col).alias("INFO"))
|
|
396
|
+
|
|
397
|
+
df = self.sumstats.select(cols)
|
|
398
|
+
self.sumstats = df
|
|
399
|
+
return df
|
|
400
|
+
|
|
401
|
+
# running wrapper for class -> thin wrapper in bin/ & nf module/subworkflow later!!!!
|
|
402
|
+
def run(self):
|
|
403
|
+
print(f"Rows at start: {self.sumstats.height}")
|
|
404
|
+
self._fix_data_types()
|
|
405
|
+
self._correct_for_missingness()
|
|
406
|
+
self._fix_pval()
|
|
407
|
+
self._remove_indels()
|
|
408
|
+
self._remove_palindromes()
|
|
409
|
+
self._remove_duplicates()
|
|
410
|
+
self._remove_multiallelic()
|
|
411
|
+
self._restrict_to_autosomal()
|
|
412
|
+
self._maf_filter()
|
|
413
|
+
self._info_filter()
|
|
414
|
+
self._liftover()
|
|
415
|
+
self._remove_multiallelic()
|
|
416
|
+
self._remove_mhc()
|
|
417
|
+
self._remove_apoe()
|
|
418
|
+
self._align_betas_to_risk_allele()
|
|
419
|
+
self._impute_effective_n()
|
|
420
|
+
self._standardise_shape()
|
|
421
|
+
print(f"Rows at end: {self.sumstats.height}")
|
|
422
|
+
return self.sumstats
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sherlock-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library behind the nf-core/sherlock pipeline
|
|
5
|
+
Project-URL: Homepage, https://github.com/guillermocomesanacimadevila/sherlock
|
|
6
|
+
Project-URL: Issues, https://github.com/guillermocomesanacimadevila/sherlock/issues
|
|
7
|
+
Author-email: Guillermo Comesaña Cimadevila <ComesanaCimadevilaG@cardiff.ac.uk>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: liftover>=1.3
|
|
14
|
+
Requires-Dist: polars>=1.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest-cov==7.1.0; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest==9.1.1; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# sherlock-tools
|
|
25
|
+
|
|
26
|
+
Python library used by the nf-core/sherlock pipeline. Scripts in the pipeline's `bin/` folder import it.
|
|
27
|
+
|
|
28
|
+
## Development install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e "sherlock-tools[dev]"
|
|
32
|
+
pytest sherlock-tools/tests
|
|
33
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sherlock_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
sherlock_tools/preprocessing/__init__.py,sha256=tx_PQVBoK7cc-K3spXqBteQIHhXFW2zY-htKITFrMm0,61
|
|
3
|
+
sherlock_tools/preprocessing/qc.py,sha256=diTIyHFSjyGYqLt-mejC_PkKhmAuouq8QXzOBfgaHUg,15450
|
|
4
|
+
sherlock_tools-0.1.0.dist-info/METADATA,sha256=tcdtOEAatRqF0vHwAW4ogJYmGdcw2cToKBC5tRunvL4,1167
|
|
5
|
+
sherlock_tools-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
6
|
+
sherlock_tools-0.1.0.dist-info/RECORD,,
|