#!/usr/bin/env python3 """Parse temperature values from filament comments and update temp range fields.""" import re import asyncio import sys from pathlib import Path # Add the spoolman module to the path sys.path.insert(0, str(Path(__file__).parent.parent)) from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from spoolman.database.models import Filament def parse_temps_from_comment(comment: str) -> dict: """Parse extruder and bed temps from a comment string. Patterns to match: - "extrude 190-230" or "extruder 190-230" or "nozzle 190-230" - "bed 50-70" - "190-230 / 50-70" (extruder / bed) - "190-230C" or "190-230°C" - Single values like "extrude 210" → min=max=210 """ result = { "extruder_temp_min": None, "extruder_temp_max": None, "bed_temp_min": None, "bed_temp_max": None, } if not comment: return result comment_lower = comment.lower() # Pattern for temperature ranges: 190-230 or 190 - 230 temp_range_pattern = r"(\d{2,3})\s*[-–]\s*(\d{2,3})" # Pattern for single temp: 210 single_temp_pattern = r"(\d{2,3})" # Try to find extruder temp (usually higher values, 150-300 range) extruder_keywords = ["extrude", "extruder", "nozzle", "hotend", "hot end", "printing temp"] for keyword in extruder_keywords: if keyword in comment_lower: # Find temp after keyword idx = comment_lower.index(keyword) search_area = comment[idx:idx+50] # Look in next 50 chars # Try range first match = re.search(temp_range_pattern, search_area) if match: result["extruder_temp_min"] = int(match.group(1)) result["extruder_temp_max"] = int(match.group(2)) break # Try single value match = re.search(single_temp_pattern, search_area[len(keyword):]) if match: val = int(match.group(1)) if 150 <= val <= 350: # Reasonable extruder temp range result["extruder_temp_min"] = val result["extruder_temp_max"] = val break # Try to find bed temp (usually lower values, 0-120 range) bed_keywords = ["bed", "plate", "build plate"] for keyword in bed_keywords: if keyword in comment_lower: # Find temp after keyword idx = comment_lower.index(keyword) search_area = comment[idx:idx+40] # Look in next 40 chars # Try range first match = re.search(temp_range_pattern, search_area) if match: result["bed_temp_min"] = int(match.group(1)) result["bed_temp_max"] = int(match.group(2)) break # Try single value match = re.search(single_temp_pattern, search_area[len(keyword):]) if match: val = int(match.group(1)) if 0 <= val <= 150: # Reasonable bed temp range result["bed_temp_min"] = val result["bed_temp_max"] = val break # If no keywords found, try to find generic temp pattern # Format: "190-230 / 50-70" (extruder / bed separated by /) if result["extruder_temp_min"] is None and "/" in comment: parts = comment.split("/") if len(parts) >= 2: # First part might be extruder match = re.search(temp_range_pattern, parts[0]) if match: t1, t2 = int(match.group(1)), int(match.group(2)) if t1 > 100: # Likely extruder temp result["extruder_temp_min"] = t1 result["extruder_temp_max"] = t2 # Second part might be bed match = re.search(temp_range_pattern, parts[1]) if match: t1, t2 = int(match.group(1)), int(match.group(2)) if t1 < 150: # Likely bed temp result["bed_temp_min"] = t1 result["bed_temp_max"] = t2 return result async def main(db_path: str = None, dry_run: bool = True): """Main function to parse temps from comments.""" # Determine database URL if db_path: db_url = f"sqlite+aiosqlite:///{db_path}" else: # Default to local spoolman database default_path = Path.home() / ".local/share/spoolman/spoolman.db" if not default_path.exists(): # Try the project directory default_path = Path(__file__).parent.parent / "spoolman.db" db_url = f"sqlite+aiosqlite:///{default_path}" print(f"Connecting to: {db_url}") engine = create_async_engine(db_url, echo=False) async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with async_session() as session: # Get all filaments with comments result = await session.execute(select(Filament)) filaments = result.scalars().all() print(f"\nFound {len(filaments)} filaments total") updated_count = 0 for fil in filaments: if not fil.comment: continue parsed = parse_temps_from_comment(fil.comment) # Check if we found any temps has_extruder = parsed["extruder_temp_min"] is not None has_bed = parsed["bed_temp_min"] is not None if has_extruder or has_bed: print(f"\n--- Filament #{fil.id}: {fil.name or 'unnamed'} ---") print(f" Comment: {fil.comment}") print(f" Parsed extruder: {parsed['extruder_temp_min']}-{parsed['extruder_temp_max']} °C" if has_extruder else " No extruder temp found") print(f" Parsed bed: {parsed['bed_temp_min']}-{parsed['bed_temp_max']} °C" if has_bed else " No bed temp found") print(f" Current extruder: {fil.settings_extruder_temp_min}-{fil.settings_extruder_temp_max}" if fil.settings_extruder_temp_min else " Current extruder: Not set") print(f" Current bed: {fil.settings_bed_temp_min}-{fil.settings_bed_temp_max}" if fil.settings_bed_temp_min else " Current bed: Not set") if not dry_run: # Only update if not already set if has_extruder and not fil.settings_extruder_temp_min: fil.settings_extruder_temp_min = parsed["extruder_temp_min"] fil.settings_extruder_temp_max = parsed["extruder_temp_max"] if has_bed and not fil.settings_bed_temp_min: fil.settings_bed_temp_min = parsed["bed_temp_min"] fil.settings_bed_temp_max = parsed["bed_temp_max"] # Clear comment after parsing fil.comment = None print(" → Updated and cleared comment") updated_count += 1 if not dry_run and updated_count > 0: await session.commit() print(f"\n✓ Committed changes to {updated_count} filaments") elif dry_run: print(f"\n[DRY RUN] Would update {updated_count} filaments") print("Run with --apply to actually make changes") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Parse temps from filament comments") parser.add_argument("--db", help="Path to SQLite database file") parser.add_argument("--apply", action="store_true", help="Actually apply changes (default is dry run)") args = parser.parse_args() asyncio.run(main(db_path=args.db, dry_run=not args.apply))