pywombat 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywombat
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: A CLI tool for processing and filtering bcftools tabulated TSV files with pedigree support
5
5
  Project-URL: Homepage, https://github.com/bourgeron-lab/pywombat
6
6
  Project-URL: Repository, https://github.com/bourgeron-lab/pywombat
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pywombat"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "A CLI tool for processing and filtering bcftools tabulated TSV files with pedigree support"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Freddy Cliquet", email = "fcliquet@pasteur.fr" }]
@@ -40,6 +40,11 @@ import yaml
40
40
  type=click.Path(exists=True, path_type=Path),
41
41
  help="Filter configuration YAML file to apply quality and impact filters.",
42
42
  )
43
+ @click.option(
44
+ "--debug",
45
+ type=str,
46
+ help="Debug mode: show rows matching chrom:pos (e.g., chr11:70486013). Displays #CHROM, POS, VEP_SYMBOL, and columns from filter expression.",
47
+ )
43
48
  def cli(
44
49
  input_file: Path,
45
50
  output: Optional[str],
@@ -47,6 +52,7 @@ def cli(
47
52
  verbose: bool,
48
53
  pedigree: Optional[Path],
49
54
  filter_config: Optional[Path],
55
+ debug: Optional[str],
50
56
  ):
51
57
  """
52
58
  Wombat: A tool for processing bcftools tabulated TSV files.
@@ -94,6 +100,11 @@ def cli(
94
100
  click.echo(f"Reading filter config: {filter_config}", err=True)
95
101
  filter_config_data = load_filter_config(filter_config)
96
102
 
103
+ # Debug mode: show specific variant
104
+ if debug:
105
+ debug_variant(input_file, pedigree_df, filter_config_data, debug, verbose)
106
+ return
107
+
97
108
  # Determine output prefix
98
109
  if output is None:
99
110
  # Generate default output prefix from input filename
@@ -147,6 +158,95 @@ def cli(
147
158
  raise click.Abort()
148
159
 
149
160
 
161
+ def debug_variant(
162
+ input_file: Path,
163
+ pedigree_df: Optional[pl.DataFrame],
164
+ filter_config: Optional[dict],
165
+ debug_pos: str,
166
+ verbose: bool,
167
+ ):
168
+ """Debug mode: display rows matching a specific chrom:pos."""
169
+ # Parse debug position
170
+ if ":" not in debug_pos:
171
+ click.echo(
172
+ "Error: Debug position must be in format 'chrom:pos' (e.g., chr11:70486013)",
173
+ err=True,
174
+ )
175
+ raise click.Abort()
176
+
177
+ chrom, pos = debug_pos.split(":", 1)
178
+ try:
179
+ pos = int(pos)
180
+ except ValueError:
181
+ click.echo(f"Error: Position must be an integer, got '{pos}'", err=True)
182
+ raise click.Abort()
183
+
184
+ if verbose:
185
+ click.echo(f"Debug mode: searching for {chrom}:{pos}", err=True)
186
+
187
+ # Read and format the data
188
+ df = pl.read_csv(input_file, separator="\t")
189
+ formatted_df = format_bcftools_tsv(df, pedigree_df)
190
+
191
+ # Filter to matching rows
192
+ matching_rows = formatted_df.filter(
193
+ (pl.col("#CHROM") == chrom) & (pl.col("POS") == pos)
194
+ )
195
+
196
+ if matching_rows.shape[0] == 0:
197
+ click.echo(f"No rows found matching {chrom}:{pos}", err=True)
198
+ return
199
+
200
+ # Determine which columns to display
201
+ columns_to_show = ["#CHROM", "POS"]
202
+
203
+ # Add VEP_SYMBOL if it exists
204
+ if "VEP_SYMBOL" in matching_rows.columns:
205
+ columns_to_show.append("VEP_SYMBOL")
206
+
207
+ # Extract column names from expression if filter config provided
208
+ if filter_config and "expression" in filter_config:
209
+ expression = filter_config["expression"]
210
+ # Extract column names from expression using regex
211
+ # Match patterns like "column_name" before operators
212
+ column_pattern = r"\b([A-Za-z_][A-Za-z0-9_]*)\b\s*[=!<>]"
213
+ found_columns = re.findall(column_pattern, expression)
214
+
215
+ for col in found_columns:
216
+ if col in matching_rows.columns and col not in columns_to_show:
217
+ columns_to_show.append(col)
218
+
219
+ # Select only the columns we want to display
220
+ display_df = matching_rows.select(
221
+ [col for col in columns_to_show if col in matching_rows.columns]
222
+ )
223
+
224
+ # Replace null and NaN values with <null> and <NaN> for display
225
+ for col in display_df.columns:
226
+ if display_df[col].dtype in [pl.Float32, pl.Float64]:
227
+ # For numeric columns, handle both NaN and null
228
+ display_df = display_df.with_columns(
229
+ pl.when(pl.col(col).is_null())
230
+ .then(pl.lit("<null>"))
231
+ .when(pl.col(col).is_nan())
232
+ .then(pl.lit("<NaN>"))
233
+ .otherwise(pl.col(col).cast(pl.Utf8))
234
+ .alias(col)
235
+ )
236
+ else:
237
+ # For non-numeric columns, only handle null
238
+ display_df = display_df.with_columns(
239
+ pl.when(pl.col(col).is_null())
240
+ .then(pl.lit("<null>"))
241
+ .otherwise(pl.col(col).cast(pl.Utf8))
242
+ .alias(col)
243
+ )
244
+
245
+ # Display the results
246
+ click.echo(f"\nFound {matching_rows.shape[0]} row(s) matching {chrom}:{pos}:\n")
247
+ click.echo(display_df.write_csv(separator="\t"))
248
+
249
+
150
250
  def load_filter_config(config_path: Path) -> dict:
151
251
  """Load and parse filter configuration from YAML file."""
152
252
  with open(config_path, "r") as f:
@@ -366,6 +466,30 @@ def parse_impact_filter_expression(expression: str, df: pl.DataFrame) -> pl.Expr
366
466
  if col_name not in df.columns:
367
467
  raise ValueError(f"Column '{col_name}' not found in dataframe")
368
468
 
469
+ # Check for null value
470
+ if value.upper() == "NULL":
471
+ col_expr = pl.col(col_name)
472
+ if op == "=":
473
+ return col_expr.is_null()
474
+ elif op == "!=":
475
+ return ~col_expr.is_null()
476
+ else:
477
+ raise ValueError(
478
+ f"Operator '{op}' not supported for null comparison, use = or !="
479
+ )
480
+
481
+ # Check for NaN value
482
+ if value.upper() == "NAN":
483
+ col_expr = pl.col(col_name).cast(pl.Float64, strict=False)
484
+ if op == "=":
485
+ return col_expr.is_nan()
486
+ elif op == "!=":
487
+ return ~col_expr.is_nan()
488
+ else:
489
+ raise ValueError(
490
+ f"Operator '{op}' not supported for NaN comparison, use = or !="
491
+ )
492
+
369
493
  # Try to convert value to number, otherwise treat as string
370
494
  try:
371
495
  value_num = float(value)
@@ -51,7 +51,7 @@ wheels = [
51
51
 
52
52
  [[package]]
53
53
  name = "pywombat"
54
- version = "0.2.0"
54
+ version = "0.3.0"
55
55
  source = { editable = "." }
56
56
  dependencies = [
57
57
  { name = "click" },
File without changes
File without changes
File without changes
File without changes