Sync Bambuddy thumbnails to InvenTree parts
This commit is contained in:
@@ -7,6 +7,13 @@ from .http_errors import ExternalApiError
|
||||
from .models import Archive
|
||||
|
||||
|
||||
class DownloadedFile:
|
||||
def __init__(self, *, content: bytes, content_type: str, filename: str) -> None:
|
||||
self.content = content
|
||||
self.content_type = content_type
|
||||
self.filename = filename
|
||||
|
||||
|
||||
class BambuddyClient:
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
base_url = settings.bambuddy_base_url.rstrip("/")
|
||||
@@ -31,6 +38,22 @@ class BambuddyClient:
|
||||
async def get_system_info(self) -> dict[str, Any]:
|
||||
return await self._request("GET", "/system/info")
|
||||
|
||||
async def get_archive_thumbnail(self, archive_id: int) -> DownloadedFile | None:
|
||||
response = await self.client.get(f"/archives/{archive_id}/thumbnail")
|
||||
if response.status_code == 404:
|
||||
return None
|
||||
if response.status_code >= 400:
|
||||
body = response.text[:1000]
|
||||
raise ExternalApiError(f"Bambuddy GET /archives/{archive_id}/thumbnail failed: HTTP {response.status_code}: {body}")
|
||||
|
||||
content_type = response.headers.get("content-type", "image/png").split(";")[0]
|
||||
extension = "jpg" if content_type == "image/jpeg" else "png"
|
||||
return DownloadedFile(
|
||||
content=response.content,
|
||||
content_type=content_type,
|
||||
filename=f"bambuddy-archive-{archive_id}-thumbnail.{extension}",
|
||||
)
|
||||
|
||||
async def list_archives(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -21,6 +21,8 @@ class Settings(BaseSettings):
|
||||
webhook_shared_secret: str | None = None
|
||||
|
||||
sync_success_only: bool = True
|
||||
sync_part_images: bool = True
|
||||
overwrite_part_images: bool = False
|
||||
default_stock_quantity: Annotated[float, Field(gt=0)] = 1
|
||||
inventree_stock_status: int = 10
|
||||
part_ipn_prefix: str = "BMB"
|
||||
|
||||
@@ -21,7 +21,6 @@ class InvenTreeClient:
|
||||
headers={
|
||||
"Authorization": f"Token {settings.inventree_token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=settings.http_timeout_seconds,
|
||||
trust_env=False,
|
||||
@@ -36,6 +35,9 @@ class InvenTreeClient:
|
||||
async def get_stock_location(self) -> dict[str, Any]:
|
||||
return await self._request("GET", f"/stock/location/{self.settings.inventree_stock_location_id}/")
|
||||
|
||||
async def get_part(self, part_id: int) -> dict[str, Any]:
|
||||
return await self._request("GET", f"/part/{part_id}/")
|
||||
|
||||
async def find_part_by_ipn(self, ipn: str) -> dict[str, Any] | None:
|
||||
for params in ({"IPN": ipn, "limit": 100}, {"search": ipn, "limit": 100}):
|
||||
data = await self._request("GET", "/part/", params=params)
|
||||
@@ -79,6 +81,16 @@ class InvenTreeClient:
|
||||
}
|
||||
return await self._request("POST", "/stock/", json=payload)
|
||||
|
||||
async def upload_part_image(self, *, part_id: int, content: bytes, filename: str, content_type: str) -> dict[str, Any]:
|
||||
response = await self.client.patch(
|
||||
f"/part/{part_id}/",
|
||||
files={"image": (filename, content, content_type)},
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
body = response.text[:1000]
|
||||
raise ExternalApiError(f"InvenTree PATCH /part/{part_id}/ image failed: HTTP {response.status_code}: {body}")
|
||||
return response.json()
|
||||
|
||||
async def upsert_part_parameter(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -132,6 +132,7 @@ class ArchiveSyncService:
|
||||
|
||||
part_id = await self._get_or_create_part(part_key=part_key, ipn=ipn, name=name, description=description)
|
||||
await self._sync_part_parameters(part_id=part_id, archive=archive)
|
||||
await self._sync_part_image(part_id=part_id, archive=archive)
|
||||
batch = self.inventree.batch_for_archive(archive.id)
|
||||
existing_stock = await self.inventree.find_stock_by_batch(part_id=part_id, batch=batch)
|
||||
|
||||
@@ -206,6 +207,27 @@ class ArchiveSyncService:
|
||||
for parameter in self.part_parameters_for_archive(archive):
|
||||
await self.inventree.upsert_part_parameter(part_id=part_id, **parameter)
|
||||
|
||||
async def _sync_part_image(self, *, part_id: int, archive: Archive) -> None:
|
||||
if not self.settings.sync_part_images:
|
||||
return
|
||||
|
||||
if not self.settings.overwrite_part_images:
|
||||
part = await self.inventree.get_part(part_id)
|
||||
if part.get("image") or part.get("remote_image"):
|
||||
return
|
||||
|
||||
thumbnail = await self.bambuddy.get_archive_thumbnail(archive.id)
|
||||
if not thumbnail:
|
||||
return
|
||||
|
||||
await self.inventree.upload_part_image(
|
||||
part_id=part_id,
|
||||
content=thumbnail.content,
|
||||
filename=thumbnail.filename,
|
||||
content_type=thumbnail.content_type,
|
||||
)
|
||||
logger.info("Synced Bambuddy archive %s thumbnail to InvenTree part %s", archive.id, part_id)
|
||||
|
||||
def part_parameters_for_archive(self, archive: Archive) -> list[dict[str, str | float | None]]:
|
||||
parameters: list[dict[str, str | float | None]] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user