11import logging
2+ from pathlib import Path
23
34import typer
45import yaml
@@ -60,6 +61,23 @@ def show_dimensions_index(
6061 help = "If set, will show all unique sample names found across all the VCF files in the project." ,
6162 )
6263 ),
64+ sample_names_limit : int = typer .Option (
65+ 20 ,
66+ "--sample-names-limit" ,
67+ min = 1 ,
68+ help = "Maximum number of sample names to display per list in terminal output." ,
69+ ),
70+ sample_names_output : str | None = typer .Option (
71+ None ,
72+ "--sample-names-output" ,
73+ help = "Write full sample names to file instead of truncating in terminal output. "
74+ "Mutually exclusive with --sample-names-stdout." ,
75+ ),
76+ sample_names_stdout : bool = typer .Option (
77+ False ,
78+ "--sample-names-stdout" ,
79+ help = "Print full sample names to stdout (useful for piping). Mutually exclusive with --sample-names-output." ,
80+ ),
6381 project : str | None = PROJECT_NAME_OPTION ,
6482) -> None :
6583 """
@@ -69,6 +87,10 @@ def show_dimensions_index(
6987
7088 project_config = resolve_project (project_name = project )
7189
90+ # These two options are mutually exclusive. But due to how typer handles options, this error will only be raise if --sample-names-output has an input value (i.e. path). If the path is missing, it will raise an error about the missing path argument instead...
91+ if sample_names_output and sample_names_stdout :
92+ raise typer .BadParameter ("Use only one of --sample-names-output or --sample-names-stdout." )
93+
7294 if unique_samples :
7395 response = make_authenticated_request (
7496 method = "GET" ,
@@ -77,6 +99,25 @@ def show_dimensions_index(
7799 )
78100 unique_sample_names_sorted = DimensionsSamplesResult (** response .json ()).unique_samples
79101 sample_count = len (unique_sample_names_sorted )
102+
103+ if sample_names_output :
104+ output_path = Path (sample_names_output )
105+ output_path .write_text ("\n " .join (unique_sample_names_sorted ) + "\n " )
106+ print (f"Wrote { sample_count } unique sample names to: { output_path } " )
107+ return
108+
109+ if sample_names_stdout :
110+ print ("\n " .join (unique_sample_names_sorted ))
111+ return
112+
113+ if sample_count > sample_names_limit :
114+ preview = unique_sample_names_sorted [:sample_names_limit ]
115+ print (
116+ f"Unique sample names found across all the VCF files in the project (count: { sample_count } , showing first { sample_names_limit } ):\n { preview } \n "
117+ f"To view all, use --sample-names-output <FILE> or --sample-names-stdout."
118+ )
119+ return
120+
80121 print (
81122 f"Unique sample names found across all the VCF files in the project (count: { sample_count } ):\n { unique_sample_names_sorted } "
82123 )
@@ -111,6 +152,15 @@ def show_dimensions_index(
111152 record = entry
112153 break
113154 if record :
155+ if sample_names_output or sample_names_stdout :
156+ _write_or_print_sample_names (
157+ indexed_files = [record ],
158+ sample_names_output = sample_names_output ,
159+ sample_names_stdout = sample_names_stdout ,
160+ )
161+ return
162+
163+ _truncate_sample_names_in_entry (record , sample_names_limit )
114164 print (yaml .safe_dump (record , sort_keys = False ))
115165 else :
116166 print (
@@ -119,6 +169,17 @@ def show_dimensions_index(
119169 )
120170 return
121171
172+ if sample_names_output or sample_names_stdout :
173+ _write_or_print_sample_names (
174+ indexed_files = dimensions_info .get ("indexed_files" , []),
175+ sample_names_output = sample_names_output ,
176+ sample_names_stdout = sample_names_stdout ,
177+ )
178+ return
179+
180+ for entry in dimensions_info .get ("indexed_files" , []):
181+ _truncate_sample_names_in_entry (entry , sample_names_limit )
182+
122183 print (yaml .safe_dump (dimensions_info , sort_keys = False ))
123184
124185
@@ -163,3 +224,43 @@ def sort_scaffolds(scaffolds: list[str]) -> list[str]:
163224 "indexed_files" : dimensions_list ,
164225 "skipped_files" : skipped_list ,
165226 }
227+
228+
229+ def _truncate_sample_names_in_entry (entry : dict , sample_names_limit : int ) -> None :
230+ """
231+ Truncate sample names output in terminal, while preserving full counts.
232+ """
233+ dimensions = entry .get ("dimensions" , {})
234+ sample_names = dimensions .get ("sample_names" , [])
235+ if len (sample_names ) > sample_names_limit :
236+ dimensions ["sample_names" ] = sample_names [:sample_names_limit ]
237+ dimensions ["sample_names_note" ] = (
238+ f"Showing first { sample_names_limit } of { len (sample_names )} samples. "
239+ "Use --sample-names-output <FILE> or --sample-names-stdout to view all."
240+ )
241+
242+
243+ def _write_or_print_sample_names (
244+ indexed_files : list [dict ],
245+ sample_names_output : str | None ,
246+ sample_names_stdout : bool ,
247+ ) -> None :
248+ """
249+ Export full sample names data from divbase-cli dimensions show
250+ either to a file or to stdout. If used without --unique-samples, it will also include the filename that each sample occurs in.
251+ """
252+ lines : list [str ] = []
253+ for entry in indexed_files :
254+ file_name = entry .get ("filename" , "" )
255+ sample_names = entry .get ("dimensions" , {}).get ("sample_names" , [])
256+ for sample_name in sample_names :
257+ lines .append (f"{ file_name } \t { sample_name } " )
258+
259+ if sample_names_output :
260+ output_path = Path (sample_names_output )
261+ output_path .write_text ("\n " .join (lines ) + ("\n " if lines else "" ))
262+ print (f"Wrote { len (lines )} sample-name rows to: { output_path } " )
263+ return
264+
265+ if sample_names_stdout :
266+ print ("\n " .join (lines ))
0 commit comments