openms-insight 0.1.9__py3-none-any.whl → 0.1.11__py3-none-any.whl

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.
@@ -238,9 +238,10 @@ def downsample_2d_simple(
238
238
  data: Union[pl.LazyFrame, pl.DataFrame],
239
239
  max_points: int = 20000,
240
240
  intensity_column: str = "intensity",
241
+ descending: bool = True,
241
242
  ) -> pl.LazyFrame:
242
243
  """
243
- Simple downsampling by keeping highest-intensity points.
244
+ Simple downsampling by keeping top-priority points.
244
245
 
245
246
  A simpler alternative to downsample_2d that doesn't require scipy.
246
247
  Less spatially aware but still preserves important peaks.
@@ -249,6 +250,7 @@ def downsample_2d_simple(
249
250
  data: Input data as Polars LazyFrame or DataFrame
250
251
  max_points: Maximum number of points to keep
251
252
  intensity_column: Name of intensity column for ranking
253
+ descending: If True (default), keep highest values. If False, keep lowest.
252
254
 
253
255
  Returns:
254
256
  Downsampled data as Polars LazyFrame
@@ -256,7 +258,7 @@ def downsample_2d_simple(
256
258
  if isinstance(data, pl.DataFrame):
257
259
  data = data.lazy()
258
260
 
259
- return data.sort(intensity_column, descending=True).head(max_points)
261
+ return data.sort(intensity_column, descending=descending).head(max_points)
260
262
 
261
263
 
262
264
  def downsample_2d_streaming(
@@ -269,6 +271,7 @@ def downsample_2d_streaming(
269
271
  y_bins: int = 50,
270
272
  x_range: Optional[tuple] = None,
271
273
  y_range: Optional[tuple] = None,
274
+ descending: bool = True,
272
275
  ) -> pl.LazyFrame:
273
276
  """
274
277
  Streaming 2D downsampling using pure Polars operations.
