cutseq 0.0.3__tar.gz → 0.0.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cutseq
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Automatically cut adapter / barcode / UMI from NGS data
5
5
  Home-page: https://github.com/y9c/cutseq
6
6
  License: MIT
@@ -19,9 +19,20 @@ Requires-Dist: cutadapt (>=4.8,<5.0)
19
19
  Project-URL: Repository, https://github.com/y9c/cutseq
20
20
  Description-Content-Type: text/markdown
21
21
 
22
- #✂️ CutSeq
22
+ # ✂️ CutSeq
23
+
24
+ [![Pypi Releases](https://img.shields.io/pypi/v/cutseq.svg)](https://pypi.python.org/pypi/cutseq)
25
+ [![Downloads](https://pepy.tech/badge/cutseq)](https://pepy.tech/project/cutseq)
26
+
27
+ # How to install?
28
+
29
+ ```bash
30
+ pip install cutseq
31
+ ```
23
32
 
24
33
  ## How to use?
25
34
 
26
- `cutseq -a "ACACGACGCTCTTCCGATCTX<XXXAGATCGGAAGAGCACACGTC" test_R1.fq.gz test_R2.fq.gz`
35
+ ```bash
36
+ cutseq -a "ACACGACGCTCTTCCGATCTX<XXXAGATCGGAAGAGCACACGTC" test_R1.fq.gz test_R2.fq.gz
37
+ ```
27
38
 
cutseq-0.0.5/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # ✂️ CutSeq
2
+
3
+ [![Pypi Releases](https://img.shields.io/pypi/v/cutseq.svg)](https://pypi.python.org/pypi/cutseq)
4
+ [![Downloads](https://pepy.tech/badge/cutseq)](https://pepy.tech/project/cutseq)
5
+
6
+ # How to install?
7
+
8
+ ```bash
9
+ pip install cutseq
10
+ ```
11
+
12
+ ## How to use?
13
+
14
+ ```bash
15
+ cutseq -a "ACACGACGCTCTTCCGATCTX<XXXAGATCGGAAGAGCACACGTC" test_R1.fq.gz test_R2.fq.gz
16
+ ```
@@ -0,0 +1,644 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright © 2024 Ye Chang yech1990@gmail.com
5
+ # Distributed under terms of the GNU license.
6
+ #
7
+ # Created: 2024-04-19 18:57
8
+
9
+ import argparse
10
+ import logging
11
+ import re
12
+ import subprocess
13
+ import sys
14
+
15
+ from cutadapt.adapters import (
16
+ BackAdapter,
17
+ NonInternalBackAdapter,
18
+ NonInternalFrontAdapter,
19
+ PrefixAdapter,
20
+ RightmostFrontAdapter,
21
+ SuffixAdapter,
22
+ )
23
+ from cutadapt.files import InputPaths, OutputFiles
24
+ from cutadapt.modifiers import (
25
+ AdapterCutter,
26
+ PairedEndRenamer,
27
+ QualityTrimmer,
28
+ Renamer,
29
+ SuffixRemover,
30
+ UnconditionalCutter,
31
+ )
32
+ from cutadapt.pipeline import PairedEndPipeline, SingleEndPipeline
33
+ from cutadapt.predicates import Predicate, TooShort
34
+ from cutadapt.runners import make_runner
35
+ from cutadapt.steps import (
36
+ PairedEndFilter,
37
+ PairedEndSink,
38
+ SingleEndFilter,
39
+ SingleEndSink,
40
+ )
41
+ from cutadapt.utils import Progress
42
+
43
+ logging.basicConfig(
44
+ level=logging.INFO,
45
+ format="%(asctime)s - %(levelname)s - %(message)s",
46
+ )
47
+
48
+
49
+ def reverse_complement(b):
50
+ return "".join(
51
+ [dict(zip("ATGCNatgcn", "TACGNtacgn"))[x] for x in b[::-1] if x in "ATGCNatgcn"]
52
+ )
53
+
54
+
55
+ def remove_fq_suffix(f):
56
+ suffixes_base = ["_R1_001", "_R2_001", "_R1", "_R2", ""]
57
+ suffixes = [
58
+ y + "." + x for x in ["fastq.gz", "fq.gz", "fastq", "fq"] for y in suffixes_base
59
+ ]
60
+ print(suffixes)
61
+ for suffix in suffixes:
62
+ if f.endswith(suffix):
63
+ return f.removesuffix(suffix)
64
+ return f
65
+
66
+
67
+ class BarcodeSeq:
68
+ def __init__(self, seq):
69
+ self.fw = seq
70
+ self.rc = reverse_complement(seq)
71
+ self.len = len(seq)
72
+
73
+
74
+ class BarcodeConfig:
75
+ """
76
+ Adapter scheme:
77
+ (p5)(inline5)(umi5)(mask5)(strand)(mask3)(umi3)(inline3)(p7)
78
+ """
79
+
80
+ def __init__(self, adapter=None):
81
+ self.strand = None
82
+ self.p5 = BarcodeSeq("")
83
+ self.p7 = BarcodeSeq("")
84
+ self.inline5 = BarcodeSeq("")
85
+ self.inline3 = BarcodeSeq("")
86
+ self.umi5 = BarcodeSeq("")
87
+ self.umi3 = BarcodeSeq("")
88
+ self.mask5 = BarcodeSeq("")
89
+ self.mask3 = BarcodeSeq("")
90
+ if adapter is not None:
91
+ self._parse_barcode(adapter)
92
+
93
+ def _parse_barcode(self, b):
94
+ m = re.match(
95
+ r"(?P<p5>[ATGCatgc]+)(\((?P<inline5>[ATGCatgc]+)\))?(?P<umi5>N*)(?P<mask5>X*)(?P<strand>-|>|<)(?P<mask3>X*)(?P<umi3>N*)(\((?P<inline3>[ATGCatgc]+)\))?(?P<p7>[ATGCatgc]+)",
96
+ b,
97
+ )
98
+ if m is None:
99
+ logging.error(f"barcode {b} is not valid")
100
+ sys.exit(1)
101
+ d = m.groupdict()
102
+ if d["inline5"] is None:
103
+ d["inline5"] = ""
104
+ if d["inline3"] is None:
105
+ d["inline3"] = ""
106
+ self.strand = "+" if d["strand"] == ">" else "-" if d["strand"] == "<" else None
107
+ self.p5 = BarcodeSeq(d["p5"])
108
+ self.p7 = BarcodeSeq(d["p7"])
109
+ self.inline5 = BarcodeSeq(d["inline5"])
110
+ self.inline3 = BarcodeSeq(d["inline3"])
111
+ self.umi5 = BarcodeSeq(d["umi5"])
112
+ self.umi3 = BarcodeSeq(d["umi3"])
113
+ self.mask5 = BarcodeSeq(d["mask5"])
114
+ self.mask3 = BarcodeSeq(d["mask3"])
115
+
116
+
117
+ class CutadaptConfig:
118
+ def __init__(self):
119
+ self.rname_suffix = False
120
+ self.ensure_inline_barcode = False
121
+ self.trim_polyA = False
122
+ self.min_length = 20
123
+ self.min_quality = 20
124
+ self.dry_run = False
125
+ self.threads = 1
126
+
127
+
128
+ class IsUntrimmedRef(Predicate):
129
+ """
130
+ Select reads for which no adapter match was found
131
+ """
132
+
133
+ def __init__(self, ref_adapters):
134
+ self.ref_adapters = ref_adapters
135
+
136
+ def __repr__(self):
137
+ return "IsUntrimmedRef()"
138
+
139
+ def test(self, read, info):
140
+ # check not all adapters with ref_adapters are exist in the matches
141
+ match_adapters = [match.adapter for match in info.matches]
142
+ if any([adapter not in match_adapters for adapter in self.ref_adapters]):
143
+ return True
144
+ return False
145
+
146
+
147
+ def run_steps(steps, dry_run=False):
148
+ if dry_run:
149
+ print(
150
+ " |\\\n".join([(" " + s if i > 0 else s) for i, s in enumerate(steps)])
151
+ )
152
+ process = subprocess.run("true", shell=True, capture_output=True)
153
+ else:
154
+ cmd = " | ".join(steps)
155
+ process = subprocess.run(cmd, shell=True, capture_output=True)
156
+ return process.stdout.decode(), process.stderr.decode()
157
+
158
+
159
+ def pipeline_single(input1, output1, short1, untrimmed1, barcode, settings):
160
+ modifiers = []
161
+ # step 1: remove suffix in the read name
162
+ modifiers.extend([SuffixRemover(".1"), SuffixRemover("/1")])
163
+ # step 2: remove adapter on the 5' end, artifact of template switching
164
+ modifiers.append(
165
+ AdapterCutter(
166
+ [
167
+ RightmostFrontAdapter(
168
+ sequence=barcode.p5.fw, max_errors=0.25, min_overlap=10
169
+ )
170
+ ],
171
+ times=1,
172
+ )
173
+ )
174
+ # step 3: remove adapter on the 3' end, read though in the sequencing
175
+ modifiers.append(
176
+ AdapterCutter(
177
+ [BackAdapter(sequence=barcode.p7.fw, max_errors=0.2, min_overlap=3)],
178
+ times=2,
179
+ ),
180
+ )
181
+ # step 4: trim inline barcode
182
+ if barcode.inline5.len > 0:
183
+ adapter_inline5 = PrefixAdapter(sequence=barcode.inline5.fw, max_errors=0.2)
184
+ modifiers.append(AdapterCutter([adapter_inline5], times=1))
185
+ if barcode.inline3.len > 0:
186
+ adapter_inline3 = SuffixAdapter(sequence=barcode.inline3.fw, max_errors=0.2)
187
+ modifiers.append(AdapterCutter([adapter_inline3], times=1))
188
+
189
+ # step 5: extract UMI
190
+ if barcode.umi5.len > 0:
191
+ modifiers.append(UnconditionalCutter(barcode.umi5.len))
192
+ if barcode.umi3.len > 0:
193
+ modifiers.append(UnconditionalCutter(-barcode.umi3.len))
194
+ if barcode.umi5.len + barcode.umi3.len > 0:
195
+ modifiers.append(Renamer("{id}_{cut_prefix}{cut_suffix}"))
196
+ else:
197
+ modifiers.append(Renamer("{id}"))
198
+
199
+ # step 6: mask tail in the RNA, which might be artifact of RT
200
+ if barcode.mask5.len > 0:
201
+ modifiers.append(UnconditionalCutter(barcode.mask5.len))
202
+ if barcode.mask3.len > 0:
203
+ modifiers.append(UnconditionalCutter(-barcode.mask3.len))
204
+ # step 7: trim polyA
205
+ if settings.trim_polyA:
206
+ if barcode.strand == "+":
207
+ modifiers.append(
208
+ AdapterCutter(
209
+ [NonInternalBackAdapter(sequence="A" * 100, max_errors=0.15)]
210
+ )
211
+ )
212
+ elif barcode.strand == "-":
213
+ modifiers.append(
214
+ AdapterCutter(
215
+ [NonInternalFrontAdapter(sequence="T" * 100, max_errors=0.15)]
216
+ )
217
+ )
218
+ else:
219
+ logging.info("No strand information provided, skip polyA trimming.")
220
+ # step 8: quality control, remove short reads
221
+ modifiers.append(
222
+ QualityTrimmer(cutoff_front=0, cutoff_back=settings.min_quality),
223
+ )
224
+
225
+ inpaths = InputPaths(input1)
226
+
227
+ with make_runner(inpaths, cores=settings.threads) as runner:
228
+ outfiles = OutputFiles(
229
+ proxied=settings.threads > 1,
230
+ qualities=runner.input_file_format().has_qualities(),
231
+ interleaved=False,
232
+ )
233
+ steps = []
234
+ steps.append(
235
+ # --info-file=info.txt
236
+ # PairedSingleEndStep(InfoFileWriter(outfiles.open_text("info.txt"))),
237
+ # -m 10
238
+ SingleEndFilter(
239
+ TooShort(settings.min_length), outfiles.open_record_writer(short1)
240
+ ),
241
+ )
242
+ if (
243
+ settings.ensure_inline_barcode
244
+ and barcode.inline5.len + barcode.inline3.len > 0
245
+ ):
246
+ ref_adapters = []
247
+ if barcode.inline5.len > 0:
248
+ ref_adapters.append(adapter_inline5)
249
+ if barcode.inline3.len > 0:
250
+ ref_adapters.append(adapter_inline3)
251
+ steps.append(
252
+ # TODO: --max-n=0 support
253
+ SingleEndFilter(
254
+ IsUntrimmedRef(ref_adapters),
255
+ outfiles.open_record_writer(untrimmed1, interleaved=False),
256
+ ),
257
+ )
258
+ steps.append(
259
+ # -o
260
+ SingleEndSink(outfiles.open_record_writer(output1, interleaved=False)),
261
+ )
262
+ pipeline = SingleEndPipeline(modifiers, steps)
263
+ _stats = runner.run(pipeline, Progress(), outfiles)
264
+ # _ = stats.as_json()
265
+ outfiles.close()
266
+
267
+
268
+ def pipeline_paired(
269
+ input1,
270
+ input2,
271
+ output1,
272
+ output2,
273
+ short1,
274
+ short2,
275
+ untrimmed1,
276
+ untrimmed2,
277
+ barcode,
278
+ settings,
279
+ ):
280
+ modifiers = []
281
+ # step 1: remove suffix in the read name
282
+ modifiers.extend(
283
+ [
284
+ (SuffixRemover(".1"), SuffixRemover(".2")),
285
+ (SuffixRemover("/1"), SuffixRemover("/2")),
286
+ ]
287
+ )
288
+ # step 2: remove adapter on the 5' end, artifact of template switching
289
+ modifiers.append(
290
+ (
291
+ AdapterCutter(
292
+ [
293
+ RightmostFrontAdapter(
294
+ sequence=barcode.p5.fw, max_errors=0.25, min_overlap=10
295
+ )
296
+ ],
297
+ times=1,
298
+ ),
299
+ AdapterCutter(
300
+ [
301
+ RightmostFrontAdapter(
302
+ sequence=barcode.p7.rc, max_errors=0.25, min_overlap=10
303
+ )
304
+ ],
305
+ times=1,
306
+ ),
307
+ ),
308
+ )
309
+ # step 3: remove adapter on the 3' end, read though in the sequencing
310
+ modifiers.append(
311
+ (
312
+ AdapterCutter(
313
+ [BackAdapter(sequence=barcode.p7.fw, max_errors=0.2, min_overlap=3)],
314
+ times=2,
315
+ ),
316
+ AdapterCutter(
317
+ [BackAdapter(sequence=barcode.p5.rc, max_errors=0.2, min_overlap=3)],
318
+ times=2,
319
+ ),
320
+ ),
321
+ )
322
+ # step 4: trim inline barcode
323
+ if barcode.inline5.len > 0:
324
+ adapter_inline5 = PrefixAdapter(sequence=barcode.inline5.fw, max_errors=0.2)
325
+ modifiers.append(
326
+ (
327
+ AdapterCutter([adapter_inline5], times=1),
328
+ UnconditionalCutter(-barcode.inline5.len),
329
+ )
330
+ )
331
+ if barcode.inline3.len > 0:
332
+ adapter_inline3 = PrefixAdapter(sequence=barcode.inline3.rc, max_errors=0.2)
333
+ modifiers.append(
334
+ (
335
+ UnconditionalCutter(-barcode.inline3.len),
336
+ AdapterCutter([adapter_inline3], times=1),
337
+ )
338
+ )
339
+
340
+ # step 5: extract UMI
341
+ if barcode.umi5.len > 0:
342
+ modifiers.append(
343
+ (
344
+ UnconditionalCutter(barcode.umi5.len),
345
+ UnconditionalCutter(-barcode.umi5.len),
346
+ ),
347
+ )
348
+ if barcode.umi3.len > 0:
349
+ modifiers.append(
350
+ (
351
+ UnconditionalCutter(-barcode.umi3.len),
352
+ UnconditionalCutter(barcode.umi3.len),
353
+ )
354
+ )
355
+ if barcode.umi5.len + barcode.umi3.len > 0:
356
+ modifiers.append(PairedEndRenamer("{id}_{r1.cut_prefix}{r2.cut_prefix}"))
357
+ else:
358
+ modifiers.append(PairedEndRenamer("{id}"))
359
+
360
+ # step 6: mask tail in the RNA, which might be artifact of RT
361
+ if barcode.mask5.len > 0:
362
+ modifiers.append(
363
+ (
364
+ UnconditionalCutter(barcode.mask5.len),
365
+ UnconditionalCutter(-barcode.mask5.len),
366
+ )
367
+ )
368
+ if barcode.mask3.len > 0:
369
+ modifiers.append(
370
+ (
371
+ UnconditionalCutter(-barcode.mask3.len),
372
+ UnconditionalCutter(barcode.mask3.len),
373
+ )
374
+ )
375
+ # step 7: trim polyA
376
+ if settings.trim_polyA:
377
+ if barcode.strand == "+":
378
+ modifiers.append(
379
+ (
380
+ AdapterCutter(
381
+ [NonInternalBackAdapter(sequence="A" * 100, max_errors=0.15)]
382
+ ),
383
+ AdapterCutter(
384
+ [NonInternalFrontAdapter(sequence="T" * 100, max_errors=0.15)]
385
+ ),
386
+ )
387
+ )
388
+ elif barcode.strand == "-":
389
+ modifiers.append(
390
+ (
391
+ AdapterCutter(
392
+ [NonInternalFrontAdapter(sequence="T" * 100, max_errors=0.15)]
393
+ ),
394
+ AdapterCutter(
395
+ [NonInternalBackAdapter(sequence="A" * 100, max_errors=0.15)]
396
+ ),
397
+ )
398
+ )
399
+ else:
400
+ logging.info("No strand information provided, skip polyA trimming.")
401
+ # step 8: quality control, remove short reads
402
+ modifiers.append(
403
+ (
404
+ QualityTrimmer(cutoff_front=0, cutoff_back=settings.min_quality),
405
+ QualityTrimmer(cutoff_front=0, cutoff_back=settings.min_quality),
406
+ )
407
+ )
408
+
409
+ inpaths = InputPaths(input1, input2)
410
+
411
+ with make_runner(inpaths, cores=settings.threads) as runner:
412
+ outfiles = OutputFiles(
413
+ proxied=settings.threads > 1,
414
+ qualities=runner.input_file_format().has_qualities(),
415
+ interleaved=False,
416
+ )
417
+ steps = []
418
+ # --info-file=info.txt
419
+ # PairedSingleEndStep(InfoFileWriter(outfiles.open_text("info.txt"))),
420
+ # -m 10:10
421
+ steps.append(
422
+ PairedEndFilter(
423
+ TooShort(settings.min_length),
424
+ TooShort(settings.min_length),
425
+ outfiles.open_record_writer(short1, short2, interleaved=False),
426
+ )
427
+ )
428
+ if (
429
+ settings.ensure_inline_barcode
430
+ and barcode.inline5.len + barcode.inline3.len > 0
431
+ ):
432
+ steps.append(
433
+ # TODO: --max-n=0 support
434
+ PairedEndFilter(
435
+ IsUntrimmedRef([adapter_inline5])
436
+ if barcode.inline5.len > 0
437
+ else None,
438
+ IsUntrimmedRef([adapter_inline3])
439
+ if barcode.inline3.len > 0
440
+ else None,
441
+ outfiles.open_record_writer(
442
+ untrimmed1, untrimmed2, interleaved=False
443
+ ),
444
+ pair_filter_mode="any",
445
+ )
446
+ )
447
+ steps.append(
448
+ # -o ... -p ...
449
+ PairedEndSink(outfiles.open_record_writer(output1, output2))
450
+ )
451
+ pipeline = PairedEndPipeline(modifiers, steps)
452
+ _stats = runner.run(pipeline, Progress(), outfiles)
453
+ # _ = stats.as_json()
454
+ outfiles.close()
455
+
456
+
457
+ def run_cutseq(args):
458
+ barcode_config = BarcodeConfig(args.adapter_scheme.upper())
459
+ settings = CutadaptConfig()
460
+ settings.rname_suffix = args.with_rname_suffix
461
+ settings.ensure_inline_barcode = args.ensure_inline_barcode
462
+ settings.trim_polyA = args.trim_polyA
463
+ settings.threads = args.threads
464
+ settings.min_length = args.min_length
465
+ settings.dry_run = args.dry_run
466
+ if len(args.input_file) == 1:
467
+ pipeline_single(
468
+ args.input_file[0],
469
+ args.output_file[0],
470
+ args.short_file[0],
471
+ args.untrimmed_file[0],
472
+ barcode_config,
473
+ settings,
474
+ )
475
+ else:
476
+ pipeline_paired(
477
+ args.input_file[0],
478
+ args.input_file[1],
479
+ args.output_file[0],
480
+ args.output_file[1],
481
+ args.short_file[0],
482
+ args.short_file[1],
483
+ args.untrimmed_file[0],
484
+ args.untrimmed_file[1],
485
+ barcode_config,
486
+ settings,
487
+ )
488
+
489
+
490
+ def main():
491
+ parser = argparse.ArgumentParser(
492
+ description="Trim sequencing adapters from NGS data automatically."
493
+ )
494
+ # input file can be one or two for single or paired-end reads, but can not be more than two
495
+ parser.add_argument(
496
+ "input_file",
497
+ type=str,
498
+ nargs="+",
499
+ help="Input file path for NGS data, one or two files.",
500
+ )
501
+ # output file can be number of files matching the input files, if not provided it will generate based on the output prefix,
502
+ # if no output prefix provided it will generate based on the input file name
503
+ parser.add_argument(
504
+ "-a",
505
+ "--adapter-scheme",
506
+ type=str,
507
+ help="Adapter sequence configuration.",
508
+ )
509
+ parser.add_argument("-A", "--adapter-name", type=str, help="Built-in adapter name.")
510
+ parser.add_argument(
511
+ "-O",
512
+ "--output-prefix",
513
+ type=str,
514
+ help="Output file prefix for keep trimmed data.",
515
+ )
516
+ parser.add_argument(
517
+ "-o",
518
+ "--output-file",
519
+ type=str,
520
+ nargs="+",
521
+ help="Output file path for keep trimmed data.",
522
+ )
523
+
524
+ parser.add_argument(
525
+ "-q",
526
+ "--min-quality",
527
+ type=int,
528
+ default=20,
529
+ help="Minimum quality of the read tails in the reads to keep.",
530
+ )
531
+ # discard short reads
532
+ parser.add_argument(
533
+ "-S",
534
+ "--short-file",
535
+ type=str,
536
+ nargs="+",
537
+ help="Output file path for discarded too short data.",
538
+ )
539
+ parser.add_argument(
540
+ "-m",
541
+ "--min-length",
542
+ type=int,
543
+ default=20,
544
+ help="Minimum length of the reads to keep.",
545
+ )
546
+
547
+ parser.add_argument(
548
+ "--with-rname-suffix",
549
+ action="store_true",
550
+ help="R1 and R2 suffix cotains suffix. MGI platform.",
551
+ )
552
+
553
+ parser.add_argument(
554
+ "--ensure-inline-barcode",
555
+ action="store_true",
556
+ help="Output discarded reads without inline barcode.",
557
+ )
558
+ parser.add_argument(
559
+ "-U",
560
+ "--untrimmed-file",
561
+ type=str,
562
+ nargs="+",
563
+ help="Output file path for discarded reads without inline barcode.",
564
+ )
565
+
566
+ parser.add_argument("--trim-polyA", action="store_true", help="Trim polyA tail.")
567
+
568
+ parser.add_argument(
569
+ "-t",
570
+ "--threads",
571
+ type=int,
572
+ default=1,
573
+ help="Number of threads to use for trimming.",
574
+ )
575
+ parser.add_argument(
576
+ "-n",
577
+ "--dry-run",
578
+ action="store_true",
579
+ help="Print command instead of running it.",
580
+ )
581
+ args = parser.parse_args()
582
+
583
+ if args.adapter_name is not None:
584
+ if args.adapter_scheme is not None:
585
+ logging.info("Adapter scheme is provided, ignore adapter name.")
586
+ else:
587
+ if args.adapter_name.upper() == "TAKARAV2":
588
+ args.adapter_scheme = "ACACGACGCTCTTCCGATCTX<XXXAGATCGGAAGAGCACACGTC"
589
+ elif args.adapter_name.upper() == "STRANDED":
590
+ args.adapter_scheme = "ACACGACGCTCTTCCGATCTX<XXXAGATCGGAAGAGCACACGTC"
591
+ elif args.adapter_name.upper() == "TAKARAV3":
592
+ args.adapter_scheme = (
593
+ "ACACGACGCTCTTCCGATCTXXX<XXXXXXNNNNNNNNAGATCGGAAGAGCACACGTC"
594
+ )
595
+ else:
596
+ logging.error("Adapter name is not valid.")
597
+ sys.exit(1)
598
+ elif args.adapter_scheme is None:
599
+ logging.error("Adapter scheme or name is required.")
600
+ sys.exit(1)
601
+
602
+ if len(args.input_file) > 2:
603
+ logging.error("Input file can not be more than two.")
604
+ sys.exit(1)
605
+
606
+ def validate_output_file(output_files, input_files, output_prefix, output_suffix):
607
+ default_format = ".fastq.gz"
608
+ r1 = "_" + output_suffix + "_R1" + default_format
609
+ r2 = "_" + output_suffix + "_R2" + default_format
610
+ if output_files:
611
+ if len(output_files) != len(input_files):
612
+ logging.error("Output file should be same as input file.")
613
+ sys.exit(1)
614
+ return output_files
615
+ elif output_prefix is not None:
616
+ if len(input_files) == 1:
617
+ return [output_prefix + r1]
618
+ else:
619
+ return [output_prefix + r1, output_prefix + r2]
620
+ else:
621
+ if len(input_files) == 1:
622
+ return [remove_fq_suffix(input_files[0]) + r1]
623
+ else:
624
+ return [
625
+ remove_fq_suffix(input_files[0]) + r1,
626
+ remove_fq_suffix(input_files[1]) + r2,
627
+ ]
628
+ return output_files
629
+
630
+ args.output_file = validate_output_file(
631
+ args.output_file, args.input_file, args.output_prefix, "trimmed"
632
+ )
633
+ args.short_file = validate_output_file(
634
+ args.short_file, args.input_file, args.output_prefix, "short"
635
+ )
636
+ args.untrimmed_file = validate_output_file(
637
+ args.untrimmed_file, args.input_file, args.output_prefix, "untrimmed"
638
+ )
639
+
640
+ run_cutseq(args)
641
+
642
+
643
+ if __name__ == "__main__":
644
+ main()
@@ -40,23 +40,29 @@ def remove_fq_suffix(f):
40
40
  return f
41
41
 
42
42
 
43
+ class BarcodeSeq:
44
+ def __init__(self, seq):
45
+ self.fw = seq
46
+ self.rc = reverse_complement(seq)
47
+ self.len = len(seq)
48
+
49
+
43
50
  class BarcodeConfig:
51
+ """
52
+ Adapter scheme:
53
+ (p5)(inline5)(umi5)(mask5)(strand)(mask3)(umi3)(inline3)(p7)
54
+ """
55
+
44
56
  def __init__(self, adapter=None):
45
57
  self.strand = None
46
- self.p5_fw = ""
47
- self.p5_rc = ""
48
- self.p7_fw = ""
49
- self.p7_rc = ""
50
- self.inline5_fw = ""
51
- self.inline5_rc = ""
52
- self.inline5 = 0
53
- self.inline3_fw = ""
54
- self.inline3_rc = ""
55
- self.inline3 = 0
56
- self.umi5 = 0
57
- self.umi3 = 0
58
- self.mask5 = 0
59
- self.mask3 = 0
58
+ self.p5 = BarcodeSeq("")
59
+ self.p7 = BarcodeSeq("")
60
+ self.inline5 = BarcodeSeq("")
61
+ self.inline3 = BarcodeSeq("")
62
+ self.umi5 = BarcodeSeq("")
63
+ self.umi3 = BarcodeSeq("")
64
+ self.mask5 = BarcodeSeq("")
65
+ self.mask3 = BarcodeSeq("")
60
66
  if adapter is not None:
61
67
  self._parse_barcode(adapter)
62
68
 
@@ -74,20 +80,14 @@ class BarcodeConfig:
74
80
  if d["inline3"] is None:
75
81
  d["inline3"] = ""
76
82
  self.strand = "+" if d["strand"] == ">" else "-" if d["strand"] == "<" else None
77
- self.p5_fw = d["p5"]
78
- self.p5_rc = reverse_complement(d["p5"])
79
- self.p7_fw = d["p7"]
80
- self.p7_rc = reverse_complement(d["p7"])
81
- self.inline5_fw = d["inline5"] if d["inline5"] else ""
82
- self.inline5_rc = reverse_complement(d["inline5"]) if d["inline5"] else ""
83
- self.inline5 = len(d["inline5"])
84
- self.inline3_fw = d["inline3"] if d["inline3"] else ""
85
- self.inline3_rc = reverse_complement(d["inline3"]) if d["inline3"] else ""
86
- self.inline3 = len(d["inline3"])
87
- self.umi5 = len(d["umi5"])
88
- self.umi3 = len(d["umi3"])
89
- self.mask5 = len(d["mask5"])
90
- self.mask3 = len(d["mask3"])
83
+ self.p5 = BarcodeSeq(d["p5"])
84
+ self.p7 = BarcodeSeq(d["p7"])
85
+ self.inline5 = BarcodeSeq(d["inline5"])
86
+ self.inline3 = BarcodeSeq(d["inline3"])
87
+ self.umi5 = BarcodeSeq(d["umi5"])
88
+ self.umi3 = BarcodeSeq(d["umi3"])
89
+ self.mask5 = BarcodeSeq(d["mask5"])
90
+ self.mask3 = BarcodeSeq(d["mask3"])
91
91
 
92
92
 
93
93
  class CutadaptConfig:
@@ -112,19 +112,19 @@ def run_cutadapt_PE(
112
112
  else:
113
113
  config_rname = ""
114
114
  steps.append(
115
- f"{cutadapt}{config_rname} -e 0.25 -n 2 -O 10 -g '{barcode.p5_fw};rightmost' -G '{barcode.p7_rc};rightmost' --interleaved {input1} {input2}"
115
+ f"{cutadapt}{config_rname} -e 0.25 -n 2 -O 10 -g '{barcode.p5.fw};rightmost' -G '{barcode.p7.rc};rightmost' --interleaved {input1} {input2}"
116
116
  )
117
117
  # step 2: remove adapter on the 3' end, read though in the sequencing
118
118
  steps.append(
119
- f"{cutadapt} -e 0.2 -n 2 -O 3 -a '{barcode.p7_fw}' -A '{barcode.p5_rc}' --interleaved -"
119
+ f"{cutadapt} -e 0.2 -n 2 -O 3 -a '{barcode.p7.fw}' -A '{barcode.p5.rc}' --interleaved -"
120
120
  )
121
121
  # step 3: trim inline barcode
122
122
  config_inline_args = []
123
- if barcode.inline5 > 0:
124
- config_inline_args.append(f"-g ^{barcode.inline5_fw} -U -{barcode.inline5}")
125
- if barcode.inline3 > 0:
126
- config_inline_args.append(f"-G ^{barcode.inline3_rc} -u -{barcode.inline3}")
127
- if barcode.inline5 + barcode.inline3 > 0:
123
+ if barcode.inline5.len > 0:
124
+ config_inline_args.append(f"-g ^{barcode.inline5.fw} -U -{barcode.inline5.len}")
125
+ if barcode.inline3.len > 0:
126
+ config_inline_args.append(f"-G ^{barcode.inline3.rc} -u -{barcode.inline3.len}")
127
+ if barcode.inline5.len + barcode.inline3.len > 0:
128
128
  if settings.discarded_untrimmed:
129
129
  config_inline_args.append(
130
130
  f"--untrimmed-output={discard1} --untrimmed-paired-output={discard2}"
@@ -132,16 +132,16 @@ def run_cutadapt_PE(
132
132
  config_inline = " ".join(config_inline_args)
133
133
  steps.append(f"{cutadapt} {config_inline} --interleaved -")
134
134
  # step 4: extract UMI
135
- if barcode.umi5 + barcode.umi3 > 0:
135
+ if barcode.umi5.len + barcode.umi3.len > 0:
136
136
  steps.append(
137
- f"{cutadapt} -u {barcode.umi5} -u -{barcode.umi3} -U {barcode.umi3} -U -{barcode.umi5} --rename='{{id}}_{{r1.cut_prefix}}{{r2.cut_prefix}}' --interleaved -"
137
+ f"{cutadapt} -u {barcode.umi5.len} -u -{barcode.umi3.len} -U {barcode.umi3.len} -U -{barcode.umi5.len} --rename='{{id}}_{{r1.cut_prefix}}{{r2.cut_prefix}}' --interleaved -"
138
138
  )
139
139
  else:
140
140
  steps.append(f"{cutadapt} --rename='{{id}}' --interleaved -")
141
141
  # step 5: mask tail in the RNA, which might be artifact of RT
142
- if barcode.mask5 + barcode.mask3 > 0:
142
+ if barcode.mask5.len + barcode.mask3.len > 0:
143
143
  steps.append(
144
- f"{cutadapt} -u {barcode.mask5} -u -{barcode.mask3} -U {barcode.mask3} -U -{barcode.mask5} --interleaved -"
144
+ f"{cutadapt} -u {barcode.mask5.len} -u -{barcode.mask3.len} -U {barcode.mask3.len} -U -{barcode.mask5.len} --interleaved -"
145
145
  )
146
146
  # step 6: trim polyA
147
147
  if settings.trim_polyA:
@@ -171,29 +171,29 @@ def run_cutadapt_SE(input1, output1, discard1, barcode, settings):
171
171
  else:
172
172
  config_rname = ""
173
173
  steps.append(
174
- f"{cutadapt}{config_rname} -e 0.25 -n 2 -O 10 -g '{barcode.p5_fw};rightmost' {input1}"
174
+ f"{cutadapt}{config_rname} -e 0.25 -n 2 -O 10 -g '{barcode.p5.fw};rightmost' {input1}"
175
175
  )
176
176
  # step 2: remove adapter on the 3' end, read though in the sequencing
177
- steps.append(f"{cutadapt} -e 0.2 -n 2 -O 3 -a '{barcode.p7_fw}' -")
177
+ steps.append(f"{cutadapt} -e 0.2 -n 2 -O 3 -a '{barcode.p7.fw}' -")
178
178
  # step 3: trim inline barcode
179
179
  config_inline_args = []
180
- if barcode.inline3 > 0:
181
- config_inline_args.append(f"-a {barcode.inline3_fw}$")
182
- if barcode.inline5 > 0:
183
- config_inline_args.append(f"-g ^{barcode.inline5_fw}")
184
- if barcode.inline5 + barcode.inline3 > 0:
180
+ if barcode.inline3.len > 0:
181
+ config_inline_args.append(f"-a {barcode.inline3.fw}$")
182
+ if barcode.inline5.len > 0:
183
+ config_inline_args.append(f"-g ^{barcode.inline5.fw}")
184
+ if barcode.inline5.len + barcode.inline3.len > 0:
185
185
  if settings.discarded_untrimmed:
186
186
  config_inline_args.append(f"--untrimmed-output={discard1}")
187
187
 
188
188
  config_inline = " ".join(config_inline_args)
189
189
  steps.append(f"{cutadapt} {config_inline} -")
190
190
  # step 4: extract UMI
191
- if barcode.umi5 + barcode.umi3 > 0:
191
+ if barcode.umi5.len + barcode.umi3.len > 0:
192
192
  steps.append(
193
- f"{cutadapt} -u {barcode.umi5} -u -{barcode.umi3} --rename='{{id}}_{{cut_prefix}}{{cut_suffix}}' -"
193
+ f"{cutadapt} -u {barcode.umi5.len} -u -{barcode.umi3.len} --rename='{{id}}_{{cut_prefix}}{{cut_suffix}}' -"
194
194
  )
195
195
  # step 5: mask tail in the RNA, which might be artifact of RT
196
- steps.append(f"{cutadapt} -u {barcode.mask5} -u -{barcode.mask3} -")
196
+ steps.append(f"{cutadapt} -u {barcode.mask5.len} -u -{barcode.mask3.len} -")
197
197
  # step 6: trim polyA
198
198
  if settings.trim_polyA:
199
199
  if barcode.strand == "+":
@@ -233,7 +233,6 @@ def run_cutseq(args):
233
233
  settings.threads = args.threads
234
234
  settings.min_length = args.min_length
235
235
  settings.dry_run = args.dry_run
236
- # Example command setup, you'll need to expand this based on your actual requirements
237
236
  if len(args.input_file) == 1:
238
237
  steps = run_cutadapt_SE(
239
238
  args.input_file[0],
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cutseq"
3
- version = "0.0.3"
3
+ version = "0.0.5"
4
4
  description = "Automatically cut adapter / barcode / UMI from NGS data"
5
5
  authors = ["Ye Chang <yech1990@gmail.com>"]
6
6
  license = "MIT"
cutseq-0.0.3/README.md DELETED
@@ -1,5 +0,0 @@
1
- #✂️ CutSeq
2
-
3
- ## How to use?
4
-
5
- `cutseq -a "ACACGACGCTCTTCCGATCTX<XXXAGATCGGAAGAGCACACGTC" test_R1.fq.gz test_R2.fq.gz`