Setting the file. One moment.
Sra Geo Fetch · Nextflow Development · anthropics/knowledge-work-plugins · Skills Docs
ContentsBack to the top of the page 22
Validate Data
Tech Debt
62
Recruiting Pipeline
71
Vendor Check
125
Zoom Meeting SDK Web
88
Vendor Review
181
Create An Asset
Video Sdk/web
def main
— line 663
This file
Number 2.15
Position 15 of 21
Type Python
Size 24 KB
Lines 732 scripts/ sra_geo_fetch.py
Python · 732 lines · 24 KB
16 python sra_geo_fetch.py download GSE110004 -o ./fastq --parallel 4
17 python sra_geo_fetch.py samplesheet GSE110004 --fastq-dir ./fastq -o samplesheet.csv
18 """
19
20 import argparse
21 import json
22 import logging
23 import os
24 import re
25 import subprocess
26 import sys
27 from concurrent.futures import ThreadPoolExecutor, as_completed
28 from dataclasses import dataclass, asdict
29 from pathlib import Path
30 from typing import Dict, List, Optional, Tuple
31
32 # Add utils to path
33 sys.path.insert( 0 , str (Path( __file__ ).parent))
34 from utils.ncbi_utils import (
35 check_network_access,
36 fetch_geo_metadata,
37 fetch_sra_study_accession,
38 fetch_sra_run_info,
39 fetch_sra_run_info_detailed,
40 fetch_ena_fastq_urls,
41 download_file,
42 format_file_size,
43 estimate_download_size,
44 group_samples_by_type,
45 format_sample_groups_table,
46 )
47
48 # Set up logging
49 logging.basicConfig(
50 level = logging. INFO ,
51 format = ' %(message)s '
52 )
53 logger = logging.getLogger( __name__ )
54
55 # Load genome mapping
56 SCRIPT_DIR = Path( __file__ ).parent
57 GENOMES_FILE = SCRIPT_DIR / "config" / "genomes.yaml"
58
59
60 @dataclass
61 class StudyInfo :
62 """Information about a GEO study."""
63 geo_id: str
64 title: str
65 organism: str
66 n_samples: int
67 summary: str
68 sra_study: Optional[ str ]
69 suggested_genome: Optional[ str ]
70 suggested_pipeline: Optional[ str ]
71
72
73 def load_genome_mapping () -> Dict:
74 """Load organism to genome mapping from config."""
75 if not GENOMES_FILE .exists():
76 return {}
77
78 try :
79 import yaml
80 with open ( GENOMES_FILE ) as f:
81 config = yaml.safe_load(f)
82 return config.get( 'organisms' , {})
83 except ImportError :
84 # Fallback: parse YAML manually for simple cases
85 mapping = {}
86 try :
87 with open ( GENOMES_FILE ) as f:
88 content = f.read()
89 # Simple regex parsing for organism blocks
90 pattern = r '" ([ ^" ] + ) ": \s * \n \s * genome: \s * " ([ ^" ] + ) "'
91 for match in re.finditer(pattern, content):
92 mapping[match.group( 1 )] = { 'genome' : match.group( 2 )}
93 except Exception :
94 pass
95 return mapping
96
97
98 def suggest_genome (organism: str ) -> Optional[ str ]:
99 """Suggest a genome based on organism name."""
100 genome_map = load_genome_mapping()
101
102 # Direct match
103 if organism in genome_map:
104 return genome_map[organism].get( 'genome' )
105
106 # Case-insensitive search
107 organism_lower = organism.lower()
108 for org_name, info in genome_map.items():
109 if org_name.lower() == organism_lower:
110 return info.get( 'genome' )
111 # Check aliases
112 aliases = info.get( 'aliases' , [])
113 if any (alias.lower() == organism_lower for alias in aliases):
114 return info.get( 'genome' )
115
116 # Common fallbacks
117 fallbacks = {
118 'homo sapiens' : 'GRCh38' ,
119 'human' : 'GRCh38' ,
120 'mus musculus' : 'GRCm39' ,
121 'mouse' : 'GRCm39' ,
122 'saccharomyces cerevisiae' : 'R64-1-1' ,
123 'yeast' : 'R64-1-1' ,
124 'drosophila melanogaster' : 'BDGP6' ,
125 'caenorhabditis elegans' : 'WBcel235' ,
126 'danio rerio' : 'GRCz11' ,
127 'arabidopsis thaliana' : 'TAIR10' ,
128 'rattus norvegicus' : 'Rnor_6.0' ,
129 }
130
131 return fallbacks.get(organism_lower)
132
133
134 def suggest_pipeline (library_strategy: str , library_source: str = '' ) -> str :
135 """Suggest nf-core pipeline based on library strategy."""
136 strategy = library_strategy.upper()
137
138 pipeline_map = {
139 'RNA-SEQ' : 'rnaseq' ,
140 'ATAC-SEQ' : 'atacseq' ,
141 'CHIP-SEQ' : 'chipseq' ,
142 'WGS' : 'sarek' ,
143 'WXS' : 'sarek' ,
144 'AMPLICON' : 'ampliseq' ,
145 'BISULFITE-SEQ' : 'methylseq' ,
146 'HI-C' : 'hic' ,
147 }
148
149 return pipeline_map.get(strategy, 'rnaseq' )
150
151
152 def cmd_info (args):
153 """Display study information."""
154 geo_id = args.geo_id.upper()
155
156 print ( f " \n Fetching information for { geo_id } ..." )
157
158 # Check network
159 network_ok, network_msg = check_network_access()
160 if not network_ok:
161 print ( f " \n ⚠️ Network issues detected: \n{ network_msg } " )
162
163 # Get GEO metadata
164 metadata = fetch_geo_metadata(geo_id)
165 if not metadata:
166 print ( f " \n ❌ Could not fetch metadata for { geo_id } " )
167 return 1
168
169 # Get SRA study accession
170 sra_study = fetch_sra_study_accession(geo_id)
171
172 # Get detailed run info
173 print ( "Fetching SRA run information..." )
174 runs = fetch_sra_run_info_detailed(geo_id)
175 if not runs:
176 # Fallback to basic method
177 runs = fetch_sra_run_info(geo_id)
178
179 # Group samples by type
180 groups = group_samples_by_type(runs) if runs else {}
181
182 # Suggest genome and pipeline
183 organism = metadata.get( 'organism' , 'Unknown' )
184 genome = suggest_genome(organism)
185
186 # Determine primary data type
187 primary_strategy = 'RNA-SEQ'
188 if groups:
189 primary_group = max (groups.items(), key =lambda x: x[ 1 ][ 'count' ])
190 primary_strategy = primary_group[ 1 ][ 'strategy' ]
191 pipeline = suggest_pipeline(primary_strategy)
192
193 # Estimate download size
194 est_size = estimate_download_size(runs)
195
196 # Display info
197 print ( " \n " + "━" * 70 )
198 print ( f " { geo_id } : { metadata.get( 'title' , 'N/A' ) } " )
199 print ( "━" * 70 )
200 print ( f "Organism: { organism } " )
201 print ( f "Samples: { metadata.get( 'n_samples' , 'N/A' ) } " )
202 print ( f "SRA Study: { sra_study or 'Not found' } " )
203 print ( f "Runs: { len (runs) } " )
204 print ( f "Est. Size: ~ { format_file_size(est_size) } " )
205 print ( f "Genome: { genome or 'Unknown (manual selection required)' } " )
206 print ( f "Pipeline: nf-core/ { pipeline } (suggested)" )
207
208 # Show sample groups table
209 if groups:
210 print (format_sample_groups_table(groups))
211
212 if metadata.get( 'summary' ):
213 summary = metadata[ 'summary' ]
214 if len (summary) > 300 :
215 summary = summary[: 297 ] + "..."
216 print ( f " \n Summary: \n { summary } " )
217
218 print ( "━" * 70 )
219
220 # Show download hints
221 if len (groups) > 1 :
222 print ( " \n 💡 To download a specific subset, use:" )
223 for key in sorted (groups.keys()):
224 print ( f " --subset \"{ key }\" " )
225
226 # Save study info JSON
227 if args.output_json:
228 info = {
229 'geo_id' : geo_id,
230 'title' : metadata.get( 'title' ),
231 'organism' : organism,
232 'n_samples' : metadata.get( 'n_samples' ),
233 'sra_study' : sra_study,
234 'n_runs' : len (runs),
235 'groups' : {k: { ** v, 'runs' : None , 'gsm_ids' : list (v.get( 'gsm_ids' , []))} for k, v in groups.items()},
236 'suggested_genome' : genome,
237 'suggested_pipeline' : pipeline,
238 'summary' : metadata.get( 'summary' ),
239 }
240 output_path = Path(args.output_json)
241 with open (output_path, 'w' ) as f:
242 json.dump(info, f, indent = 2 )
243 print ( f " \n 📄 Study info saved to: { output_path } " )
244
245 return 0
246
247
248 def cmd_groups (args):
249 """Display sample groups in a study for interactive selection."""
250 geo_id = args.geo_id.upper()
251
252 print ( f " \n Fetching sample groups for { geo_id } ..." )
253
254 # Get detailed run info
255 runs = fetch_sra_run_info_detailed(geo_id)
256 if not runs:
257 runs = fetch_sra_run_info(geo_id)
258
259 if not runs:
260 print ( f " \n ❌ No runs found for { geo_id } " )
261 return 1
262
263 # Group samples
264 groups = group_samples_by_type(runs)
265
266 print (format_sample_groups_table(groups))
267
268 # Output for interactive selection
269 print ( " \n 📋 Available groups for --subset option:" )
270 for i, (key, info) in enumerate ( sorted (groups.items(), key =lambda x: - x[ 1 ][ 'count' ]), 1 ):
271 size_str = format_file_size(info[ 'size_estimate' ])
272 print ( f " { i } . \"{ key }\" - { info[ 'count' ] } samples (~ { size_str } )" )
273
274 # Save to JSON if requested
275 if args.output:
276 output_path = Path(args.output)
277 output_data = {
278 'geo_id' : geo_id,
279 'groups' : {}
280 }
281 for key, info in groups.items():
282 output_data[ 'groups' ][key] = {
283 'count' : info[ 'count' ],
284 'gsm_range' : info[ 'gsm_range' ],
285 'gsm_ids' : info.get( 'gsm_ids' , []),
286 'size_estimate' : info[ 'size_estimate' ],
287 'strategy' : info[ 'strategy' ],
288 'layout' : info[ 'layout' ],
289 'srr_ids' : [r[ 'srr' ] for r in info[ 'runs' ]],
290 }
291 with open (output_path, 'w' ) as f:
292 json.dump(output_data, f, indent = 2 )
293 print ( f " \n 📄 Groups saved to: { output_path } " )
294
295 return 0
296
297
298 def cmd_list (args):
299 """List all samples and runs in a study."""
300 geo_id = args.geo_id.upper()
301
302 print ( f " \n Fetching run list for { geo_id } ..." )
303
304 runs = fetch_sra_run_info(geo_id)
305 if not runs:
306 print ( f " \n ❌ No runs found for { geo_id } " )
307 return 1
308
309 # Apply filter if specified
310 if args.filter:
311 filter_parts = args.filter.split( ':' )
312 strategy_filter = filter_parts[ 0 ].upper() if filter_parts else None
313 layout_filter = filter_parts[ 1 ].upper() if len (filter_parts) > 1 else None
314
315 filtered = []
316 for run in runs:
317 if strategy_filter and run.get( 'library_strategy' , '' ).upper() != strategy_filter:
318 continue
319 if layout_filter and run.get( 'layout' , '' ).upper() != layout_filter:
320 continue
321 filtered.append(run)
322 runs = filtered
323
324 print ( f " \n{ 'SRR' :<15} { 'GSM' :<12} { 'Layout' :<8} { 'Strategy' :<12} { 'Size' :>10} " )
325 print ( "-" * 60 )
326
327 for run in runs:
328 size = format_file_size(run.get( 'bases' , 0 ) // 4 )
329 print ( f " { run[ 'srr' ] :<15} { run.get( 'gsm' , 'N/A' ) :<12} { run.get( 'layout' , 'N/A' ) :<8} "
330 f " { run.get( 'library_strategy' , 'N/A' ) :<12} { size :>10} " )
331
332 print ( f " \n Total: { len (runs) } runs" )
333
334 # Output as TSV if requested
335 if args.output:
336 output_path = Path(args.output)
337 with open (output_path, 'w' ) as f:
338 f.write( "run_accession \t gsm \t layout \t library_strategy \t bases \n " )
339 for run in runs:
340 f.write( f " { run[ 'srr' ] }\t{ run.get( 'gsm' , '' ) }\t{ run.get( 'layout' , '' ) }\t "
341 f " { run.get( 'library_strategy' , '' ) }\t{ run.get( 'bases' , 0 ) }\n " )
342 print ( f " \n 📄 Run list saved to: { output_path } " )
343
344 return 0
345
346
347 def download_fastq_file (url: str , output_path: Path, timeout: int = 600 ) -> Tuple[ str , bool ]:
348 """Download a single FASTQ file."""
349 filename = output_path.name
350 if output_path.exists():
351 return filename, True # Already exists
352
353 success = download_file(url, output_path, timeout = timeout, show_progress = False )
354 return filename, success
355
356
357 def interactive_select_group (groups: Dict[ str , Dict]) -> Optional[ str ]:
358 """Interactively select a sample group."""
359 if len (groups) <= 1 :
360 return None # No selection needed
361
362 print ( " \n " + "=" * 60 )
363 print ( " SELECT SAMPLE GROUP TO DOWNLOAD" )
364 print ( "=" * 60 )
365
366 sorted_groups = sorted (groups.items(), key =lambda x: - x[ 1 ][ 'count' ])
367
368 for i, (key, info) in enumerate (sorted_groups, 1 ):
369 size_str = format_file_size(info[ 'size_estimate' ])
370 print ( f " \n [ { i } ] { info[ 'strategy' ] } ( { info[ 'layout' ].lower() } )" )
371 print ( f " Samples: { info[ 'count' ] } " )
372 print ( f " GSM: { info[ 'gsm_range' ] } " )
373 print ( f " Size: ~ { size_str } " )
374
375 print ( f " \n [0] Download ALL ( { sum (g[ 'count' ] for g in groups.values()) } samples)" )
376 print ( "-" * 60 )
377
378 try :
379 choice = input ( " \n Enter selection (0- {} ): " .format( len (sorted_groups))).strip()
380 choice_num = int (choice)
381
382 if choice_num == 0 :
383 return None # Download all
384 elif 1 <= choice_num <= len (sorted_groups):
385 selected_key = sorted_groups[choice_num - 1 ][ 0 ]
386 print ( f " \n ✓ Selected: { selected_key } " )
387 return selected_key
388 else :
389 print ( "Invalid selection, downloading all." )
390 return None
391 except ( ValueError , EOFError , KeyboardInterrupt ):
392 print ( " \n Invalid input, downloading all." )
393 return None
394
395
396 def cmd_download (args):
397 """Download FASTQ files from ENA."""
398 geo_id = args.geo_id.upper()
399 output_dir = Path(args.output)
400 output_dir.mkdir( parents = True , exist_ok = True )
401
402 print ( f " \n Preparing download for { geo_id } ..." )
403
404 # Get detailed run info (includes BioProject fallback for SuperSeries)
405 print ( "Fetching SRA run information..." )
406 runs = fetch_sra_run_info_detailed(geo_id)
407 if not runs:
408 runs = fetch_sra_run_info(geo_id)
409
410 if not runs:
411 print ( f "❌ No runs found for { geo_id } " )
412 return 1
413
414 # Collect all unique SRA studies from runs (SuperSeries may have multiple)
415 sra_studies = set (r.get( 'sra_study' , '' ) for r in runs if r.get( 'sra_study' ))
416 if not sra_studies:
417 print ( f "❌ Could not find any SRA studies for { geo_id } " )
418 return 1
419
420 if len (sra_studies) > 1 :
421 print ( f "SuperSeries detected with { len (sra_studies) } SRA studies: { ', ' .join( sorted (sra_studies)) } " )
422 else :
423 print ( f "SRA Study: { list (sra_studies)[ 0 ] } " )
424
425 # Group samples
426 groups = group_samples_by_type(runs)
427
428 # Show sample groups if multiple types exist
429 if len (groups) > 1 :
430 print (format_sample_groups_table(groups))
431
432 # Handle subset selection
433 selected_subset = args.subset
434
435 # Interactive mode if multiple groups and no subset specified
436 if args.interactive and len (groups) > 1 and not selected_subset:
437 selected_subset = interactive_select_group(groups)
438
439 # Get ENA FASTQ URLs from all SRA studies
440 print ( " \n Fetching FASTQ URLs from ENA..." )
441 fastq_urls = {}
442 for sra_study in sorted (sra_studies):
443 study_urls = fetch_ena_fastq_urls(sra_study)
444 if study_urls:
445 print ( f " { sra_study } : { len (study_urls) } runs" )
446 fastq_urls.update(study_urls)
447
448 if not fastq_urls:
449 print ( "❌ No FASTQ URLs found in ENA" )
450 print ( "Tip: Try using SRA toolkit directly with prefetch + fasterq-dump" )
451 return 1
452
453 # Apply filter if specified
454 if selected_subset:
455 filter_parts = selected_subset.split( ':' )
456 strategy_filter = filter_parts[ 0 ].upper() if filter_parts else None
457 layout_filter = filter_parts[ 1 ].upper() if len (filter_parts) > 1 else None
458
459 filtered_srrs = set ()
460 for run in runs:
461 if strategy_filter and run.get( 'library_strategy' , '' ).upper() != strategy_filter:
462 continue
463 if layout_filter and run.get( 'layout' , '' ).upper() != layout_filter:
464 continue
465 filtered_srrs.add(run[ 'srr' ])
466
467 fastq_urls = {srr: urls for srr, urls in fastq_urls.items() if srr in filtered_srrs}
468 print ( f " \n 📦 Filtered to { len (fastq_urls) } runs matching \"{ selected_subset }\" " )
469
470 # Count files to download
471 total_files = sum ( len (urls) for urls in fastq_urls.values())
472 print ( f " \n 📦 Found { len (fastq_urls) } runs, { total_files } FASTQ files to download" )
473
474 # Check for existing files
475 existing = 0
476 downloads_needed = []
477 for srr, urls in fastq_urls.items():
478 for url in urls:
479 filename = url.split( '/' )[ - 1 ]
480 filepath = output_dir / filename
481 if filepath.exists():
482 existing += 1
483 else :
484 downloads_needed.append((url, filepath))
485
486 if existing:
487 print ( f " ✓ { existing } files already exist, skipping" )
488
489 if not downloads_needed:
490 print ( " \n ✅ All files already downloaded!" )
491 return 0
492
493 print ( f " ↓ { len (downloads_needed) } files to download" )
494 print ()
495
496 # Download files
497 successful = 0
498 failed = []
499
500 if args.parallel > 1 :
501 # Parallel download
502 with ThreadPoolExecutor( max_workers = args.parallel) as executor:
503 futures = {
504 executor.submit(download_fastq_file, url, filepath): filepath
505 for url, filepath in downloads_needed
506 }
507
508 for i, future in enumerate (as_completed(futures), 1 ):
509 filepath = futures[future]
510 filename, success = future.result()
511 status = "✓" if success else "✗"
512 print ( f " [ { i } / { len (downloads_needed) } ] { status } { filename } " )
513 if success:
514 successful += 1
515 else :
516 failed.append(filename)
517 else :
518 # Sequential download
519 for i, (url, filepath) in enumerate (downloads_needed, 1 ):
520 filename = filepath.name
521 print ( f " [ { i } / { len (downloads_needed) } ] Downloading { filename } ..." )
522 success = download_file(url, filepath, timeout = args.timeout)
523 if success:
524 successful += 1
525 print ( f " ✓ Done" )
526 else :
527 failed.append(filename)
528 print ( f " ✗ Failed" )
529
530 print ( f " \n 📊 Download summary:" )
531 print ( f " ✓ Successful: { successful + existing } " )
532 print ( f " ✗ Failed: { len (failed) } " )
533
534 if failed:
535 print ( f " \n Failed downloads:" )
536 for f in failed:
537 print ( f " - { f } " )
538 return 1
539
540 print ( f " \n ✅ All files downloaded to: { output_dir } " )
541
542 # Save metadata
543 metadata_path = output_dir / "download_metadata.json"
544 metadata = {
545 'geo_id' : geo_id,
546 'sra_studies' : sorted (sra_studies),
547 'n_runs' : len (fastq_urls),
548 'n_files' : total_files,
549 'output_dir' : str (output_dir.absolute()),
550 }
551 with open (metadata_path, 'w' ) as f:
552 json.dump(metadata, f, indent = 2 )
553
554 return 0
555
556
557 def cmd_samplesheet (args):
558 """Generate samplesheet for nf-core pipeline."""
559 geo_id = args.geo_id.upper()
560 fastq_dir = Path(args.fastq_dir)
561 output_path = Path(args.output)
562
563 print ( f " \n Generating samplesheet for { geo_id } ..." )
564
565 # Get run info
566 runs = fetch_sra_run_info(geo_id)
567 if not runs:
568 print ( f "❌ No runs found for { geo_id } " )
569 return 1
570
571 # Get GEO metadata for sample naming
572 metadata = fetch_geo_metadata(geo_id)
573 organism = metadata.get( 'organism' , 'Unknown' ) if metadata else 'Unknown'
574 genome = suggest_genome(organism)
575
576 # Detect pipeline from data
577 strategies = set (r.get( 'library_strategy' , 'RNA-SEQ' ) for r in runs)
578 primary_strategy = list (strategies)[ 0 ] if strategies else 'RNA-SEQ'
579 pipeline = args.pipeline or suggest_pipeline(primary_strategy)
580
581 # Map SRR to local FASTQ files
582 samples = []
583 for run in runs:
584 srr = run[ 'srr' ]
585 layout = run.get( 'layout' , 'PAIRED' )
586
587 # Find FASTQ files
588 if layout == 'PAIRED' :
589 r1 = fastq_dir / f " { srr } _1.fastq.gz"
590 r2 = fastq_dir / f " { srr } _2.fastq.gz"
591 if not r1.exists() or not r2.exists():
592 logger.warning( f "FASTQ files not found for { srr } " )
593 continue
594 samples.append({
595 'srr' : srr,
596 'gsm' : run.get( 'gsm' , '' ),
597 'fastq_1' : str (r1.absolute()),
598 'fastq_2' : str (r2.absolute()),
599 'layout' : 'PAIRED' ,
600 })
601 else :
602 r1 = fastq_dir / f " { srr } .fastq.gz"
603 if not r1.exists():
604 r1 = fastq_dir / f " { srr } _1.fastq.gz"
605 if not r1.exists():
606 logger.warning( f "FASTQ file not found for { srr } " )
607 continue
608 samples.append({
609 'srr' : srr,
610 'gsm' : run.get( 'gsm' , '' ),
611 'fastq_1' : str (r1.absolute()),
612 'fastq_2' : '' ,
613 'layout' : 'SINGLE' ,
614 })
615
616 if not samples:
617 print ( f "❌ No FASTQ files found in { fastq_dir } " )
618 return 1
619
620 # Generate sample names
621 # Try to infer meaningful names from GSM IDs or use SRR
622 sample_names = {}
623 for sample in samples:
624 # Default to SRR accession
625 sample_names[sample[ 'srr' ]] = sample[ 'srr' ]
626
627 # Write samplesheet
628 with open (output_path, 'w' ) as f:
629 if pipeline == 'rnaseq' :
630 f.write( "sample,fastq_1,fastq_2,strandedness \n " )
631 for sample in samples:
632 name = sample_names[sample[ 'srr' ]]
633 f.write( f " { name } , { sample[ 'fastq_1' ] } , { sample[ 'fastq_2' ] } ,auto \n " )
634 elif pipeline == 'atacseq' :
635 f.write( "sample,fastq_1,fastq_2,replicate \n " )
636 for i, sample in enumerate (samples, 1 ):
637 name = sample_names[sample[ 'srr' ]]
638 f.write( f " { name } , { sample[ 'fastq_1' ] } , { sample[ 'fastq_2' ] } ,1 \n " )
639 else :
640 # Generic format
641 f.write( "sample,fastq_1,fastq_2 \n " )
642 for sample in samples:
643 name = sample_names[sample[ 'srr' ]]
644 f.write( f " { name } , { sample[ 'fastq_1' ] } , { sample[ 'fastq_2' ] }\n " )
645
646 print ( f " \n ✅ Generated samplesheet: { output_path } " )
647 print ( f " Samples: { len (samples) } " )
648 print ( f " Pipeline: nf-core/ { pipeline } " )
649 if genome:
650 print ( f " Genome: { genome } " )
651
652 print ( f " \n 💡 Suggested command:" )
653 print ( f " nextflow run nf-core/ { pipeline } \\ " )
654 print ( f " --input { output_path } \\ " )
655 print ( f " --outdir results \\ " )
656 if genome:
657 print ( f " --genome { genome } \\ " )
658 print ( f " -profile docker" )
659
660 return 0
661
662
663 def main ():
664 parser = argparse.ArgumentParser(
665 description = "Download GEO/SRA data and prepare for nf-core pipelines" ,
666 formatter_class = argparse.RawDescriptionHelpFormatter,
667 epilog = """
668 Examples:
669 %(prog)s info GSE110004 # Get study info with sample groups
670 %(prog)s groups GSE110004 # Show sample groups for selection
671 %(prog)s list GSE110004 --filter RNA-Seq # List RNA-seq runs
672 %(prog)s download GSE110004 -o ./fastq -i # Download with interactive selection
673 %(prog)s download GSE110004 -o ./fastq --subset "RNA-Seq:PAIRED"
674 %(prog)s samplesheet GSE110004 \\
675 --fastq-dir ./fastq -o samplesheet.csv # Generate samplesheet
676 """
677 )
678
679 subparsers = parser.add_subparsers( dest = 'command' , help = 'Commands' )
680
681 # info command
682 info_parser = subparsers.add_parser( 'info' , help = 'Display study information with sample groups' )
683 info_parser.add_argument( 'geo_id' , help = 'GEO accession (e.g., GSE110004)' )
684 info_parser.add_argument( '--output-json' , '-o' , help = 'Save info to JSON file' )
685
686 # groups command
687 groups_parser = subparsers.add_parser( 'groups' , help = 'Show sample groups for interactive selection' )
688 groups_parser.add_argument( 'geo_id' , help = 'GEO accession' )
689 groups_parser.add_argument( '--output' , '-o' , help = 'Save groups to JSON file' )
690
691 # list command
692 list_parser = subparsers.add_parser( 'list' , help = 'List samples and runs' )
693 list_parser.add_argument( 'geo_id' , help = 'GEO accession' )
694 list_parser.add_argument( '--filter' , '-f' , help = 'Filter by strategy:layout (e.g., RNA-Seq:PAIRED)' )
695 list_parser.add_argument( '--output' , '-o' , help = 'Save to TSV file' )
696
697 # download command
698 dl_parser = subparsers.add_parser( 'download' , help = 'Download FASTQ files' )
699 dl_parser.add_argument( 'geo_id' , help = 'GEO accession' )
700 dl_parser.add_argument( '--output' , '-o' , required = True , help = 'Output directory' )
701 dl_parser.add_argument( '--subset' , '-s' , help = 'Filter subset (e.g., RNA-Seq:PAIRED)' )
702 dl_parser.add_argument( '--interactive' , '-i' , action = 'store_true' ,
703 help = 'Interactively select sample group to download' )
704 dl_parser.add_argument( '--parallel' , '-p' , type = int , default = 4 , help = 'Parallel downloads' )
705 dl_parser.add_argument( '--timeout' , '-t' , type = int , default = 600 , help = 'Download timeout (sec)' )
706
707 # samplesheet command
708 ss_parser = subparsers.add_parser( 'samplesheet' , help = 'Generate samplesheet' )
709 ss_parser.add_argument( 'geo_id' , help = 'GEO accession' )
710 ss_parser.add_argument( '--fastq-dir' , '-f' , required = True , help = 'Directory with FASTQ files' )
711 ss_parser.add_argument( '--output' , '-o' , default = 'samplesheet.csv' , help = 'Output samplesheet' )
712 ss_parser.add_argument( '--pipeline' , '-p' , help = 'Target pipeline (auto-detected if not specified)' )
713
714 args = parser.parse_args()
715
716 if not args.command:
717 parser.print_help()
718 return 1
719
720 commands = {
721 'info' : cmd_info,
722 'groups' : cmd_groups,
723 'list' : cmd_list,
724 'download' : cmd_download,
725 'samplesheet' : cmd_samplesheet,
726 }
727
728 return commands[args.command](args)
729
730
731 if __name__ == '__main__' :
732 sys.exit(main())