Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added data/spoolman.db
Binary file not shown.
1,614 changes: 1,614 additions & 0 deletions pdm.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion spoolman/api/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from datetime import datetime, timezone
from enum import Enum
from typing import TYPE_CHECKING, Annotated, Literal
from typing import TYPE_CHECKING, Annotated, Any, Literal

from pydantic import BaseModel, Field, PlainSerializer

Expand Down Expand Up @@ -623,6 +623,10 @@ class SpoolEvent(Event):

payload: Spool = Field(description="Updated spool.")
resource: Literal["spool"] = Field(description="Resource type.")
payload_extras: dict[str, Any] | None = Field(
Comment thread
chof747 marked this conversation as resolved.
Outdated
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 @@ -585,7 +585,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 @@ -631,7 +631,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 @@ -730,16 +730,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: dict | None = 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
2 changes: 1 addition & 1 deletion tests_integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytest==9.1.1
pytest-asyncio==1.4.0
httpx==0.27.0
websockets==13.1
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 websocket.
await ws.close(code=1000)
await asyncio.sleep(0.6)
finally:
# Cleanup.
httpx.delete(f"{URL}/api/v1/spool/{spool_id}").raise_for_status()
Loading