@@ -287,6 +290,8 @@ def downsample_2d_streaming(
287
290
  y_bins: Number of bins along y-axis
288
291
  x_range: Optional (min, max) tuple for x-axis. If None, computed from data.
289
292
  y_range: Optional (min, max) tuple for y-axis. If None, computed from data.
293
+ descending: If True (default), keep highest intensity per bin.
294
+ If False, keep lowest intensity per bin.
290
295
 
291
296
  Returns:
292
297
  Downsampled data as Polars LazyFrame (fully lazy, no collection)
@@ -319,7 +324,7 @@ def downsample_2d_streaming(
319
324
 
320
325
  result = (
321
326
  data.with_columns([x_bin_expr, y_bin_expr])
322
- .sort(intensity_column, descending=True)
327
+ .sort(intensity_column, descending=descending)
323
328
  .group_by(["_x_bin", "_y_bin"])
324
329
  .head(points_per_bin)
325
330
  .drop(["_x_bin", "_y_bin"])
@@ -349,7 +354,7 @@ def downsample_2d_streaming(
349
354
  .alias("_y_bin"),
350
355
  ]
351
356
  )
352
- .sort(intensity_column, descending=True)
357
+ .sort(intensity_column, descending=descending)
353
358
  .group_by(["_x_bin", "_y_bin"])
354
359
  .head(points_per_bin)
355
360
  .drop(["_x_bin", "_y_bin"])
@@ -16,6 +16,20 @@ _DEBUG_HASH_TRACKING = os.environ.get("SVC_DEBUG_HASH", "").lower() == "true"
16
16
  _DEBUG_STATE_SYNC = os.environ.get("SVC_DEBUG_STATE", "").lower() == "true"
17
17
  _logger = logging.getLogger(__name__)
18
18
 
19
+ # Numeric data types for type conversion during selection validation
20
+ NUMERIC_DTYPES = (
21
+ pl.Int8,
22
+ pl.Int16,
23
+ pl.Int32,
24
+ pl.Int64,
25
+ pl.UInt8,
26
+ pl.UInt16,
27
+ pl.UInt32,
28
+ pl.UInt64,
29
+ pl.Float32,
30
+ pl.Float64,
31
+ )
32
+
19
33
 
20
34
  def _make_hashable(value: Any) -> Any:
21
35
  """
@@ -237,9 +251,7 @@ def _prepare_vue_data_cached(
237
251
 
238
252
  if _DEBUG_HASH_TRACKING:
239
253
  cache_hit = cached is not None
240
- _logger.warning(
241
- f"[CacheDebug] {component._cache_id}: cache_hit={cache_hit}"
242
- )
254
+ _logger.warning(f"[CacheDebug] {component._cache_id}: cache_hit={cache_hit}")
243
255
 
244
256
  if cached is not None:
245
257
  cached_data, cached_hash, _ = cached # Ignore cached annotation hash here
@@ -259,7 +271,16 @@ def _prepare_vue_data_cached(
259
271
  data_hash = _hash_data(vue_data)
260
272
  return vue_data, data_hash
261
273
  else:
262
- # No dynamic annotations - return cached as-is
274
+ # No dynamic annotations - ensure _plotConfig is present
275
+ # When annotations are cleared, Vue needs _plotConfig with null columns
276
+ # to stop showing stale annotations (Vue merge only updates keys present)
277
+ if hasattr(component, "_build_plot_config"):
278
+ vue_data = dict(cached_data)
279
+ vue_data["_plotConfig"] = component._build_plot_config(
280
+ getattr(component, "_highlight_column", None),
281
+ getattr(component, "_annotation_column", None),
282
+ )
283
+ return vue_data, cached_hash
263
284
  return cached_data, cached_hash
264
285
 
265
286
  # Cache miss - compute data
@@ -276,7 +297,9 @@ def _prepare_vue_data_cached(
276
297
  # Fallback: store without _plotConfig (may have stale column refs)
277
298
  base_data = {k: v for k, v in vue_data.items() if k != "_plotConfig"}
278
299
  base_hash = _hash_data(base_data)
279
- _set_cached_vue_data(component_id, filter_state_hashable, base_data, base_hash, ann_hash)
300
+ _set_cached_vue_data(
301
+ component_id, filter_state_hashable, base_data, base_hash, ann_hash
302
+ )
280
303
 
281
304
  # Return full data with annotations
282
305
  data_hash = _hash_data(vue_data)
@@ -284,7 +307,9 @@ def _prepare_vue_data_cached(
284
307
  else:
285
308
  # Store complete data in cache
286
309
  data_hash = _hash_data(vue_data)
287
- _set_cached_vue_data(component_id, filter_state_hashable, vue_data, data_hash, ann_hash)
310
+ _set_cached_vue_data(
311
+ component_id, filter_state_hashable, vue_data, data_hash, ann_hash
312
+ )
288
313
  return vue_data, data_hash
289
314
 
290
315
 
@@ -330,6 +355,113 @@ def get_vue_component_function():
330
355
  return _vue_component_func
331
356
 
332
357
 
358
+ def _validate_interactivity_selections(
359
+ component: "BaseComponent",
360
+ state_manager: "StateManager",
361
+ state: Dict[str, Any],
362
+ ) -> bool:
363
+ """
364
+ Validate that interactivity selections still exist in filtered data.
365
+
366
+ When an external filter changes, the currently selected value may no longer
367
+ exist in the filtered dataset. This function checks each interactivity
368
+ selection and clears it if invalid.
369
+
370
+ Args:
371
+ component: The component to validate selections for
372
+ state_manager: StateManager to clear invalid selections
373
+ state: Current state dict (already has filter values)
374
+
375
+ Returns:
376
+ True if any selection was cleared (state changed), False otherwise
377
+ """
378
+ interactivity = getattr(component, "_interactivity", None)
379
+ if not interactivity:
380
+ return False
381
+
382
+ filters = getattr(component, "_filters", None) or {}
383
+ filter_defaults = getattr(component, "_filter_defaults", None) or {}
384
+
385
+ # Get the preprocessed data
386
+ preprocessed = getattr(component, "_preprocessed_data", None)
387
+ if preprocessed is not None:
388
+ data = preprocessed.get("data")
389
+ else:
390
+ data = None
391
+ if data is None:
392
+ data = getattr(component, "_raw_data", None)
393
+ if data is None:
394
+ # Component doesn't have data we can validate against
395
+ return False
396
+
397
+ # Ensure LazyFrame
398
+ if isinstance(data, pl.DataFrame):
399
+ data = data.lazy()
400
+
401
+ # Apply filters to get the filtered dataset
402
+ for identifier, column in filters.items():
403
+ selected_value = state.get(identifier)
404
+ if selected_value is None and identifier in filter_defaults:
405
+ selected_value = filter_defaults[identifier]
406
+
407
+ if selected_value is None:
408
+ # Awaiting filter - no data to validate against
409
+ return False
410
+
411
+ # Convert float to int for integer columns (type mismatch handling)
412
+ if isinstance(selected_value, float) and selected_value.is_integer():
413
+ selected_value = int(selected_value)
414
+
415
+ data = data.filter(pl.col(column) == selected_value)
416
+
417
+ # Collect all checks to validate
418
+ state_changed = False
419
+ schema = data.collect_schema()
420
+ checks_to_validate: list[tuple[str, str, Any]] = []
421
+
422
+ for identifier, column in interactivity.items():
423
+ selected_value = state.get(identifier)
424
+ if selected_value is None:
425
+ continue # No selection to validate
426
+
427
+ if column not in schema.names():
428
+ continue # Column doesn't exist
429
+
430
+ # Type conversion for numeric columns
431
+ col_dtype = schema[column]
432
+ if col_dtype in NUMERIC_DTYPES:
433
+ if isinstance(selected_value, float) and selected_value.is_integer():
434
+ selected_value = int(selected_value)
435
+
436
+ checks_to_validate.append((identifier, column, selected_value))
437
+
438
+ # Early return if nothing to validate
439
+ if not checks_to_validate:
440
+ return False
441
+
442
+ # Build single query with all existence checks
443
+ existence_exprs = [
444
+ (pl.col(column) == value).any().alias(identifier)
445
+ for identifier, column, value in checks_to_validate
446
+ ]
447
+
448
+ # Execute ONE query instead of N
449
+ results = data.select(existence_exprs).collect().row(0, named=True)
450
+
451
+ # Process results and clear invalid selections
452
+ for identifier, _column, value in checks_to_validate:
453
+ if not results[identifier]:
454
+ state_manager.set_selection(identifier, None)
455
+ state_changed = True
456
+ if _DEBUG_STATE_SYNC:
457
+ _logger.warning(
458
+ f"[Bridge:{component._cache_id}] Cleared invalid selection: "
459
+ f"{identifier}={value} not in filtered data"
460
+ )
461
+
462
+ return state_changed
463
+
464
+
333
465
  def render_component(
334
466
  component: "BaseComponent",
335
467
  state_manager: "StateManager",
@@ -418,8 +550,8 @@ def render_component(
418
550
  cached_ann_hash = None
419
551
 
420
552
  # Cache valid only if BOTH filter state AND annotation state match
421
- filter_state_matches = (cached_state == current_filter_state)
422
- ann_state_matches = (cached_ann_hash == current_ann_hash)
553
+ filter_state_matches = cached_state == current_filter_state
554
+ ann_state_matches = cached_ann_hash == current_ann_hash
423
555
  cache_valid = filter_state_matches and ann_state_matches
424
556
 
425
557
  if _DEBUG_STATE_SYNC:
@@ -522,6 +654,15 @@ def render_component(
522
654
  awaiting_filter = True
523
655
  break
524
656
 
657
+ # === Validate interactivity selections BEFORE preparing data ===
658
+ # When filter changes, the current selection may no longer exist
659
+ # in the filtered data. Clear it so auto-selection can kick in.
660
+ if not awaiting_filter:
661
+ if _validate_interactivity_selections(component, state_manager, state):
662
+ state_changed = True
663
+ # Refresh state after clearing invalid selections
664
+ state = state_manager.get_state_for_vue()
665
+
525
666
  if not awaiting_filter:
526
667
  # Extract state keys that affect this component's data
527
668
  state_keys = set(component.get_state_dependencies())
@@ -548,6 +689,14 @@ def render_component(
548
689
  component, component_id, filter_state_hashable, relevant_state
549
690
  )
550
691
 
692
+ # Apply auto-selection from first row (for tables with interactivity)
693
+ # This ensures downstream components receive data when a filter changes
694
+ auto_selection = vue_data.pop("_auto_selection", {})
695
+ for identifier, value in auto_selection.items():
696
+ if state_manager.get_selection(identifier) is None:
697
+ state_manager.set_selection(identifier, value)
698
+ state_changed = True
699
+
551
700
  # Check if Python overrode state during _prepare_vue_data
552
701
  # (e.g., table.py sets page to last page after sort)
553
702
  final_state = state_manager.get_state_for_vue()
@@ -562,7 +711,9 @@ def render_component(
562
711
  data_hash = "awaiting_filter"
563
712
  filter_state_hashable = ()
564
713
 
565
- _logger.info(f"[bridge] Phase4: {component._cache_id} prepared data, hash={data_hash[:8] if data_hash else 'None'}")
714
+ _logger.info(
715
+ f"[bridge] Phase4: {component._cache_id} prepared data, hash={data_hash[:8] if data_hash else 'None'}"
716
+ )
566
717
 
567
718
  # === PHASE 5: Cache data for next render ===
568
719
  if vue_data:
@@ -579,7 +730,12 @@ def render_component(
579
730
  converted_data[data_key] = value
580
731
 
581
732
  # Store in cache for next render (include annotation hash for validity check)
582
- cache[component_id] = (filter_state_hashable, converted_data, data_hash, current_ann_hash)
733
+ cache[component_id] = (
734
+ filter_state_hashable,
735
+ converted_data,
736
+ data_hash,
737
+ current_ann_hash,
738
+ )
583
739
 
584
740
  # If cache was invalid at Phase 1, we didn't send data to Vue (dataChanged=False).
585
741
  # Trigger a rerun so the newly cached data gets sent on the next render.
@@ -595,9 +751,13 @@ def render_component(
595
751
 
596
752
  if _DEBUG_STATE_SYNC:
597
753
  # Log what we're caching for debugging
598
- pagination_key = next((k for k, v in filter_state_hashable if "page" in k.lower()), None)
754
+ pagination_key = next(
755
+ (k for k, v in filter_state_hashable if "page" in k.lower()), None
756
+ )
599
757
  if pagination_key:
600
- pagination_val = next((v for k, v in filter_state_hashable if k == pagination_key), None)
758
+ pagination_val = next(
759
+ (v for k, v in filter_state_hashable if k == pagination_key), None
760
+ )
601
761
  _logger.warning(
602
762
  f"[Bridge:{component._cache_id}] Phase5: Cached data with hash={data_hash[:8]}, "
603
763
  f"filter_state includes {pagination_key}={pagination_val}"
@@ -625,6 +785,7 @@ def render_component(
625
785
  if st.session_state.get(ann_hash_key) is not None:
626
786
  annotations_changed = True
627
787
  st.session_state[ann_hash_key] = None
788
+ _store_component_annotations(key, None)
628
789
 
629
790
  if annotations_changed:
630
791
  state_changed = True
@@ -664,7 +825,11 @@ def _hash_data(data: Dict[str, Any]) -> str:
664
825
  hash_parts = []
665
826
  for key, value in sorted(data.items()):
666
827
  # Skip internal metadata but NOT dynamic annotation columns or pagination
667
- if key.startswith("_") and not key.startswith("_dynamic") and not key.startswith("_pagination"):
828
+ if (
829
+ key.startswith("_")
830
+ and not key.startswith("_dynamic")
831
+ and not key.startswith("_pagination")
832
+ ):
668
833
  continue
669
834
  if isinstance(value, pd.DataFrame):
670
835
  # Efficient hash for DataFrames
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openms-insight
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Interactive visualization components for mass spectrometry data in Streamlit
5
5
  Project-URL: Homepage, https://github.com/t0mdavid-m/OpenMS-Insight
6
6
  Project-URL: Documentation, https://github.com/t0mdavid-m/OpenMS-Insight#readme
@@ -243,7 +243,9 @@ Heatmap(
243
243
  - `min_points`: Target size for downsampling (default: 20000)
244
244
  - `x_bins`, `y_bins`: Grid resolution for spatial binning
245
245
  - `colorscale`: Plotly colorscale name (default: 'Portland')
246
+ - `reversescale`: Invert colorscale direction (default: False)
246
247
  - `log_scale`: Use log10 color mapping (default: True). Set to False for linear.
248
+ - `low_values_on_top`: Prioritize low values during downsampling and display them on top (default: False). Use for scores where lower = better (e.g., e-values, PEP, q-values).
247
249
  - `intensity_label`: Custom colorbar label (default: 'Intensity')
248
250
 
249
251
  **Linear scale example:**
@@ -260,6 +262,24 @@ Heatmap(
260
262
  )
261
263
  ```
262
264
 
265
+ **Low values on top (PSM scores):**
266
+ For identification results where lower scores indicate better matches (e.g., e-values, PEP, q-values), use `low_values_on_top=True` to preserve low-scoring points during downsampling and display them on top of high-scoring points:
267
+
268
+ ```python
269
+ Heatmap(
270
+ cache_id="psm_evalue",
271
+ data_path="psm_data.parquet",
272
+ x_column='rt',
273
+ y_column='mz',
274
+ intensity_column='e_value',
275
+ log_scale=True, # Log scale for e-values
276
+ low_values_on_top=True, # Keep/show low e-values (best hits)
277
+ reversescale=True, # Bright color = low value = best
278
+ intensity_label='E-value',
279
+ colorscale='Portland',
280
+ )
281
+ ```
282
+
263
283
  **Categorical mode:**
264
284
  Use `category_column` for discrete coloring by category instead of continuous intensity colorscale:
265
285
 
@@ -1,9 +1,9 @@
1
- openms_insight/__init__.py,sha256=b4H9k0fPVZ6jCiJ4QVSwzlUQnyElF6ZwAlDI8fdPMJE,1111
1
+ openms_insight/__init__.py,sha256=tmst1YCzXuVLQAs7Hzit5idMPmJsAqn3djpV8fVduYU,1112
2
2
  openms_insight/components/__init__.py,sha256=Lcg-D0FILta-YVgMJBlWMKLKC5G5kXOqdy9hBOENABw,233
3
- openms_insight/components/heatmap.py,sha256=psrdW4gNzZR1jAIox9YS9EHaZaTRrDHFR0t2_0APU9Y,47214
3
+ openms_insight/components/heatmap.py,sha256=Yek06-TCDformcR42VeUCu0d7WDtRQPCpNQ5kOnfv5w,48372
4
4
  openms_insight/components/lineplot.py,sha256=I-JPvDzCr3Nu8Boc1V4D8QQ1bHgTqvM6CbeoIe7zJ-s,30896
5
- openms_insight/components/sequenceview.py,sha256=0pDOE0xeoc1-85QZNGdNwwoBwXi-5MFfeb9pCcOi6rc,30274
6
- openms_insight/components/table.py,sha256=6r0SiWTDSJS6AHNCy4jTxByY9aCt4ussCQFKKrEg77U,36955
5
+ openms_insight/components/sequenceview.py,sha256=ufvtzuU9zaAybsT3pqfqIR0ZHCl7dKJ-jk3kwJALr54,31241
6
+ openms_insight/components/table.py,sha256=gmgWlsd5GXV2NRkYoDmhi4asaS0pzwoW0C4yf5jI_cI,42740
7
7
  openms_insight/components/volcanoplot.py,sha256=F-cmYxJMKXVK-aYJpifp8be7nB8hkQd2kLi9DrBElD8,15155
8
8
  openms_insight/core/__init__.py,sha256=EPjKX_FFQRgO8mWHs59I-o0BiuzEMzEU1Pfu9YOfLC4,338
9
9
  openms_insight/core/base.py,sha256=h0OaubHLky8mk7Yfy3HTIimsz-DfuNRgLfotJu3pZVw,20517
@@ -12,19 +12,19 @@ openms_insight/core/registry.py,sha256=Hak80Jqhx0qa4gbd1YolNZnM6xBrS8I4U_X7zC0bQ
12
12
  openms_insight/core/state.py,sha256=CMToxxNyGnqxMccwOcn7FwABNTzjjTsgsMrJCZOZc2o,12438
13
13
  openms_insight/core/subprocess_preprocess.py,sha256=m9FbAAFy9Do1Exlh-m4Wo-LDwv6yHlEI4klz5OVwemc,3133
14
14
  openms_insight/preprocessing/__init__.py,sha256=xoGdhNVrX8Ty3ywmyaCcWAO3a6QlKceO1xxsy1C8ZTI,596
15
- openms_insight/preprocessing/compression.py,sha256=T4YbX9PUlfTfPit_kpuLZn8hYpqLYu3xtTme_CG2ymc,12241
15
+ openms_insight/preprocessing/compression.py,sha256=jHcZqjcFCKb4rRns4PY2DglQecbO0cSThB523bUvUtM,12519
16
16
  openms_insight/preprocessing/filtering.py,sha256=fkmaIXfR5hfjyWfaMYqaeybMHaZjvUZYaKCqvxPOWMQ,14152
17
17
  openms_insight/preprocessing/scatter.py,sha256=2ifCNTUKHEW9UVpv4z9c5GaLnz5zw9o1119IenzAe9s,4703
18
18
  openms_insight/rendering/__init__.py,sha256=ApHvKeh87yY4GTIEai-tCeIXpNbwOXWlmcmIwMMRZYc,198
19
- openms_insight/rendering/bridge.py,sha256=a_6lq3jR9tDFxdSxMjeJhyV_Tgw7ALcm-UFEvl_TE_M,25615
19
+ openms_insight/rendering/bridge.py,sha256=xFM74gbyzQHFmKt61-hk2ouYZ7KAG7HHosvwmKNBY_M,31311
20
20
  openms_insight/js-component/dist/index.html,sha256=LSJ3B_YmGUrCCdZ1UaZO2p6Wqsih6nTH62Z_0uZxpD8,430
21
- openms_insight/js-component/dist/assets/index.css,sha256=wFvo7FbG132LL7uw0slXfrL_oSQ8u2RKL1DW9hD9-kk,884264
22
- openms_insight/js-component/dist/assets/index.js,sha256=aqGc3g7XLTRr7ptEgoA3XDu5oMS47yxxjUBXgansIo0,6091480
21
+ openms_insight/js-component/dist/assets/index.css,sha256=s0J7lg0ben8EwwtgSI5tDqNPmPlTI4xOy0XoKsum46Y,884264
22
+ openms_insight/js-component/dist/assets/index.js,sha256=TjpKz5QuvUcdM5-iXP4cHh49m0U4z618yJMHRpS0gEc,6092354
23
23
  openms_insight/js-component/dist/assets/materialdesignicons-webfont.eot,sha256=CxgxBNL8XyYZbnc8d72vLgVQn9QlnS0V7O3Kebh-hPk,1307880
24
24
  openms_insight/js-component/dist/assets/materialdesignicons-webfont.ttf,sha256=YeirpaTpgf4iz3yOi82-oAR251xiw38Bv37jM2HWhCg,1307660
25
25
  openms_insight/js-component/dist/assets/materialdesignicons-webfont.woff,sha256=pZKKDVwvYk5G-Y2bFcL2AEU3f3xZTdeKF1kTLqO0Y-s,587984
26
26
  openms_insight/js-component/dist/assets/materialdesignicons-webfont.woff2,sha256=Zi_vqPL4qVwYWI0hd0eJwQfGTnccvmWmmvRikcQxGvw,403216
27
- openms_insight-0.1.9.dist-info/METADATA,sha256=--VBuRN6Co39E_BZYFk6OwYkOyWEE9uOq_OBK21PdWE,16787
28
- openms_insight-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
- openms_insight-0.1.9.dist-info/licenses/LICENSE,sha256=INFF4rOMmpah7Oi14hLqu7NTOsx56KRRNChAAUcfh2E,1823
30
- openms_insight-0.1.9.dist-info/RECORD,,
27
+ openms_insight-0.1.11.dist-info/METADATA,sha256=J4jpnQDVfM73lukRzb5JLGZjqeUt-lzbfdtsnMIm8x8,17708
28
+ openms_insight-0.1.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
+ openms_insight-0.1.11.dist-info/licenses/LICENSE,sha256=INFF4rOMmpah7Oi14hLqu7NTOsx56KRRNChAAUcfh2E,1823
30
+ openms_insight-0.1.11.dist-info/RECORD,,