local_presets.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. """API routes for local slicer presets (imported from OrcaSlicer, etc.)."""
  2. import json
  3. import logging
  4. from fastapi import APIRouter, Depends, HTTPException, UploadFile
  5. from sqlalchemy import select
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from backend.app.core.auth import RequirePermissionIfAuthEnabled
  8. from backend.app.core.database import get_db
  9. from backend.app.core.permissions import Permission
  10. from backend.app.models.local_preset import LocalPreset
  11. from backend.app.models.user import User
  12. from backend.app.schemas.local_preset import (
  13. ImportResponse,
  14. LocalPresetCreate,
  15. LocalPresetDetail,
  16. LocalPresetResponse,
  17. LocalPresetsResponse,
  18. LocalPresetUpdate,
  19. )
  20. from backend.app.services.orca_profiles import (
  21. extract_core_fields,
  22. get_cache_status,
  23. import_orca_file,
  24. reclassify_presets,
  25. refresh_base_cache,
  26. resolve_preset,
  27. )
  28. logger = logging.getLogger(__name__)
  29. router = APIRouter(prefix="/local-presets", tags=["Local Presets"])
  30. @router.get("/", response_model=LocalPresetsResponse)
  31. async def list_local_presets(
  32. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
  33. db: AsyncSession = Depends(get_db),
  34. ):
  35. """List all local presets grouped by type."""
  36. result = await db.execute(select(LocalPreset).order_by(LocalPreset.name))
  37. presets = result.scalars().all()
  38. grouped = LocalPresetsResponse()
  39. for p in presets:
  40. resp = LocalPresetResponse.model_validate(p)
  41. if p.preset_type == "filament":
  42. grouped.filament.append(resp)
  43. elif p.preset_type == "printer":
  44. grouped.printer.append(resp)
  45. elif p.preset_type == "process":
  46. grouped.process.append(resp)
  47. return grouped
  48. @router.get("/{preset_id}", response_model=LocalPresetDetail)
  49. async def get_local_preset(
  50. preset_id: int,
  51. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
  52. db: AsyncSession = Depends(get_db),
  53. ):
  54. """Get full detail for a local preset including the setting JSON."""
  55. result = await db.execute(select(LocalPreset).where(LocalPreset.id == preset_id))
  56. preset = result.scalar_one_or_none()
  57. if not preset:
  58. raise HTTPException(404, "Local preset not found")
  59. data = LocalPresetResponse.model_validate(preset).model_dump()
  60. try:
  61. data["setting"] = json.loads(preset.setting)
  62. except Exception:
  63. data["setting"] = {}
  64. return LocalPresetDetail(**data)
  65. @router.post("/import", response_model=ImportResponse)
  66. async def import_presets(
  67. file: UploadFile,
  68. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  69. db: AsyncSession = Depends(get_db),
  70. ):
  71. """Import presets from an OrcaSlicer export file (.json, .orca_filament, .bbscfg, .bbsflmt, .zip)."""
  72. if not file.filename:
  73. raise HTTPException(400, "No filename provided")
  74. content = await file.read()
  75. if not content:
  76. raise HTTPException(400, "Empty file")
  77. result = await import_orca_file(file.filename, content, db)
  78. return ImportResponse(**result)
  79. @router.post("/", response_model=LocalPresetResponse)
  80. async def create_local_preset(
  81. data: LocalPresetCreate,
  82. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  83. db: AsyncSession = Depends(get_db),
  84. ):
  85. """Manually create a local preset."""
  86. if data.preset_type not in ("filament", "printer", "process"):
  87. raise HTTPException(400, "preset_type must be filament, printer, or process")
  88. # Extract core fields
  89. core = extract_core_fields(data.setting)
  90. preset = LocalPreset(
  91. name=data.name,
  92. preset_type=data.preset_type,
  93. source="manual",
  94. setting=json.dumps(data.setting),
  95. **core,
  96. )
  97. db.add(preset)
  98. await db.flush()
  99. await db.refresh(preset)
  100. return LocalPresetResponse.model_validate(preset)
  101. @router.put("/{preset_id}", response_model=LocalPresetResponse)
  102. async def update_local_preset(
  103. preset_id: int,
  104. data: LocalPresetUpdate,
  105. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  106. db: AsyncSession = Depends(get_db),
  107. ):
  108. """Update a local preset's name or settings."""
  109. result = await db.execute(select(LocalPreset).where(LocalPreset.id == preset_id))
  110. preset = result.scalar_one_or_none()
  111. if not preset:
  112. raise HTTPException(404, "Local preset not found")
  113. if data.name is not None:
  114. preset.name = data.name
  115. if data.setting is not None:
  116. # Re-resolve and extract core fields
  117. resolved = await resolve_preset(data.setting, preset.preset_type, db)
  118. core = extract_core_fields(resolved)
  119. preset.setting = json.dumps(resolved)
  120. preset.filament_type = core.get("filament_type")
  121. preset.filament_vendor = core.get("filament_vendor")
  122. preset.nozzle_temp_min = core.get("nozzle_temp_min")
  123. preset.nozzle_temp_max = core.get("nozzle_temp_max")
  124. preset.pressure_advance = core.get("pressure_advance")
  125. preset.default_filament_colour = core.get("default_filament_colour")
  126. preset.filament_cost = core.get("filament_cost")
  127. preset.filament_density = core.get("filament_density")
  128. preset.compatible_printers = core.get("compatible_printers")
  129. await db.flush()
  130. await db.refresh(preset)
  131. return LocalPresetResponse.model_validate(preset)
  132. @router.delete("/{preset_id}")
  133. async def delete_local_preset(
  134. preset_id: int,
  135. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  136. db: AsyncSession = Depends(get_db),
  137. ):
  138. """Delete a local preset."""
  139. result = await db.execute(select(LocalPreset).where(LocalPreset.id == preset_id))
  140. preset = result.scalar_one_or_none()
  141. if not preset:
  142. raise HTTPException(404, "Local preset not found")
  143. await db.delete(preset)
  144. return {"success": True}
  145. @router.get("/base-cache/status")
  146. async def base_cache_status(
  147. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
  148. db: AsyncSession = Depends(get_db),
  149. ):
  150. """Get the status of the OrcaSlicer base profile cache."""
  151. return await get_cache_status(db)
  152. @router.post("/base-cache/refresh")
  153. async def refresh_cache(
  154. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  155. db: AsyncSession = Depends(get_db),
  156. ):
  157. """Force refresh all cached base profiles from GitHub."""
  158. return await refresh_base_cache(db)
  159. @router.post("/reclassify")
  160. async def reclassify(
  161. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  162. db: AsyncSession = Depends(get_db),
  163. ):
  164. """Re-evaluate preset types for all local presets using the improved heuristic."""
  165. return await reclassify_presets(db)