cutseq 0.0.4__tar.gz → 0.0.6__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.4
3
+ Version: 0.0.6
4
4
  Summary: Automatically cut adapter / barcode / UMI from NGS data
5
5
  Home-page: https://github.com/y9c/cutseq
6
6
  License: MIT
@@ -7,6 +7,7 @@
7
7
  # Created: 2024-04-19 18:57
8
8
 
9
9
  import argparse
10
+ import importlib.metadata
10
11
  import logging
11
12
  import re
12
13
  import subprocess
@@ -18,6 +19,7 @@ from cutadapt.adapters import (
18
19
  NonInternalFrontAdapter,
19
20
  PrefixAdapter,
20
21
  RightmostFrontAdapter,
22
+ SuffixAdapter,
21
23
  )
22
24
  from cutadapt.files import InputPaths, OutputFiles
23
25
  from cutadapt.modifiers import (
@@ -29,18 +31,33 @@ from cutadapt.modifiers import (
29
31
  UnconditionalCutter,
30
32
  )
31
33
  from cutadapt.pipeline import PairedEndPipeline, SingleEndPipeline
32
- from cutadapt.predicates import IsUntrimmed, TooShort
34
+ from cutadapt.predicates import Predicate, TooShort
35
+ from cutadapt.report import Statistics
33
36
  from cutadapt.runners import make_runner
34
37
  from cutadapt.steps import (
35
- InfoFileWriter,
36
38
  PairedEndFilter,
37
39
  PairedEndSink,
38
- PairedSingleEndStep,
39
40
  SingleEndFilter,
40
41
  SingleEndSink,
41
42
  )
42
43
  from cutadapt.utils import Progress
43
44
 
45
+ # monkey patching ....
46
+ original_method = Statistics._collect_modifier
47
+
48
+
49
+ def patched_problematic_method(self, *args, **kwargs):
50
+ try:
51
+ return original_method(self, *args, **kwargs)
52
+ except AssertionError:
53
+ pass
54
+
55
+
56
+ Statistics._collect_modifier = patched_problematic_method
57
+
58
+
59
+ __version__ = importlib.metadata.version(__package__ or __name__)
60
+
44
61
  logging.basicConfig(
45
62
  level=logging.INFO,
46
63
  format="%(asctime)s - %(levelname)s - %(message)s",
@@ -118,7 +135,7 @@ class BarcodeConfig:
118
135
  class CutadaptConfig:
119
136
  def __init__(self):
120
137
  self.rname_suffix = False
121
- self.discarded_untrimmed = False
138
+ self.ensure_inline_barcode = False
122
139
  self.trim_polyA = False
123
140
  self.min_length = 20
124
141
  self.min_quality = 20
@@ -126,6 +143,25 @@ class CutadaptConfig:
126
143
  self.threads = 1
127
144
 
128
145
 
146
+ class IsUntrimmedAny(Predicate):
147
+ """
148
+ Select reads for which no adapter match was found
149
+ """
150
+
151
+ def __init__(self, ref_adapters):
152
+ self.ref_adapters = ref_adapters
153
+
154
+ def __repr__(self):
155
+ return "IsUntrimmedAny()"
156
+
157
+ def test(self, read, info):
158
+ # check not all adapters with ref_adapters are exist in the matches
159
+ match_adapters = [match.adapter for match in info.matches]
160
+ if any([adapter not in match_adapters for adapter in self.ref_adapters]):
161
+ return True
162
+ return False
163
+
164
+
129
165
  def run_steps(steps, dry_run=False):
130
166
  if dry_run:
131
167
  print(
@@ -138,7 +174,7 @@ def run_steps(steps, dry_run=False):
138
174
  return process.stdout.decode(), process.stderr.decode()
139
175
 
140
176
 
141
- def pipeline_single(input1, output1, short1, untrimed1, barcode, settings):
177
+ def pipeline_single(input1, output1, short1, untrimmed1, barcode, settings):
142
178
  modifiers = []
143
179
  # step 1: remove suffix in the read name
144
180
  modifiers.extend([SuffixRemover(".1"), SuffixRemover("/1")])
@@ -162,14 +198,11 @@ def pipeline_single(input1, output1, short1, untrimed1, barcode, settings):
162
198
  )
163
199
  # step 4: trim inline barcode
164
200
  if barcode.inline5.len > 0:
165
- modifiers.append(
166
- AdapterCutter(
167
- [PrefixAdapter(sequence=barcode.inline5.fw, max_errors=0.2)],
168
- times=1,
169
- )
170
- )
201
+ adapter_inline5 = PrefixAdapter(sequence=barcode.inline5.fw, max_errors=0.2)
202
+ modifiers.append(AdapterCutter([adapter_inline5], times=1))
171
203
  if barcode.inline3.len > 0:
172
- modifiers.append(UnconditionalCutter(-barcode.inline3.len))
204
+ adapter_inline3 = SuffixAdapter(sequence=barcode.inline3.fw, max_errors=0.2)
205
+ modifiers.append(AdapterCutter([adapter_inline3], times=1))
173
206
 
174
207
  # step 5: extract UMI
175
208
  if barcode.umi5.len > 0:
@@ -215,19 +248,35 @@ def pipeline_single(input1, output1, short1, untrimed1, barcode, settings):
215
248
  qualities=runner.input_file_format().has_qualities(),
216
249
  interleaved=False,
217
250
  )
218
- steps = [
251
+ steps = []
252
+ steps.append(
219
253
  # --info-file=info.txt
220
254
  # PairedSingleEndStep(InfoFileWriter(outfiles.open_text("info.txt"))),
221
255
  # -m 10
222
256
  SingleEndFilter(
223
257
  TooShort(settings.min_length), outfiles.open_record_writer(short1)
224
258
  ),
225
- # TODO: --max-n=0 support
226
- # --discard-untrimmed
227
- # SingleEndFilter( IsUntrimmed(), IsUntrimmed(), pair_filter_mode="any"),
259
+ )
260
+ if (
261
+ settings.ensure_inline_barcode
262
+ and barcode.inline5.len + barcode.inline3.len > 0
263
+ ):
264
+ ref_adapters = []
265
+ if barcode.inline5.len > 0:
266
+ ref_adapters.append(adapter_inline5)
267
+ if barcode.inline3.len > 0:
268
+ ref_adapters.append(adapter_inline3)
269
+ steps.append(
270
+ # TODO: --max-n=0 support
271
+ SingleEndFilter(
272
+ IsUntrimmedAny(ref_adapters),
273
+ outfiles.open_record_writer(untrimmed1, interleaved=False),
274
+ ),
275
+ )
276
+ steps.append(
228
277
  # -o
229
278
  SingleEndSink(outfiles.open_record_writer(output1, interleaved=False)),
230
- ]
279
+ )
231
280
  pipeline = SingleEndPipeline(modifiers, steps)
232
281
  _stats = runner.run(pipeline, Progress(), outfiles)
233
282
  # _ = stats.as_json()
@@ -241,7 +290,7 @@ def pipeline_paired(
241
290
  output2,
242
291
  short1,
243
292
  short2,
244
- untrimed1,
293
+ untrimmed1,
245
294
  untrimmed2,
246
295
  barcode,
247
296
  settings,
@@ -290,23 +339,19 @@ def pipeline_paired(
290
339
  )
291
340
  # step 4: trim inline barcode
292
341
  if barcode.inline5.len > 0:
342
+ adapter_inline5 = PrefixAdapter(sequence=barcode.inline5.fw, max_errors=0.2)
293
343
  modifiers.append(
294
344
  (
295
- AdapterCutter(
296
- [PrefixAdapter(sequence=barcode.inline5.fw, max_errors=0.2)],
297
- times=1,
298
- ),
345
+ AdapterCutter([adapter_inline5], times=1),
299
346
  UnconditionalCutter(-barcode.inline5.len),
300
347
  )
301
348
  )
302
349
  if barcode.inline3.len > 0:
350
+ adapter_inline3 = PrefixAdapter(sequence=barcode.inline3.rc, max_errors=0.2)
303
351
  modifiers.append(
304
352
  (
305
353
  UnconditionalCutter(-barcode.inline3.len),
306
- AdapterCutter(
307
- [BackAdapter(sequence=barcode.inline3.rc, max_errors=0.2)],
308
- times=1,
309
- ),
354
+ AdapterCutter([adapter_inline3], times=1),
310
355
  )
311
356
  )
312
357
 
@@ -387,21 +432,40 @@ def pipeline_paired(
387
432
  qualities=runner.input_file_format().has_qualities(),
388
433
  interleaved=False,
389
434
  )
390
- steps = [
391
- # --info-file=info.txt
392
- # PairedSingleEndStep(InfoFileWriter(outfiles.open_text("info.txt"))),
393
- # -m 10:10
435
+ steps = []
436
+ # --info-file=info.txt
437
+ # PairedSingleEndStep(InfoFileWriter(outfiles.open_text("info.txt"))),
438
+ # -m 10:10
439
+ steps.append(
394
440
  PairedEndFilter(
395
441
  TooShort(settings.min_length),
396
442
  TooShort(settings.min_length),
397
443
  outfiles.open_record_writer(short1, short2, interleaved=False),
398
- ),
399
- # TODO: --max-n=0 support
400
- # --discard-untrimmed
401
- # PairedEndFilter( IsUntrimmed(), IsUntrimmed(), pair_filter_mode="any"),
444
+ )
445
+ )
446
+ if (
447
+ settings.ensure_inline_barcode
448
+ and barcode.inline5.len + barcode.inline3.len > 0
449
+ ):
450
+ steps.append(
451
+ # TODO: --max-n=0 support
452
+ PairedEndFilter(
453
+ IsUntrimmedAny([adapter_inline5])
454
+ if barcode.inline5.len > 0
455
+ else None,
456
+ IsUntrimmedAny([adapter_inline3])
457
+ if barcode.inline3.len > 0
458
+ else None,
459
+ outfiles.open_record_writer(
460
+ untrimmed1, untrimmed2, interleaved=False
461
+ ),
462
+ pair_filter_mode="any",
463
+ )
464
+ )
465
+ steps.append(
402
466
  # -o ... -p ...
403
- PairedEndSink(outfiles.open_record_writer(output1, output2)),
404
- ]
467
+ PairedEndSink(outfiles.open_record_writer(output1, output2))
468
+ )
405
469
  pipeline = PairedEndPipeline(modifiers, steps)
406
470
  _stats = runner.run(pipeline, Progress(), outfiles)
407
471
  # _ = stats.as_json()
@@ -411,12 +475,9 @@ def pipeline_paired(
411
475
  def run_cutseq(args):
412
476
  barcode_config = BarcodeConfig(args.adapter_scheme.upper())
413
477
  settings = CutadaptConfig()
414
- if args.with_rname_suffix:
415
- settings.rname_suffix = True
416
- if args.untrimmed_file is None:
417
- settings.discarded_untrimmed = True
418
- if args.trim_polyA:
419
- settings.trim_polyA = True
478
+ settings.rname_suffix = args.with_rname_suffix
479
+ settings.ensure_inline_barcode = args.ensure_inline_barcode
480
+ settings.trim_polyA = args.trim_polyA
420
481
  settings.threads = args.threads
421
482
  settings.min_length = args.min_length
422
483
  settings.dry_run = args.dry_run
@@ -506,6 +567,12 @@ def main():
506
567
  action="store_true",
507
568
  help="R1 and R2 suffix cotains suffix. MGI platform.",
508
569
  )
570
+
571
+ parser.add_argument(
572
+ "--ensure-inline-barcode",
573
+ action="store_true",
574
+ help="Output discarded reads without inline barcode.",
575
+ )
509
576
  parser.add_argument(
510
577
  "-U",
511
578
  "--untrimmed-file",
@@ -529,6 +596,11 @@ def main():
529
596
  action="store_true",
530
597
  help="Print command instead of running it.",
531
598
  )
599
+
600
+ parser.add_argument(
601
+ "-V", "--version", action="version", version=f"%(prog)s {__version__}"
602
+ )
603
+
532
604
  args = parser.parse_args()
533
605
 
534
606
  if args.adapter_name is not None:
@@ -584,12 +656,8 @@ def main():
584
656
  args.short_file = validate_output_file(
585
657
  args.short_file, args.input_file, args.output_prefix, "short"
586
658
  )
587
- args.untrimmed_file = (
588
- validate_output_file(
589
- args.untrimmed_file, args.input_file, args.output_prefix, "untrimmed"
590
- )
591
- if args.untrimmed_file is not None
592
- else [None] * len(args.input_file)
659
+ args.untrimmed_file = validate_output_file(
660
+ args.untrimmed_file, args.input_file, args.output_prefix, "untrimmed"
593
661
  )
594
662
 
595
663
  run_cutseq(args)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cutseq"
3
- version = "0.0.4"
3
+ version = "0.0.6"
4
4
  description = "Automatically cut adapter / barcode / UMI from NGS data"
5
5
  authors = ["Ye Chang <yech1990@gmail.com>"]
6
6
  license = "MIT"
File without changes
File without changes