Skip to content
3 changes: 3 additions & 0 deletions spoolman/api/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,9 @@ class SpoolEvent(Event):

payload: Spool = Field(description="Updated spool.")
resource: Literal["spool"] = Field(description="Resource type.")
payload_extras: dict[str, float] | None = \
Field(default=None,
description="Payload extra fields outside of core Spool model")


class FilamentEvent(Event):
Expand Down
10 changes: 6 additions & 4 deletions spoolman/database/spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.S
spool.last_used = datetime.utcnow().replace(microsecond=0)

await db.commit()
await spool_changed(spool, EventType.UPDATED)
await spool_changed(spool, EventType.UPDATED, {"weight_delta": weight})
return spool


Expand Down Expand Up @@ -344,7 +344,7 @@ async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.S
spool.last_used = datetime.utcnow().replace(microsecond=0)

await db.commit()
await spool_changed(spool, EventType.UPDATED)
await spool_changed(spool, EventType.UPDATED, {"weight_delta": weight})
return spool


Expand Down Expand Up @@ -436,16 +436,18 @@ async def find_lot_numbers(
return [row[0] for row in rows.all() if row[0] is not None]


async def spool_changed(spool: models.Spool, typ: EventType) -> None:
async def spool_changed(spool: models.Spool, typ: EventType, delta: Optional[dict] = None) -> None:
"""Notify websocket clients that a spool has changed."""
try:
spool = Spool.from_db(spool)
await websocket_manager.send(
("spool", str(spool.id)),
SpoolEvent(
type=typ,
resource="spool",
date=datetime.utcnow(),
payload=Spool.from_db(spool),
payload=spool,
payload_extras=delta
),
)
except Exception:
Expand Down
1 change: 1 addition & 0 deletions tests_integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pytest==8.3.2
pytest-asyncio==0.23.8
httpx==0.27.0
websockets==15.0.1
51 changes: 51 additions & 0 deletions tests_integration/tests/spool/test_spool_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Integration tests for spool websocket events."""

import asyncio
import json
from typing import Any

import httpx
import pytest
import websockets

from ..conftest import URL


@pytest.mark.asyncio
async def test_use_weight_websocket_has_weight_delta(random_filament: dict[str, Any]):
"""Test websocket payload extras for spool weight usage."""
#Setup
result = httpx.post(
f"{URL}/api/v1/spool",
json={"filament_id": random_filament["id"], "remaining_weight": 1000},
)
result.raise_for_status()
spool = result.json()
spool_id = spool["id"]
ws_url = URL.replace("http://", "ws://").replace("https://", "wss://") + f"/api/v1/spool/{spool_id}"
use_weight = 6.9

try:
async with websockets.connect(ws_url) as ws:
# keep the socket loop healthy before triggering update
await ws.send("ping")
check = json.loads(await asyncio.wait_for(ws.recv(), timeout=2))
assert check["status"] == "healthy"

#execute
r = httpx.put(f"{URL}/api/v1/spool/{spool_id}/use", json={"use_weight": use_weight})
r.raise_for_status()
raw = await asyncio.wait_for(ws.recv(), timeout=5)
evt = json.loads(raw)
#verify
assert evt["resource"] == "spool"
assert evt["type"] == "updated"
assert evt["payload"]["id"] == spool_id
assert evt["payload_extras"]["weight_delta"] == pytest.approx(use_weight)
assert "event_delta" not in evt["payload"].get("extra", {})
#cleanup-ws
await ws.close(code=1000)
await asyncio.sleep(0.6)
finally:
#cleanup
httpx.delete(f"{URL}/api/v1/spool/{spool_id}").raise_for_status()