Skill Tracer
Skills › Knowledge Work Plugins › nextflow-development › sra_geo_fetch.py

sra_geo_fetch.py

732 lines · bio-research/skills/nextflow-development/scripts/sra_geo_fetch.py at 2574e29

1#!/usr/bin/env python3
2"""
3GEO/SRA Data Fetcher
4====================
5Download raw sequencing data from NCBI GEO/SRA and prepare for nf-core pipelines.
6
7Usage:
8 python sra_geo_fetch.py info <GEO_ID> # Get study information
9 python sra_geo_fetch.py list <GEO_ID> # List all samples/runs
10 python sra_geo_fetch.py download <GEO_ID> -o DIR # Download FASTQ files
11 python sra_geo_fetch.py samplesheet <GEO_ID> ... # Generate samplesheet
13Examples:
14 python sra_geo_fetch.py info GSE110004
15 python sra_geo_fetch.py list GSE110004 --filter "RNA-Seq:PAIRED"
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"""
20import argparse
21import json
22import logging
23import os
24import re
25import subprocess
26import sys
27from concurrent.futures import ThreadPoolExecutor, as_completed
28from dataclasses import dataclass, asdict
29from pathlib import Path
30from typing import Dict, List, Optional, Tuple
32# Add utils to path
33sys.path.insert(0, str(Path(__file__).parent))
34from 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,
48# Set up logging
49logging.basicConfig(
50 level=logging.INFO,
51 format='%(message)s'
53logger = logging.getLogger(__name__)
55# Load genome mapping
56SCRIPT_DIR = Path(__file__).parent
57GENOMES_FILE = SCRIPT_DIR / "config" / "genomes.yaml"
60@dataclass
61class 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]
73def load_genome_mapping() -> Dict:
74 """Load organism to genome mapping from config."""
75 if not GENOMES_FILE.exists():
76 return {}
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
98def suggest_genome(organism: str) -> Optional[str]:
99 """Suggest a genome based on organism name."""
100 genome_map = load_genome_mapping()
102 # Direct match
103 if organism in genome_map:
104 return genome_map[organism].get('genome')
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')
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 }
131 return fallbacks.get(organism_lower)
134def suggest_pipeline(library_strategy: str, library_source: str = '') -> str:
135 """Suggest nf-core pipeline based on library strategy."""
136 strategy = library_strategy.upper()
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 }
149 return pipeline_map.get(strategy, 'rnaseq')
152def cmd_info(args):
153 """Display study information."""
154 geo_id = args.geo_id.upper()
156 print(f"\nFetching information for {geo_id}...")
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}")
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
169 # Get SRA study accession
170 sra_study = fetch_sra_study_accession(geo_id)
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)
179 # Group samples by type
180 groups = group_samples_by_type(runs) if runs else {}
182 # Suggest genome and pipeline
183 organism = metadata.get('organism', 'Unknown')
184 genome = suggest_genome(organism)
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)
193 # Estimate download size
194 est_size = estimate_download_size(runs)
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)")
208 # Show sample groups table
209 if groups:
210 print(format_sample_groups_table(groups))
212 if metadata.get('summary'):
213 summary = metadata['summary']
214 if len(summary) > 300:
215 summary = summary[:297] + "..."
216 print(f"\nSummary:\n {summary}")
218 print("━" * 70)
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}\"")
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}")
245 return 0
248def cmd_groups(args):
249 """Display sample groups in a study for interactive selection."""
250 geo_id = args.geo_id.upper()
252 print(f"\nFetching sample groups for {geo_id}...")
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)
259 if not runs:
260 print(f"\n❌ No runs found for {geo_id}")
261 return 1
263 # Group samples
264 groups = group_samples_by_type(runs)
266 print(format_sample_groups_table(groups))
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})")
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}")
295 return 0
298def cmd_list(args):
299 """List all samples and runs in a study."""
300 geo_id = args.geo_id.upper()
302 print(f"\nFetching run list for {geo_id}...")
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
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
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
324 print(f"\n{'SRR':<15} {'GSM':<12} {'Layout':<8} {'Strategy':<12} {'Size':>10}")
325 print("-" * 60)
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}")
332 print(f"\nTotal: {len(runs)} runs")
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\tgsm\tlayout\tlibrary_strategy\tbases\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}")
344 return 0
347def 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
353 success = download_file(url, output_path, timeout=timeout, show_progress=False)
354 return filename, success
357def 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
362 print("\n" + "=" * 60)
363 print(" SELECT SAMPLE GROUP TO DOWNLOAD")
364 print("=" * 60)
366 sorted_groups = sorted(groups.items(), key=lambda x: -x[1]['count'])
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}")
375 print(f"\n [0] Download ALL ({sum(g['count'] for g in groups.values())} samples)")
376 print("-" * 60)
378 try:
379 choice = input("\nEnter selection (0-{}): ".format(len(sorted_groups))).strip()
380 choice_num = int(choice)
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("\nInvalid input, downloading all.")
393 return None
396def 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)
402 print(f"\nPreparing download for {geo_id}...")
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)
410 if not runs:
411 print(f"❌ No runs found for {geo_id}")
412 return 1
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
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]}")
425 # Group samples
426 groups = group_samples_by_type(runs)
428 # Show sample groups if multiple types exist
429 if len(groups) > 1:
430 print(format_sample_groups_table(groups))
432 # Handle subset selection
433 selected_subset = args.subset
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)
439 # Get ENA FASTQ URLs from all SRA studies
440 print("\nFetching 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)
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
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
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'])
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}\"")
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")
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))
486 if existing:
487 print(f" ✓ {existing} files already exist, skipping")
489 if not downloads_needed:
490 print("\n✅ All files already downloaded!")
491 return 0
493 print(f" ↓ {len(downloads_needed)} files to download")
494 print()
496 # Download files
497 successful = 0
498 failed = []
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 }
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")
530 print(f"\n📊 Download summary:")
531 print(f" ✓ Successful: {successful + existing}")
532 print(f" ✗ Failed: {len(failed)}")
534 if failed:
535 print(f"\nFailed downloads:")
536 for f in failed:
537 print(f" - {f}")
538 return 1
540 print(f"\n✅ All files downloaded to: {output_dir}")
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)
554 return 0
557def 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)
563 print(f"\nGenerating samplesheet for {geo_id}...")
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
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)
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)
581 # Map SRR to local FASTQ files
582 samples = []
583 for run in runs:
584 srr = run['srr']
585 layout = run.get('layout', 'PAIRED')
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 })
616 if not samples:
617 print(f"❌ No FASTQ files found in {fastq_dir}")
618 return 1
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']
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")
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}")
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")
660 return 0
663def main():
664 parser = argparse.ArgumentParser(
665 description="Download GEO/SRA data and prepare for nf-core pipelines",
666 formatter_class=argparse.RawDescriptionHelpFormatter,
667 epilog="""
668Examples:
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 )
679 subparsers = parser.add_subparsers(dest='command', help='Commands')
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')
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')
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')
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)')
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)')
714 args = parser.parse_args()
716 if not args.command:
717 parser.print_help()
718 return 1
720 commands = {
721 'info': cmd_info,
722 'groups': cmd_groups,
723 'list': cmd_list,
724 'download': cmd_download,
725 'samplesheet': cmd_samplesheet,
726 }
728 return commands[args.command](args)
731if __name__ == '__main__':
732 sys.exit(main())