@sjcrh/proteinpaint-rust 2.192.0 → 2.193.0

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.
package/Cargo.toml CHANGED
@@ -103,10 +103,6 @@ path="src/readHDF5.rs"
103
103
  name="validateHDF5"
104
104
  path="src/validateHDF5.rs"
105
105
 
106
- [[bin]]
107
- name="gdcGRIN2"
108
- path="src/gdcGRIN2.rs"
109
-
110
106
  [[bin]]
111
107
  name="cerno"
112
108
  path="src/cerno.rs"
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.192.0",
2
+ "version": "2.193.0",
3
3
  "name": "@sjcrh/proteinpaint-rust",
4
4
  "type": "module",
5
5
  "description": "Rust-based utilities for proteinpaint",
package/src/gdcGRIN2.rs DELETED
@@ -1,1261 +0,0 @@
1
- /*
2
- This script can either download cohort maf/cnv files from GDC or read them from local files, with default behavior being to download from GDC. It gracefully handles timeout and other possible errors related to GDC API processing or file reading for use by the client file summary div.
3
-
4
- Key improvements:
5
- 1. Graceful error handling - individual file failures don't stop the entire process
6
- 2. Better timeout handling with retries
7
- 3. More detailed error reporting
8
- 4. Continues processing even when some files fail
9
- 5. Added chromosome filtering
10
- 6. Supports reading from local files with --from-file flag
11
-
12
- Command-line arguments:
13
- - --from-file: Read data from local files instead of downloading from GDC
14
-
15
- Input JSON:
16
- caseFiles
17
- mafOptions: For SNVindel filtering
18
- cnvOptions: For CNV filtering
19
- chromosomes: chromosomes will be included:[]
20
-
21
- Output mutations as JSON array.
22
- {
23
- grin2lesion:str,
24
- summary:{}
25
- }
26
-
27
- Example of usage:
28
- echo '{"caseFiles": {"MP2PRT-PATFJE": {"maf": "26ea7b6f-8bc4-4e83-ace1-2125b493a361"},"MP2PRT-PAPIGD": {"maf": "653d7458-f4af-4328-a1ce-3bbf22a2e347"}, "TCGA-CG-4300": { "cnv":"46372ec2-ff79-4d07-b375-9ba8a12c11f3", "maf":"c09b208d-2e7b-4116-9580-27f20f4c7e67"}},"mafOptions": {"minTotalDepth": 10,"minAltAlleleCount": 2,"hyperMutator":8000,"consequences":["missense_variant","frameshift_variant"]}, "cnvOptions":{"lossThreshold":-0.4, "gainThreshold": 0.3, "segLength":2000000, "hyperMutator":500}, "chromosomes":["chr1","chr2","chr3"], "max_record": 100000}' | ./target/release/gdcGRIN2
29
- Example of usage (read from local files):
30
- echo '{"caseFiles": {"MP2PRT-PATFJE": {"maf": "26ea7b6f-8bc4-4e83-ace1-2125b493a361"},"MP2PRT-PAPIGD": {"maf": "653d7458-f4af-4328-a1ce-3bbf22a2e347"}, "TCGA-CG-4300": { "cnv":"46372ec2-ff79-4d07-b375-9ba8a12c11f3", "maf":"c09b208d-2e7b-4116-9580-27f20f4c7e67"}},"mafOptions": {"minTotalDepth": 10,"minAltAlleleCount": 2,"hyperMutator":8000,"consequences":["missense_variant","frameshift_variant"]}, "cnvOptions":{"lossThreshold":-0.4, "gainThreshold": 0.3, "segLength":2000000, "hyperMutator":500}, "chromosomes":["chr1","chr2","chr3"], "max_record": 100000}' | ./target/release/gdcGRIN2 --from-file
31
-
32
- */
33
-
34
- use flate2::read::GzDecoder;
35
- use futures::StreamExt;
36
- use memchr::memchr;
37
- use serde::{Deserialize, Serialize};
38
- use serde_json;
39
- use std::collections::{HashMap, HashSet};
40
- use std::env;
41
- use std::fs;
42
- use std::io::{self, Read};
43
- use std::sync::Arc;
44
- use std::sync::atomic::{AtomicUsize, Ordering};
45
- use std::time::Duration;
46
- use tokio::io::{AsyncReadExt, BufReader};
47
- use tokio::sync::Mutex;
48
- use tokio::time::timeout;
49
-
50
- // Struct to hold error information for JSON output
51
- #[derive(serde::Serialize, Clone)]
52
- struct ErrorEntry {
53
- case_id: String,
54
- data_type: String,
55
- error_type: String,
56
- error_details: String,
57
- attempts_made: u32,
58
- }
59
-
60
- // Define the structure for datadd
61
- #[derive(Deserialize, Debug)]
62
- struct DataType {
63
- cnv: Option<String>,
64
- maf: Option<String>,
65
- }
66
-
67
- // Define the structure for mafOptions
68
- #[derive(Deserialize, Debug)]
69
- struct MafOptions {
70
- #[serde(rename = "minTotalDepth")]
71
- min_total_depth: i32,
72
- #[serde(rename = "minAltAlleleCount")]
73
- min_alt_allele_count: i32,
74
- #[serde(rename = "hyperMutator")]
75
- hyper_mutator: i32,
76
- consequences: Option<Vec<String>>, // Optional list of consequences to filter MAF files
77
- }
78
-
79
- // Define the structure for cnvOptions
80
- #[derive(Deserialize, Debug)]
81
- struct CnvOptions {
82
- #[serde(rename = "lossThreshold")]
83
- loss_threshold: f32,
84
- #[serde(rename = "gainThreshold")]
85
- gain_threshold: f32,
86
- #[serde(rename = "segLength")]
87
- seg_length: i32,
88
- #[serde(rename = "hyperMutator")]
89
- hyper_mutator: i32,
90
- }
91
-
92
- // struct for MAF filter details
93
- #[derive(Clone, Serialize, Default)]
94
- struct FilteredMafDetails {
95
- matched_consequences: HashMap<String, usize>,
96
- rejected_consequences: HashMap<String, usize>,
97
- t_alt_count: usize,
98
- t_depth: usize,
99
- invalid_rows: usize,
100
- excluded_by_min_depth: usize,
101
- excluded_by_min_alt_count: usize,
102
- excluded_by_consequence_type: usize,
103
- total_processed: usize,
104
- total_included: usize,
105
- skipped_chromosomes: HashMap<String, usize>,
106
- }
107
-
108
- // struct for CNV filter details
109
- #[derive(Clone, Serialize, Default)]
110
- struct FilteredCnvDetails {
111
- segment_mean: usize,
112
- seg_length: usize,
113
- invalid_rows: usize,
114
- excluded_by_loss_threshold: usize,
115
- excluded_by_gain_threshold: usize,
116
- excluded_by_segment_length: usize,
117
- total_processed: usize,
118
- total_included: usize,
119
- skipped_chromosomes: HashMap<String, usize>,
120
- }
121
-
122
- // struct for per-case filter details
123
- #[derive(Clone, Serialize)]
124
- struct FilteredCaseDetails {
125
- maf: FilteredMafDetails,
126
- cnv: FilteredCnvDetails,
127
- }
128
-
129
- // Final summary output (JSONL format)
130
- #[derive(serde::Serialize)]
131
- struct FinalSummary {
132
- total_files: usize,
133
- successful_files: usize,
134
- failed_files: usize,
135
- errors: Vec<ErrorEntry>,
136
- filtered_records: usize,
137
- filtered_maf_records: usize,
138
- filtered_cnv_records: usize,
139
- included_maf_records: usize,
140
- included_cnv_records: usize,
141
- filtered_records_by_case: HashMap<String, FilteredCaseDetails>,
142
- hyper_mutator_records: HashMap<String, Vec<String>>,
143
- excluded_by_max_record: HashMap<String, Vec<String>>,
144
- }
145
-
146
- // Enum to hold both SuccessfulFileoutput and FinalSummary
147
- #[derive(Serialize)]
148
- struct Output {
149
- grin2lesion: String,
150
- summary: FinalSummary,
151
- }
152
-
153
- // Define the top-level input structure
154
- #[derive(Deserialize, Debug)]
155
- struct InputData {
156
- #[serde(rename = "caseFiles")]
157
- case_files: HashMap<String, DataType>,
158
- #[serde(rename = "mafOptions")]
159
- maf_options: Option<MafOptions>,
160
- #[serde(rename = "cnvOptions")]
161
- cnv_options: Option<CnvOptions>,
162
- chromosomes: Vec<String>,
163
- max_record: usize,
164
- }
165
-
166
- // Configuration for different data types
167
- #[derive(Deserialize, Debug)]
168
- struct DataTypeConfig {
169
- header_marker: &'static str,
170
- output_columns: Vec<&'static str>,
171
- }
172
-
173
- // Function to parse TSV content
174
- async fn parse_content(
175
- content: &str,
176
- case_id: &str,
177
- data_type: &str,
178
- min_total_depth: i32,
179
- min_alt_allele_count: i32,
180
- maf_hyper_mutator: i32,
181
- consequences: &Option<Vec<String>>,
182
- gain_threshold: f32,
183
- loss_threshold: f32,
184
- seg_length: i32,
185
- cnv_hyper_mutator: i32,
186
- chromosomes: &HashSet<String>,
187
- filtered_records: &Arc<Mutex<HashMap<String, FilteredCaseDetails>>>,
188
- filtered_maf_records: &AtomicUsize,
189
- filtered_cnv_records: &AtomicUsize,
190
- included_maf_records: &AtomicUsize,
191
- included_cnv_records: &AtomicUsize,
192
- hyper_mutator_records: &Arc<Mutex<HashMap<String, Vec<String>>>>,
193
- ) -> Result<Vec<Vec<String>>, (String, String, String)> {
194
- let config = match data_type {
195
- "cnv" => DataTypeConfig {
196
- header_marker: "Segment_Mean",
197
- output_columns: vec!["Chromosome", "Start", "End", "Segment_Mean"],
198
- },
199
- "maf" => DataTypeConfig {
200
- header_marker: "Hugo_Symbol",
201
- output_columns: vec!["Chromosome", "Start_Position", "End_Position", "t_depth", "t_alt_count"],
202
- },
203
- _ => {
204
- return Err((
205
- case_id.to_string(),
206
- data_type.to_string(),
207
- "Invalid data type".to_string(),
208
- ));
209
- }
210
- };
211
-
212
- // check hyperMutator for MAF and CNV files
213
- let hyper_mutator = if data_type == "maf" {
214
- maf_hyper_mutator
215
- } else {
216
- cnv_hyper_mutator
217
- };
218
- if hyper_mutator > 0 {
219
- let line_count = content.lines().count();
220
- if line_count as i32 > hyper_mutator {
221
- let mut hyper_records = hyper_mutator_records.lock().await;
222
- hyper_records
223
- .entry(data_type.to_string())
224
- .or_insert_with(Vec::new)
225
- .push(case_id.to_string());
226
- if data_type == "maf" {
227
- filtered_maf_records.fetch_add(line_count, Ordering::Relaxed);
228
- } else if data_type == "cnv" {
229
- filtered_cnv_records.fetch_add(line_count, Ordering::Relaxed);
230
- }
231
- return Ok(Vec::new());
232
- }
233
- };
234
-
235
- let lines = content.lines();
236
- let mut parsed_data = Vec::new();
237
- let mut columns_indices: Vec<usize> = Vec::new();
238
- let mut variant_classification_index: Option<usize> = None;
239
- let mut header: Vec<String> = Vec::new();
240
-
241
- for line in lines {
242
- if line.starts_with("#") {
243
- continue;
244
- };
245
- if line.contains(config.header_marker) {
246
- header = line.split("\t").map(|s| s.to_string()).collect();
247
- if let Err(err) = setup_columns(
248
- &header,
249
- &config,
250
- &mut columns_indices,
251
- &mut variant_classification_index,
252
- case_id,
253
- data_type,
254
- ) {
255
- return Err(err);
256
- }
257
- continue;
258
- };
259
-
260
- let row = match data_type {
261
- "maf" => {
262
- process_mafline(
263
- line,
264
- case_id,
265
- data_type,
266
- &columns_indices,
267
- variant_classification_index,
268
- consequences,
269
- min_total_depth,
270
- min_alt_allele_count,
271
- chromosomes,
272
- filtered_records,
273
- filtered_maf_records,
274
- included_maf_records,
275
- )
276
- .await
277
- }
278
- "cnv" => {
279
- process_cnvline(
280
- line,
281
- case_id,
282
- data_type,
283
- &header,
284
- &columns_indices,
285
- gain_threshold,
286
- loss_threshold,
287
- seg_length,
288
- chromosomes,
289
- filtered_records,
290
- filtered_cnv_records,
291
- included_cnv_records,
292
- )
293
- .await
294
- }
295
- _ => {
296
- return Err((
297
- case_id.to_string(),
298
- data_type.to_string(),
299
- "Invalid data type".to_string(),
300
- ));
301
- }
302
- }?;
303
-
304
- if let Some(out_lst) = row {
305
- parsed_data.push(out_lst);
306
- };
307
- }
308
-
309
- if columns_indices.is_empty() {
310
- return Err((
311
- case_id.to_string(),
312
- data_type.to_string(),
313
- "No matching columns found. Problematic file!".to_string(),
314
- ));
315
- };
316
-
317
- Ok(parsed_data)
318
- }
319
-
320
- // Set up column indices for processing
321
- fn setup_columns(
322
- header: &[String],
323
- config: &DataTypeConfig,
324
- columns_indices: &mut Vec<usize>,
325
- variant_classification_index: &mut Option<usize>,
326
- case_id: &str,
327
- data_type: &str,
328
- ) -> Result<(), (String, String, String)> {
329
- for col in &config.output_columns {
330
- match header.iter().position(|x| x == col) {
331
- Some(index) => columns_indices.push(index),
332
- None => {
333
- return Err((
334
- case_id.to_string(),
335
- data_type.to_string(),
336
- format!("Column {} was not found", col),
337
- ));
338
- }
339
- }
340
- }
341
-
342
- if data_type == "maf" {
343
- *variant_classification_index = header.iter().position(|x| x == "One_Consequence");
344
- if variant_classification_index.is_none() {
345
- return Err((
346
- case_id.to_string(),
347
- data_type.to_string(),
348
- "Column Variant_Classification was not found".to_string(),
349
- ));
350
- }
351
- }
352
-
353
- Ok(())
354
- }
355
-
356
- // Process a single row of MAF file
357
- async fn process_mafline(
358
- line: &str,
359
- case_id: &str,
360
- data_type: &str,
361
- columns_indices: &[usize],
362
- variant_classification_index: Option<usize>,
363
- consequences: &Option<Vec<String>>,
364
- min_total_depth: i32,
365
- min_alt_allele_count: i32,
366
- chromosomes: &HashSet<String>,
367
- filtered_records: &Arc<Mutex<HashMap<String, FilteredCaseDetails>>>,
368
- filtered_maf_records: &AtomicUsize,
369
- included_maf_records: &AtomicUsize,
370
- ) -> Result<Option<Vec<String>>, (String, String, String)> {
371
- let cont_lst: Vec<String> = line.split("\t").map(|s| s.to_string()).collect();
372
- let mut out_lst = vec![case_id.to_string()];
373
-
374
- // Initialize or update case details
375
- let mut filtered_map = filtered_records.lock().await;
376
- filtered_map
377
- .entry(case_id.to_string())
378
- .or_insert_with(|| FilteredCaseDetails {
379
- maf: FilteredMafDetails::default(),
380
- cnv: FilteredCnvDetails::default(),
381
- });
382
- let case_details = filtered_map.get_mut(case_id).unwrap();
383
-
384
- // Track total processed records
385
- case_details.maf.total_processed += 1;
386
-
387
- // Handle consequence filtering and counting for MAF files
388
-
389
- if let Some(var_class_idx) = variant_classification_index {
390
- if var_class_idx < cont_lst.len() {
391
- let variant_classification = &cont_lst[var_class_idx];
392
- if let Some(consequence_filter) = consequences {
393
- if !consequence_filter.is_empty() {
394
- if consequence_filter.contains(variant_classification) {
395
- // Matched consequence
396
- *case_details
397
- .maf
398
- .matched_consequences
399
- .entry(variant_classification.to_string())
400
- .or_insert(0) += 1;
401
- } else {
402
- // Unmatched consequence
403
- *case_details
404
- .maf
405
- .rejected_consequences
406
- .entry(variant_classification.to_string())
407
- .or_insert(0) += 1;
408
- case_details.maf.excluded_by_consequence_type += 1;
409
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
410
- return Ok(None);
411
- }
412
- } else {
413
- // Empty filter, count as matched
414
- *case_details
415
- .maf
416
- .matched_consequences
417
- .entry(variant_classification.to_string())
418
- .or_insert(0) += 1;
419
- }
420
- } else {
421
- // No filter, count as matched
422
- *case_details
423
- .maf
424
- .matched_consequences
425
- .entry(variant_classification.to_string())
426
- .or_insert(0) += 1;
427
- }
428
- } else {
429
- case_details.maf.invalid_rows += 1;
430
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
431
- return Ok(None);
432
- }
433
- } else {
434
- case_details.maf.invalid_rows += 1;
435
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
436
- return Ok(None);
437
- }
438
-
439
- // Extract relevant columns
440
- for &x in columns_indices {
441
- if x >= cont_lst.len() {
442
- case_details.maf.invalid_rows += 1;
443
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
444
- return Ok(None); // Invalid row
445
- }
446
- let element = cont_lst[x].to_string();
447
- out_lst.push(element);
448
- }
449
-
450
- // Additional MAF-specific processing
451
- if out_lst.len() < 6 {
452
- case_details.maf.invalid_rows += 1;
453
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
454
- return Ok(None); // Not enough columns
455
- }
456
-
457
- let alle_depth = out_lst[4].parse::<i32>().map_err(|_| {
458
- case_details.maf.invalid_rows += 1;
459
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
460
- (
461
- case_id.to_string(),
462
- data_type.to_string(),
463
- "Failed to convert t_depth to integer.".to_string(),
464
- )
465
- })?;
466
-
467
- let alt_count = out_lst[5].parse::<i32>().map_err(|_| {
468
- case_details.maf.invalid_rows += 1;
469
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
470
- (
471
- case_id.to_string(),
472
- data_type.to_string(),
473
- "Failed to convert t_alt_count to integer.".to_string(),
474
- )
475
- })?;
476
-
477
- if alle_depth < min_total_depth {
478
- case_details.maf.t_depth += 1;
479
- case_details.maf.excluded_by_min_depth += 1;
480
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
481
- return Ok(None);
482
- }
483
- if alt_count < min_alt_allele_count {
484
- case_details.maf.t_alt_count += 1;
485
- case_details.maf.excluded_by_min_alt_count += 1;
486
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
487
- return Ok(None);
488
- }
489
-
490
- // Keep case_id, chr, start, end, and add "mutation"
491
- out_lst = out_lst[0..4].to_vec();
492
- out_lst.push("mutation".to_string());
493
-
494
- // adding 'chr' to chromosome if it is not start with 'chr'
495
- if !out_lst[1].starts_with("chr") {
496
- out_lst[1] = format!("chr{}", out_lst[1]);
497
- }
498
-
499
- // Chromosome filtering
500
- if !chromosomes.is_empty() && !chromosomes.contains(&out_lst[1]) {
501
- *case_details
502
- .maf
503
- .skipped_chromosomes
504
- .entry(out_lst[1].clone())
505
- .or_insert(0) += 1;
506
- filtered_maf_records.fetch_add(1, Ordering::Relaxed);
507
- return Ok(None);
508
- }
509
-
510
- // Update counters for included MAF records
511
- case_details.maf.total_included += 1;
512
- included_maf_records.fetch_add(1, Ordering::Relaxed);
513
-
514
- Ok(Some(out_lst))
515
- }
516
-
517
- // Process a single row of CNV file
518
- async fn process_cnvline(
519
- line: &str,
520
- case_id: &str,
521
- data_type: &str,
522
- header: &[String],
523
- columns_indices: &[usize],
524
- gain_threshold: f32,
525
- loss_threshold: f32,
526
- seg_length: i32,
527
- chromosomes: &HashSet<String>,
528
- filtered_records: &Arc<Mutex<HashMap<String, FilteredCaseDetails>>>,
529
- filtered_cnv_records: &AtomicUsize,
530
- included_cnv_records: &AtomicUsize,
531
- ) -> Result<Option<Vec<String>>, (String, String, String)> {
532
- let cont_lst: Vec<String> = line.split("\t").map(|s| s.to_string()).collect();
533
- let mut out_lst = vec![case_id.to_string()];
534
-
535
- // Initialize or update case details
536
- let mut filtered_map = filtered_records.lock().await;
537
- filtered_map
538
- .entry(case_id.to_string())
539
- .or_insert_with(|| FilteredCaseDetails {
540
- maf: FilteredMafDetails::default(),
541
- cnv: FilteredCnvDetails::default(),
542
- });
543
- let case_details = filtered_map.get_mut(case_id).unwrap();
544
-
545
- // Track total processed records
546
- case_details.cnv.total_processed += 1;
547
-
548
- // Extract relevant columns
549
- for &x in columns_indices {
550
- if x >= cont_lst.len() {
551
- case_details.cnv.invalid_rows += 1;
552
- filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
553
- return Ok(None); // Invalid row
554
- }
555
- let mut element = cont_lst[x].to_string();
556
- if header[x] == "Segment_Mean" {
557
- element = process_segment_mean(&element, case_id, data_type, gain_threshold, loss_threshold)?;
558
- if element.is_empty() {
559
- case_details.cnv.segment_mean += 1;
560
- let seg_mean = cont_lst[x].parse::<f32>().unwrap_or(0.0);
561
- if seg_mean > loss_threshold && seg_mean < gain_threshold {
562
- // Between thresholds - not a significant gain or loss
563
- if seg_mean >= 0.0 {
564
- case_details.cnv.excluded_by_gain_threshold += 1;
565
- } else {
566
- case_details.cnv.excluded_by_loss_threshold += 1;
567
- }
568
- }
569
- filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
570
- return Ok(None);
571
- }
572
- }
573
- out_lst.push(element);
574
- }
575
-
576
- // filter cnvs based on segment length. Default: 0 (no filtering)
577
- // calculate segment length (End_Position - Start_Position)
578
- let end_position = out_lst[3].parse::<i32>().map_err(|_| {
579
- case_details.cnv.invalid_rows += 1;
580
- filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
581
- (
582
- case_id.to_string(),
583
- data_type.to_string(),
584
- "Failed to convert End Position of cnv to integer.".to_string(),
585
- )
586
- })?;
587
-
588
- let start_position = out_lst[2].parse::<i32>().map_err(|_| {
589
- case_details.cnv.invalid_rows += 1;
590
- filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
591
- (
592
- case_id.to_string(),
593
- data_type.to_string(),
594
- "Failed to convert Start Position of cnv to integer.".to_string(),
595
- )
596
- })?;
597
- let cnv_length = end_position - start_position;
598
- if seg_length > 0 && cnv_length > seg_length {
599
- case_details.cnv.seg_length += 1;
600
- case_details.cnv.excluded_by_segment_length += 1;
601
- filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
602
- return Ok(None);
603
- }
604
-
605
- // adding 'chr' to chromosome if it is not start with 'chr'
606
- if !out_lst[1].starts_with("chr") {
607
- out_lst[1] = format!("chr{}", out_lst[1]);
608
- }
609
-
610
- // Chromosome filtering
611
- if !chromosomes.is_empty() && !chromosomes.contains(&out_lst[1]) {
612
- *case_details
613
- .cnv
614
- .skipped_chromosomes
615
- .entry(out_lst[1].clone())
616
- .or_insert(0) += 1;
617
- filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
618
- return Ok(None);
619
- }
620
-
621
- // Update counters for included MAF records
622
- case_details.cnv.total_included += 1;
623
- included_cnv_records.fetch_add(1, Ordering::Relaxed);
624
-
625
- Ok(Some(out_lst))
626
- }
627
-
628
- // Process Segment_Mean for CNV files
629
- fn process_segment_mean(
630
- element: &str,
631
- case_id: &str,
632
- data_type: &str,
633
- gain_threshold: f32,
634
- loss_threshold: f32,
635
- ) -> Result<String, (String, String, String)> {
636
- let seg_mean = element.parse::<f32>().map_err(|_| {
637
- (
638
- case_id.to_string(),
639
- data_type.to_string(),
640
- "Segment_Mean in cnv file is not float".to_string(),
641
- )
642
- })?;
643
-
644
- if seg_mean >= gain_threshold {
645
- Ok("gain".to_string())
646
- } else if seg_mean <= loss_threshold {
647
- Ok("loss".to_string())
648
- } else {
649
- Ok(String::new())
650
- }
651
- }
652
-
653
- /// Updated helper function to normalize MAF consequence types to frontend format
654
- /// Downloads a single file with minimal retry logic for transient failures
655
- async fn download_single_file(
656
- case_id: String,
657
- data_type: String,
658
- url: String,
659
- max_attempts: u32,
660
- ) -> Result<(String, String, String), (String, String, String, u32)> {
661
- let mut last_error = String::new();
662
- let mut error_type = String::new();
663
-
664
- for attempt in 0..max_attempts {
665
- // Build HTTP client with aggressive timeouts for real-time processing
666
- let client = match reqwest::Client::builder()
667
- .timeout(Duration::from_secs(10)) // 10 second timeout per request
668
- .connect_timeout(Duration::from_secs(3)) // 3 second connect timeout
669
- .build()
670
- {
671
- Ok(client) => client,
672
- Err(e) => {
673
- last_error = format!("Client build error: {}", e);
674
- error_type = "client_build_error".to_string();
675
- continue;
676
- }
677
- };
678
-
679
- // Attempt download with tight timeout - fail fast if server is slow
680
- match timeout(Duration::from_secs(12), client.get(&url).send()).await {
681
- Ok(Ok(resp)) if resp.status().is_success() => {
682
- match resp.bytes().await {
683
- Ok(content) => {
684
- // Handle both compressed and uncompressed content
685
- let text = if memchr(0x00, &content).is_some() {
686
- // Likely compressed (gzipped) content
687
- let mut decoder = GzDecoder::new(&content[..]);
688
- let mut decompressed_content = Vec::new();
689
- match decoder.read_to_end(&mut decompressed_content) {
690
- Ok(_) => String::from_utf8_lossy(&decompressed_content).to_string(),
691
- Err(e) => {
692
- last_error = format!("Decompression failed: {}", e);
693
- error_type = "decompression_error".to_string();
694
- continue; // Retry on decompression failure
695
- }
696
- }
697
- } else {
698
- // Plain text content
699
- String::from_utf8_lossy(&content).to_string()
700
- };
701
-
702
- // Success! Return immediately
703
- return Ok((case_id, data_type, text));
704
- }
705
- Err(e) => {
706
- last_error = format!("Failed to read response bytes: {}", e);
707
- error_type = "connection_error".to_string();
708
- // This could be "connection closed before message completed"
709
- // Worth retrying for transient network issues
710
- }
711
- }
712
- }
713
- Ok(Ok(resp)) => {
714
- last_error = format!(
715
- "HTTP error {}: {}",
716
- resp.status(),
717
- resp.status().canonical_reason().unwrap_or("Unknown")
718
- );
719
- error_type = if resp.status().is_client_error() {
720
- "client_error".to_string()
721
- } else {
722
- "server_error".to_string()
723
- };
724
- // Don't retry 4xx errors (client errors), but retry 5xx (server errors)
725
- if resp.status().is_client_error() {
726
- break; // No point retrying client errors
727
- }
728
- }
729
- Ok(Err(e)) => {
730
- last_error = format!("Request error: {}", e);
731
- error_type = "network_error".to_string();
732
- // Network errors are worth retrying
733
- }
734
- Err(_) => {
735
- last_error = "Request timeout (12s) - server too slow".to_string();
736
- error_type = "timeout_error".to_string();
737
- // Timeouts might be transient, worth a quick retry
738
- }
739
- }
740
-
741
- // If this isn't the last attempt, wait briefly before retrying
742
- if attempt < max_attempts - 1 {
743
- // Silent retry - no stderr noise
744
- tokio::time::sleep(Duration::from_secs(1)).await; // 1 second between retries
745
- }
746
- }
747
-
748
- Err((
749
- case_id,
750
- data_type,
751
- format!("{}: {}", error_type, last_error),
752
- max_attempts,
753
- ))
754
- }
755
-
756
- /// Downloading from GDC
757
- /// Outputs JSONL format: one JSON object per line
758
- async fn download_data(
759
- data4dl: HashMap<String, DataType>,
760
- host: &str,
761
- min_total_depth: i32,
762
- min_alt_allele_count: i32,
763
- maf_hyper_mutator: i32,
764
- consequences: &Option<Vec<String>>,
765
- gain_threshold: f32,
766
- loss_threshold: f32,
767
- seg_length: i32,
768
- cnv_hyper_mutator: i32,
769
- chromosomes: &HashSet<String>,
770
- max_record: usize,
771
- ) {
772
- let data_urls: Vec<(String, String, String)> = data4dl
773
- .into_iter()
774
- .flat_map(|(case_id, data_types)| {
775
- let mut urls = Vec::new();
776
- if let Some(cnv_uuid) = &data_types.cnv {
777
- urls.push((case_id.clone(), "cnv".to_string(), format!("{}{}", host, cnv_uuid)));
778
- }
779
- if let Some(maf_uuid) = &data_types.maf {
780
- urls.push((case_id.clone(), "maf".to_string(), format!("{}{}", host, maf_uuid)));
781
- }
782
- urls
783
- })
784
- .collect();
785
-
786
- let total_files = data_urls.len();
787
-
788
- // Counters for final summary
789
- let successful_downloads = Arc::new(AtomicUsize::new(0));
790
- let failed_downloads = Arc::new(AtomicUsize::new(0));
791
- let filtered_maf_records = Arc::new(AtomicUsize::new(0));
792
- let filtered_cnv_records = Arc::new(AtomicUsize::new(0));
793
- let filtered_records = Arc::new(Mutex::new(HashMap::<String, FilteredCaseDetails>::new()));
794
- let hyper_mutator_records = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
795
- let excluded_by_max_record = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
796
- let included_maf_records = Arc::new(AtomicUsize::new(0));
797
- let included_cnv_records = Arc::new(AtomicUsize::new(0));
798
- let all_records = Arc::new(Mutex::new(Vec::<Vec<String>>::new()));
799
- let data_count = Arc::new(AtomicUsize::new(0));
800
-
801
- // Only collect errors (successful data is output immediately)
802
- let errors = Arc::new(Mutex::new(Vec::<ErrorEntry>::new()));
803
-
804
- let download_futures = futures::stream::iter(
805
- data_urls
806
- .into_iter()
807
- .map(|(case_id, data_type, url)| async move { download_single_file(case_id, data_type, url, 2).await }),
808
- );
809
-
810
- // Process downloads and output results immediately as JSONL
811
- download_futures
812
- .buffer_unordered(20) // Increased concurrency for better performance
813
- .for_each(|download_result| {
814
- let successful_downloads = Arc::clone(&successful_downloads);
815
- let failed_downloads = Arc::clone(&failed_downloads);
816
- let filtered_maf_records = Arc::clone(&filtered_maf_records);
817
- let filtered_cnv_records = Arc::clone(&filtered_cnv_records);
818
- let filtered_records = Arc::clone(&filtered_records);
819
- let included_maf_records = Arc::clone(&included_maf_records);
820
- let included_cnv_records = Arc::clone(&included_cnv_records);
821
- let hyper_mutator_records = Arc::clone(&hyper_mutator_records);
822
- let excluded_by_max_record = Arc::clone(&excluded_by_max_record);
823
- let errors = Arc::clone(&errors);
824
- let all_records = Arc::clone(&all_records);
825
- let data_count = Arc::clone(&data_count);
826
-
827
- async move {
828
- let current_count = data_count.load(Ordering::Relaxed);
829
- if current_count >= max_record {
830
- // Skip processing and mark as excluded by max_record
831
- if let Ok((case_id, data_type, _)) = download_result {
832
- let mut exclud_max_record = excluded_by_max_record.lock().await;
833
- exclud_max_record
834
- .entry(data_type.to_string())
835
- .or_insert_with(Vec::new)
836
- .push(case_id.to_string());
837
- successful_downloads.fetch_add(1, Ordering::Relaxed);
838
- }
839
- return;
840
- }
841
- match download_result {
842
- Ok((case_id, data_type, content)) => {
843
- // Try to parse the content
844
- match parse_content(
845
- &content,
846
- &case_id,
847
- &data_type,
848
- min_total_depth,
849
- min_alt_allele_count,
850
- maf_hyper_mutator,
851
- &consequences,
852
- gain_threshold,
853
- loss_threshold,
854
- seg_length,
855
- cnv_hyper_mutator,
856
- &chromosomes,
857
- &filtered_records,
858
- &filtered_maf_records,
859
- &filtered_cnv_records,
860
- &included_maf_records,
861
- &included_cnv_records,
862
- &hyper_mutator_records,
863
- )
864
- .await
865
- {
866
- Ok(parsed_data) => {
867
- let remaining = max_record - current_count;
868
- if parsed_data.len() <= remaining {
869
- data_count.fetch_add(parsed_data.len(), Ordering::Relaxed);
870
- all_records.lock().await.extend(parsed_data);
871
- } else {
872
- // Skip file if it would exceed max_record
873
- let mut exclud_max_record = excluded_by_max_record.lock().await;
874
- exclud_max_record
875
- .entry(data_type.to_string())
876
- .or_insert_with(Vec::new)
877
- .push(case_id.to_string());
878
- }
879
- successful_downloads.fetch_add(1, Ordering::Relaxed);
880
- }
881
- Err((cid, dtp, error)) => {
882
- // Parsing failed - add to errors
883
- failed_downloads.fetch_add(1, Ordering::Relaxed);
884
- let error = ErrorEntry {
885
- case_id: cid,
886
- data_type: dtp,
887
- error_type: "parsing_error".to_string(),
888
- error_details: error,
889
- attempts_made: 1,
890
- };
891
- errors.lock().await.push(error);
892
- }
893
- }
894
- }
895
- Err((case_id, data_type, error_details, attempts)) => {
896
- // Download failed - add to errors
897
- failed_downloads.fetch_add(1, Ordering::Relaxed);
898
-
899
- let (error_type, clean_details) = if error_details.contains(":") {
900
- let parts: Vec<&str> = error_details.splitn(2, ": ").collect();
901
- (parts[0].to_string(), parts[1].to_string())
902
- } else {
903
- ("unknown_error".to_string(), error_details)
904
- };
905
-
906
- let error = ErrorEntry {
907
- case_id,
908
- data_type,
909
- error_type,
910
- error_details: clean_details,
911
- attempts_made: attempts,
912
- };
913
- errors.lock().await.push(error);
914
- }
915
- }
916
- }
917
- })
918
- .await;
919
-
920
- // Output final summary as the last line
921
- let success_count = successful_downloads.load(Ordering::Relaxed);
922
- let failed_count = failed_downloads.load(Ordering::Relaxed);
923
- let filtered_maf_count = filtered_maf_records.load(Ordering::Relaxed);
924
- let filtered_cnv_count = filtered_cnv_records.load(Ordering::Relaxed);
925
- let included_maf_count = included_maf_records.load(Ordering::Relaxed);
926
- let included_cnv_count = included_cnv_records.load(Ordering::Relaxed);
927
-
928
- let summary = FinalSummary {
929
- total_files,
930
- successful_files: success_count,
931
- failed_files: failed_count,
932
- errors: errors.lock().await.clone(),
933
- filtered_records: filtered_maf_count + filtered_cnv_count,
934
- filtered_maf_records: filtered_maf_count,
935
- filtered_cnv_records: filtered_cnv_count,
936
- filtered_records_by_case: filtered_records.lock().await.clone(),
937
- included_maf_records: included_maf_count,
938
- included_cnv_records: included_cnv_count,
939
- hyper_mutator_records: hyper_mutator_records.lock().await.clone(),
940
- excluded_by_max_record: excluded_by_max_record.lock().await.clone(),
941
- };
942
-
943
- let grin2lesion = serde_json::to_string(&all_records.lock().await.drain(..).collect::<Vec<Vec<String>>>())
944
- .unwrap_or_else(|_| "[]".to_string());
945
- let output = Output { grin2lesion, summary };
946
-
947
- // Output final summary - Node.js will know processing is complete when it sees this
948
- // if let Ok(json) = serde_json::to_string(&summary) {
949
- if let Ok(json) = serde_json::to_string(&output) {
950
- println!("{}", json);
951
- use std::io::Write;
952
- let _ = std::io::stdout().flush();
953
- }
954
- }
955
-
956
- /// Read data from local file
957
- async fn localread_data(
958
- case_files: HashMap<String, DataType>,
959
- min_total_depth: i32,
960
- min_alt_allele_count: i32,
961
- maf_hyper_mutator: i32,
962
- consequences: &Option<Vec<String>>,
963
- gain_threshold: f32,
964
- loss_threshold: f32,
965
- seg_length: i32,
966
- cnv_hyper_mutator: i32,
967
- chromosomes: &HashSet<String>,
968
- max_record: usize,
969
- ) {
970
- let data_files: Vec<(String, String, String)> = case_files
971
- .into_iter()
972
- .flat_map(|(case_id, data_types)| {
973
- let mut files = Vec::new();
974
- if let Some(cnv_file) = &data_types.cnv {
975
- files.push((case_id.clone(), "cnv".to_string(), cnv_file.clone()));
976
- }
977
- if let Some(maf_file) = &data_types.maf {
978
- files.push((case_id.clone(), "maf".to_string(), maf_file.clone()));
979
- }
980
- files
981
- })
982
- .collect();
983
- let total_files = data_files.len();
984
-
985
- // Counters for final summary
986
- let successful_reads = Arc::new(AtomicUsize::new(0));
987
- let failed_reads = Arc::new(AtomicUsize::new(0));
988
- let filtered_maf_records = Arc::new(AtomicUsize::new(0));
989
- let filtered_cnv_records = Arc::new(AtomicUsize::new(0));
990
- let filtered_records = Arc::new(Mutex::new(HashMap::<String, FilteredCaseDetails>::new()));
991
- let hyper_mutator_records = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
992
- let excluded_by_max_record = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
993
- let included_maf_records = Arc::new(AtomicUsize::new(0));
994
- let included_cnv_records = Arc::new(AtomicUsize::new(0));
995
- let errors = Arc::new(Mutex::new(Vec::<ErrorEntry>::new()));
996
- let all_records = Arc::new(Mutex::new(Vec::<Vec<String>>::new()));
997
- let data_count = Arc::new(AtomicUsize::new(0));
998
-
999
- // Process files concurrently
1000
- let read_futures = futures::stream::iter(data_files.into_iter().map(
1001
- |(case_id, data_type, file_path)| async move {
1002
- // read the local file
1003
- match fs::read_to_string(&file_path) {
1004
- Ok(content) => Ok((case_id, data_type, content)),
1005
- Err(e) => Err((
1006
- case_id,
1007
- data_type,
1008
- format!("file_read_error: {}", e),
1009
- 1, // Single attempt for local file readng
1010
- )),
1011
- }
1012
- },
1013
- ));
1014
-
1015
- // Process files and output results
1016
- read_futures
1017
- .buffer_unordered(3)
1018
- .for_each(|read_result| {
1019
- let successful_reads = Arc::clone(&successful_reads);
1020
- let failed_reads = Arc::clone(&failed_reads);
1021
- let filtered_maf_records = Arc::clone(&filtered_maf_records);
1022
- let filtered_cnv_records = Arc::clone(&filtered_cnv_records);
1023
- let filtered_records = Arc::clone(&filtered_records);
1024
- let included_maf_records = Arc::clone(&included_maf_records);
1025
- let included_cnv_records = Arc::clone(&included_cnv_records);
1026
- let hyper_mutator_records = Arc::clone(&hyper_mutator_records);
1027
- let excluded_by_max_record = Arc::clone(&excluded_by_max_record);
1028
- let errors = Arc::clone(&errors);
1029
- let all_records = Arc::clone(&all_records);
1030
- let data_count = Arc::clone(&data_count);
1031
-
1032
- async move {
1033
- let current_count = data_count.load(Ordering::Relaxed);
1034
- if current_count >= max_record {
1035
- // Skip processing and mark as excluded by max_record
1036
- if let Ok((case_id, data_type, _)) = read_result {
1037
- let mut exclud_max_record = excluded_by_max_record.lock().await;
1038
- exclud_max_record
1039
- .entry(data_type.to_string())
1040
- .or_insert_with(Vec::new)
1041
- .push(case_id.to_string());
1042
- successful_reads.fetch_add(1, Ordering::Relaxed);
1043
- }
1044
- return;
1045
- }
1046
- match read_result {
1047
- Ok((case_id, data_type, content)) => {
1048
- match parse_content(
1049
- &content,
1050
- &case_id,
1051
- &data_type,
1052
- min_total_depth,
1053
- min_alt_allele_count,
1054
- maf_hyper_mutator,
1055
- consequences,
1056
- gain_threshold,
1057
- loss_threshold,
1058
- seg_length,
1059
- cnv_hyper_mutator,
1060
- chromosomes,
1061
- &filtered_records,
1062
- &filtered_maf_records,
1063
- &filtered_cnv_records,
1064
- &included_maf_records,
1065
- &included_cnv_records,
1066
- &hyper_mutator_records,
1067
- )
1068
- .await
1069
- {
1070
- Ok(parsed_data) => {
1071
- let remaining = max_record - current_count;
1072
- if parsed_data.len() <= remaining {
1073
- data_count.fetch_add(parsed_data.len(), Ordering::Relaxed);
1074
- all_records.lock().await.extend(parsed_data);
1075
- } else {
1076
- // Skip file if it would exceed max_record
1077
- let mut exclud_max_record = excluded_by_max_record.lock().await;
1078
- exclud_max_record
1079
- .entry(data_type.to_string())
1080
- .or_insert_with(Vec::new)
1081
- .push(case_id.to_string());
1082
- }
1083
- successful_reads.fetch_add(1, Ordering::Relaxed);
1084
- }
1085
- Err((cid, dtp, error)) => {
1086
- failed_reads.fetch_add(1, Ordering::Relaxed);
1087
- let error = ErrorEntry {
1088
- case_id: cid,
1089
- data_type: dtp,
1090
- error_type: "parsing_error".to_string(),
1091
- error_details: error,
1092
- attempts_made: 1,
1093
- };
1094
- errors.lock().await.push(error);
1095
- }
1096
- }
1097
- }
1098
- Err((case_id, data_type, error_details, attempts)) => {
1099
- failed_reads.fetch_add(1, Ordering::Relaxed);
1100
- let (error_type, clean_details) = if error_details.contains(":") {
1101
- let parts: Vec<&str> = error_details.splitn(2, ": ").collect();
1102
- (parts[0].to_string(), parts[1].to_string())
1103
- } else {
1104
- ("unknown_error".to_string(), error_details)
1105
- };
1106
- let error = ErrorEntry {
1107
- case_id,
1108
- data_type,
1109
- error_type,
1110
- error_details: clean_details,
1111
- attempts_made: attempts,
1112
- };
1113
- errors.lock().await.push(error);
1114
- }
1115
- }
1116
- }
1117
- })
1118
- .await;
1119
- // Output final summary as the last line
1120
- let success_count = successful_reads.load(Ordering::Relaxed);
1121
- let failed_count = failed_reads.load(Ordering::Relaxed);
1122
- let filtered_maf_count = filtered_maf_records.load(Ordering::Relaxed);
1123
- let filtered_cnv_count = filtered_cnv_records.load(Ordering::Relaxed);
1124
- let included_maf_count = included_maf_records.load(Ordering::Relaxed);
1125
- let included_cnv_count = included_cnv_records.load(Ordering::Relaxed);
1126
-
1127
- let summary = FinalSummary {
1128
- total_files,
1129
- successful_files: success_count,
1130
- failed_files: failed_count,
1131
- errors: errors.lock().await.clone(),
1132
- filtered_records: filtered_maf_count + filtered_cnv_count,
1133
- filtered_maf_records: filtered_maf_count,
1134
- filtered_cnv_records: filtered_cnv_count,
1135
- filtered_records_by_case: filtered_records.lock().await.clone(),
1136
- included_maf_records: included_maf_count,
1137
- included_cnv_records: included_cnv_count,
1138
- hyper_mutator_records: hyper_mutator_records.lock().await.clone(),
1139
- excluded_by_max_record: excluded_by_max_record.lock().await.clone(),
1140
- };
1141
-
1142
- let grin2lesion = serde_json::to_string(&all_records.lock().await.drain(..).collect::<Vec<Vec<String>>>())
1143
- .unwrap_or_else(|_| "[]".to_string());
1144
- let output = Output { grin2lesion, summary };
1145
-
1146
- // Output final JSON array
1147
- if let Ok(json) = serde_json::to_string(&output) {
1148
- println!("{}", json);
1149
- use std::io::Write;
1150
- let _ = std::io::stdout().flush();
1151
- }
1152
- }
1153
-
1154
- #[tokio::main]
1155
- async fn main() -> Result<(), Box<dyn std::error::Error>> {
1156
- let args: Vec<String> = env::args().collect();
1157
- let from_file = args.contains(&"--from-file".to_string());
1158
-
1159
- const HOST: &str = "https://api.gdc.cancer.gov/data/";
1160
-
1161
- // Read input with timeout
1162
- let timeout_duration = Duration::from_secs(10); // Increased timeout for input
1163
-
1164
- let result = timeout(timeout_duration, async {
1165
- let mut buffer = String::new();
1166
- let mut reader = BufReader::new(tokio::io::stdin());
1167
- reader.read_to_string(&mut buffer).await?;
1168
- Ok::<String, io::Error>(buffer)
1169
- })
1170
- .await;
1171
-
1172
- // Handle input parsing (silently)
1173
- let input_js: InputData = match result {
1174
- Ok(Ok(buffer)) => match serde_json::from_str(&buffer) {
1175
- Ok(js) => js,
1176
- Err(_e) => {
1177
- // Silent failure - exit without stderr
1178
- std::process::exit(1);
1179
- }
1180
- },
1181
- Ok(Err(_e)) => {
1182
- // Silent failure - exit without stderr
1183
- std::process::exit(1);
1184
- }
1185
- Err(_) => {
1186
- // Silent failure - exit without stderr
1187
- std::process::exit(1);
1188
- }
1189
- };
1190
-
1191
- // Validate input (silently)
1192
- if input_js.case_files.is_empty() {
1193
- // Silent failure - exit without stderr
1194
- std::process::exit(1);
1195
- }
1196
-
1197
- let case_files = input_js.case_files;
1198
- let max_record: usize = input_js.max_record;
1199
-
1200
- // Set default maf_options
1201
- let (min_total_depth, min_alt_allele_count, maf_hyper_mutator, consequences) = match input_js.maf_options {
1202
- Some(options) => (
1203
- options.min_total_depth,
1204
- options.min_alt_allele_count,
1205
- options.hyper_mutator,
1206
- options.consequences.clone(),
1207
- ),
1208
- None => (10, 2, 8000, None), // Default values
1209
- };
1210
-
1211
- // Set default cnv_options
1212
- let (gain_threshold, loss_threshold, seg_length, cnv_hyper_mutator) = match input_js.cnv_options {
1213
- Some(options) => (
1214
- options.gain_threshold,
1215
- options.loss_threshold,
1216
- options.seg_length,
1217
- options.hyper_mutator,
1218
- ),
1219
- None => (0.3, -0.4, 0, 500), // Default values
1220
- };
1221
-
1222
- // Convert Vec<String> to HashSet<String> for faster lookup
1223
- let chromosomes = input_js.chromosomes.into_iter().collect::<HashSet<String>>();
1224
-
1225
- if from_file {
1226
- localread_data(
1227
- case_files,
1228
- min_total_depth,
1229
- min_alt_allele_count,
1230
- maf_hyper_mutator,
1231
- &consequences,
1232
- gain_threshold,
1233
- loss_threshold,
1234
- seg_length,
1235
- cnv_hyper_mutator,
1236
- &chromosomes,
1237
- max_record,
1238
- )
1239
- .await;
1240
- } else {
1241
- // Download data from GDC- this will now handle errors gracefully
1242
- download_data(
1243
- case_files,
1244
- HOST,
1245
- min_total_depth,
1246
- min_alt_allele_count,
1247
- maf_hyper_mutator,
1248
- &consequences,
1249
- gain_threshold,
1250
- loss_threshold,
1251
- seg_length,
1252
- cnv_hyper_mutator,
1253
- &chromosomes,
1254
- max_record,
1255
- )
1256
- .await;
1257
- }
1258
-
1259
- // Always exit successfully - individual file failures are logged but don't stop the process
1260
- Ok(())
1261
- }