feat(#19): Add filamentcolors.xyz color lookup integration
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled

- Backend proxy endpoints: GET /filamentcolors/manufacturers, GET /filamentcolors/search
- Frontend: FilamentColorLookup modal with manufacturer filter + color search
- Color grid shows swatches with hex codes, click to apply
- Added to both filament create and edit forms next to ColorPicker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 00:21:13 -06:00
parent c321d916b5
commit 23ef41270c
5 changed files with 310 additions and 26 deletions

View File

@@ -4,6 +4,7 @@ import logging
from datetime import datetime, timedelta
from typing import Annotated, Optional
import httpx
import sqlalchemy
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field, RootModel
@@ -371,3 +372,104 @@ async def get_cost_stats(
avg_cost_per_kg=avg_cost_per_kg,
cost_by_material=cost_by_material,
)
# --- filamentcolors.xyz integration ---
FILAMENTCOLORS_API = "https://filamentcolors.xyz/api"
class FilamentColorSwatch(BaseModel):
"""A color swatch from filamentcolors.xyz."""
id: int
color_name: str
hex_color: Optional[str] = None
manufacturer_name: str
filament_type: str
image_front: Optional[str] = None
class FilamentColorManufacturer(BaseModel):
"""A manufacturer from filamentcolors.xyz."""
id: int
name: str
class FilamentColorSearchResult(BaseModel):
"""Search results from filamentcolors.xyz."""
count: int
swatches: list[FilamentColorSwatch]
@router.get(
"/filamentcolors/manufacturers",
name="Get filamentcolors.xyz manufacturers",
description="List all manufacturers from filamentcolors.xyz.",
response_model=list[FilamentColorManufacturer],
)
async def get_filamentcolors_manufacturers() -> list[FilamentColorManufacturer]:
"""Proxy manufacturer list from filamentcolors.xyz."""
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{FILAMENTCOLORS_API}/manufacturer/", params={"format": "json", "limit": 500})
resp.raise_for_status()
data = resp.json()
return [
FilamentColorManufacturer(id=m["id"], name=m["name"])
for m in data.get("results", [])
]
@router.get(
"/filamentcolors/search",
name="Search filamentcolors.xyz swatches",
description="Search color swatches from filamentcolors.xyz by manufacturer and/or color name.",
response_model=FilamentColorSearchResult,
)
async def search_filamentcolors(
*,
manufacturer: Annotated[
Optional[str],
Query(description="Manufacturer name to filter by."),
] = None,
color: Annotated[
Optional[str],
Query(description="Color name to search for."),
] = None,
filament_type: Annotated[
Optional[str],
Query(description="Filament type to filter by (e.g., PLA, PETG)."),
] = None,
limit: Annotated[int, Query(ge=1, le=100)] = 50,
page: Annotated[int, Query(ge=1)] = 1,
) -> FilamentColorSearchResult:
"""Proxy swatch search from filamentcolors.xyz."""
params: dict = {"format": "json", "limit": limit, "page": page}
if manufacturer:
params["manufacturer__name"] = manufacturer
if color:
params["color_name__icontains"] = color
if filament_type:
params["filament_type__name__icontains"] = filament_type
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{FILAMENTCOLORS_API}/swatch/", params=params)
resp.raise_for_status()
data = resp.json()
swatches = [
FilamentColorSwatch(
id=s["id"],
color_name=s.get("color_name", ""),
hex_color=s.get("hex_color"),
manufacturer_name=s.get("manufacturer", {}).get("name", ""),
filament_type=s.get("filament_type", {}).get("name", ""),
image_front=s.get("image_front"),
)
for s in data.get("results", [])
]
return FilamentColorSearchResult(count=data.get("count", 0), swatches=swatches)