From 3f21e0b8ff26bb87d09aeaedf9baa33f744ee885 Mon Sep 17 00:00:00 2001 From: maziggy Date: Thu, 16 Jul 2026 10:13:24 +0200 Subject: [PATCH] follow-up(queue): cache per-plate 3MF metadata so queue polling stops re-parsing every row (#2573) --- backend/app/utils/threemf_tools.py | 41 +++++++++++++----------- backend/tests/unit/test_threemf_tools.py | 31 ++++++++++++++++++ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/backend/app/utils/threemf_tools.py b/backend/app/utils/threemf_tools.py index 2d2ef087a..90fcce99a 100644 --- a/backend/app/utils/threemf_tools.py +++ b/backend/app/utils/threemf_tools.py @@ -493,19 +493,23 @@ def _parse_plate_metadata_uncached(file_path: Path, plate_id: int | None) -> Pla out: list[dict] = [] for f in plate_elem.findall("filament"): filament_id = f.get("id") + # Both the used_g float() and the id int() must stay inside the guard: + # a non-numeric id or used_g is silently skipped (matches the legacy + # helpers, which tolerated garbage rows rather than raising — a raise + # here would 500 the whole queue listing). try: used_amount = float(f.get("used_g", "0")) + if filament_id: + out.append( + { + "slot_id": int(filament_id), + "used_g": used_amount, + "type": f.get("type", ""), + "color": f.get("color", ""), + } + ) except (ValueError, TypeError): continue - if filament_id: - out.append( - { - "slot_id": int(filament_id), - "used_g": used_amount, - "type": f.get("type", ""), - "color": f.get("color", ""), - } - ) return out print_time: int | None = None @@ -539,19 +543,20 @@ def _parse_plate_metadata_uncached(file_path: Path, plate_id: int | None) -> Pla # Legacy plate_id=None usage: every filament in the file, not just plate 1. for f in root.findall(".//filament"): filament_id = f.get("id") + # int()/float() both guarded — a garbage id/used_g row is skipped, not raised. try: used_amount = float(f.get("used_g", "0")) + if filament_id: + filament_usage.append( + { + "slot_id": int(filament_id), + "used_g": used_amount, + "type": f.get("type", ""), + "color": f.get("color", ""), + } + ) except (ValueError, TypeError): continue - if filament_id: - filament_usage.append( - { - "slot_id": int(filament_id), - "used_g": used_amount, - "type": f.get("type", ""), - "color": f.get("color", ""), - } - ) return PlateMetadata( print_time_seconds=print_time, diff --git a/backend/tests/unit/test_threemf_tools.py b/backend/tests/unit/test_threemf_tools.py index 651c8a2be..4738f1fb5 100644 --- a/backend/tests/unit/test_threemf_tools.py +++ b/backend/tests/unit/test_threemf_tools.py @@ -1151,6 +1151,37 @@ class TestExtractPlateMetadataFrom3mf: assert len(cached.filament_usage) == 2 assert all(f["used_g"] > 0 for f in cached.filament_usage) + def test_non_numeric_filament_id_is_skipped_not_raised(self, tmp_path): + # A garbage filament id (or used_g) must be silently skipped, exactly as + # the legacy helpers did — a raise here would 500 the queue listing that + # calls this per row. Guards both the plate-specific and plate_id=None paths. + from backend.app.utils.threemf_tools import ( + extract_filament_usage_from_3mf, + extract_plate_metadata_from_3mf, + ) + + xml_content = """ + + + + + + + + + + """ + file_path = self._write(tmp_path, xml_content) + + meta = extract_plate_metadata_from_3mf(file_path, plate_id=1) + assert [f["slot_id"] for f in meta.filament_usage] == [1] + assert meta.filament_used_grams == 10.0 + assert meta.print_time_seconds == 3600 + + # plate_id=None path (collects all filaments in the file) must skip too. + none_result = extract_filament_usage_from_3mf(file_path, plate_id=None) + assert [f["slot_id"] for f in none_result] == [1] + def test_missing_file_returns_empty_and_is_not_cached(self, tmp_path): from unittest.mock import patch