Added/deleted-type websocket messages

Events are now sent for not only when an object changes but also when one is created or deleted.

For now it's not really possible to subscribe to these events however.

This breaks backwards-compatibility with the websocket messages.
This commit is contained in:
Donkie
2023-10-15 19:46:10 +02:00
parent 3c5b3cea5a
commit a6d527aaf0
6 changed files with 92 additions and 25 deletions

View File

@@ -58,6 +58,7 @@ ignore = [
"D406", "D406",
"D407", "D407",
"FA100", "FA100",
"FIX002",
"S104", "S104",
"TRY201", "TRY201",
"TRY003", "TRY003",
@@ -66,6 +67,8 @@ ignore = [
"DTZ003", "DTZ003",
"PLR0913", "PLR0913",
"SIM108", "SIM108",
"TD002",
"TD003",
] ]
line-length = 120 line-length = 120
target-version = "py39" target-version = "py39"

View File

@@ -1,6 +1,7 @@
"""Pydantic data models for typing the FastAPI request/responses.""" """Pydantic data models for typing the FastAPI request/responses."""
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum
from typing import Optional from typing import Optional
from pydantic import BaseModel as PydanticBaseModel from pydantic import BaseModel as PydanticBaseModel
@@ -228,3 +229,37 @@ class BackupResponse(BaseModel):
description="Path to the created backup file.", description="Path to the created backup file.",
example="/home/app/.local/share/spoolman/backups/spoolman.db", example="/home/app/.local/share/spoolman/backups/spoolman.db",
) )
class EventType(str, Enum):
"""Event types."""
ADDED = "added"
UPDATED = "updated"
DELETED = "deleted"
class Event(BaseModel):
"""Event."""
type: EventType = Field(description="Event type.")
date: datetime = Field(description="When the event occured. UTC Timezone.")
payload: BaseModel
class SpoolEvent(Event):
"""Event."""
payload: Spool = Field(description="Updated spool.")
class FilamentEvent(Event):
"""Event."""
payload: Filament = Field(description="Updated filament.")
class VendorEvent(Event):
"""Event."""
payload: Vendor = Field(description="Updated vendor.")

View File

@@ -10,7 +10,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import contains_eager, joinedload from sqlalchemy.orm import contains_eager, joinedload
from spoolman.api.v1.models import Filament from spoolman.api.v1.models import EventType, Filament, FilamentEvent
from spoolman.database import models, vendor from spoolman.database import models, vendor
from spoolman.database.utils import ( from spoolman.database.utils import (
SortOrder, SortOrder,
@@ -45,7 +45,7 @@ async def create(
if vendor_id is not None: if vendor_id is not None:
vendor_item = await vendor.get_by_id(db, vendor_id) vendor_item = await vendor.get_by_id(db, vendor_id)
db_item = models.Filament( filament = models.Filament(
name=name, name=name,
registered=datetime.utcnow().replace(microsecond=0), registered=datetime.utcnow().replace(microsecond=0),
vendor=vendor_item, vendor=vendor_item,
@@ -61,9 +61,10 @@ async def create(
settings_bed_temp=settings_bed_temp, settings_bed_temp=settings_bed_temp,
color_hex=color_hex, color_hex=color_hex,
) )
db.add(db_item) db.add(filament)
await db.commit() await db.commit()
return db_item await filament_changed(filament, EventType.ADDED)
return filament
async def get_by_id(db: AsyncSession, filament_id: int) -> models.Filament: async def get_by_id(db: AsyncSession, filament_id: int) -> models.Filament:
@@ -150,7 +151,7 @@ async def update(
else: else:
setattr(filament, k, v) setattr(filament, k, v)
await db.commit() await db.commit()
await filament_changed(filament) await filament_changed(filament, EventType.UPDATED)
return filament return filament
@@ -160,6 +161,7 @@ async def delete(db: AsyncSession, filament_id: int) -> None:
await db.delete(filament) await db.delete(filament)
try: try:
await db.commit() # Flush immediately so any errors are propagated in this request. await db.commit() # Flush immediately so any errors are propagated in this request.
await filament_changed(filament, EventType.DELETED)
except IntegrityError as exc: except IntegrityError as exc:
await db.rollback() await db.rollback()
raise ItemDeleteError("Failed to delete filament.") from exc raise ItemDeleteError("Failed to delete filament.") from exc
@@ -188,6 +190,13 @@ async def find_article_numbers(
return [row[0] for row in rows.all() if row[0] is not None] return [row[0] for row in rows.all() if row[0] is not None]
async def filament_changed(filament: models.Filament) -> None: async def filament_changed(filament: models.Filament, typ: EventType) -> None:
"""Notify websocket clients that a filament has changed.""" """Notify websocket clients that a filament has changed."""
await websocket_manager.send(("filament", str(filament.id)), Filament.from_db(filament).json()) await websocket_manager.send(
("filament", str(filament.id)),
FilamentEvent(
type=typ,
date=datetime.utcnow(),
payload=Filament.from_db(filament),
),
)

View File

@@ -10,7 +10,7 @@ from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import contains_eager, joinedload from sqlalchemy.orm import contains_eager, joinedload
from spoolman.api.v1.models import Spool from spoolman.api.v1.models import EventType, Spool, SpoolEvent
from spoolman.database import filament, models from spoolman.database import filament, models
from spoolman.database.utils import ( from spoolman.database.utils import (
SortOrder, SortOrder,
@@ -59,7 +59,7 @@ async def create(
if last_used is not None: if last_used is not None:
last_used = utc_timezone_naive(last_used) last_used = utc_timezone_naive(last_used)
db_item = models.Spool( spool = models.Spool(
filament=filament_item, filament=filament_item,
registered=datetime.utcnow().replace(microsecond=0), registered=datetime.utcnow().replace(microsecond=0),
used_weight=used_weight, used_weight=used_weight,
@@ -70,9 +70,10 @@ async def create(
comment=comment, comment=comment,
archived=archived, archived=archived,
) )
db.add(db_item) db.add(spool)
await db.commit() await db.commit()
return db_item await spool_changed(spool, EventType.ADDED)
return spool
async def get_by_id(db: AsyncSession, spool_id: int) -> models.Spool: async def get_by_id(db: AsyncSession, spool_id: int) -> models.Spool:
@@ -182,7 +183,7 @@ async def update(
else: else:
setattr(spool, k, v) setattr(spool, k, v)
await db.commit() await db.commit()
await spool_changed(spool) await spool_changed(spool, EventType.UPDATED)
return spool return spool
@@ -190,6 +191,7 @@ async def delete(db: AsyncSession, spool_id: int) -> None:
"""Delete a spool object.""" """Delete a spool object."""
spool = await get_by_id(db, spool_id) spool = await get_by_id(db, spool_id)
await db.delete(spool) await db.delete(spool)
await spool_changed(spool, EventType.DELETED)
async def use_weight_safe(db: AsyncSession, spool_id: int, weight: float) -> None: async def use_weight_safe(db: AsyncSession, spool_id: int, weight: float) -> None:
@@ -235,7 +237,7 @@ async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.S
spool.last_used = datetime.utcnow().replace(microsecond=0) spool.last_used = datetime.utcnow().replace(microsecond=0)
await db.commit() await db.commit()
await spool_changed(spool) await spool_changed(spool, EventType.UPDATED)
return spool return spool
@@ -280,7 +282,7 @@ async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.S
spool.last_used = datetime.utcnow().replace(microsecond=0) spool.last_used = datetime.utcnow().replace(microsecond=0)
await db.commit() await db.commit()
await spool_changed(spool) await spool_changed(spool, EventType.UPDATED)
return spool return spool
@@ -304,6 +306,13 @@ async def find_lot_numbers(
return [row[0] for row in rows.all() if row[0] is not None] return [row[0] for row in rows.all() if row[0] is not None]
async def spool_changed(spool: models.Spool) -> None: async def spool_changed(spool: models.Spool, typ: EventType) -> None:
"""Notify websocket clients that a spool has changed.""" """Notify websocket clients that a spool has changed."""
await websocket_manager.send(("spool", str(spool.id)), Spool.from_db(spool).json()) await websocket_manager.send(
("spool", str(spool.id)),
SpoolEvent(
type=typ,
date=datetime.utcnow(),
payload=Spool.from_db(spool),
),
)

View File

@@ -6,7 +6,7 @@ from typing import Optional
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.api.v1.models import Vendor from spoolman.api.v1.models import EventType, Vendor, VendorEvent
from spoolman.database import models from spoolman.database import models
from spoolman.database.utils import SortOrder, add_where_clause_str from spoolman.database.utils import SortOrder, add_where_clause_str
from spoolman.exceptions import ItemNotFoundError from spoolman.exceptions import ItemNotFoundError
@@ -20,14 +20,15 @@ async def create(
comment: Optional[str] = None, comment: Optional[str] = None,
) -> models.Vendor: ) -> models.Vendor:
"""Add a new vendor to the database.""" """Add a new vendor to the database."""
db_item = models.Vendor( vendor = models.Vendor(
name=name, name=name,
registered=datetime.utcnow().replace(microsecond=0), registered=datetime.utcnow().replace(microsecond=0),
comment=comment, comment=comment,
) )
db.add(db_item) db.add(vendor)
await db.commit() await db.commit()
return db_item await vendor_changed(vendor, EventType.ADDED)
return vendor
async def get_by_id(db: AsyncSession, vendor_id: int) -> models.Vendor: async def get_by_id(db: AsyncSession, vendor_id: int) -> models.Vendor:
@@ -89,7 +90,7 @@ async def update(
for k, v in data.items(): for k, v in data.items():
setattr(vendor, k, v) setattr(vendor, k, v)
await db.commit() await db.commit()
await vendor_changed(vendor) await vendor_changed(vendor, EventType.UPDATED)
return vendor return vendor
@@ -97,8 +98,16 @@ async def delete(db: AsyncSession, vendor_id: int) -> None:
"""Delete a vendor object.""" """Delete a vendor object."""
vendor = await get_by_id(db, vendor_id) vendor = await get_by_id(db, vendor_id)
await db.delete(vendor) await db.delete(vendor)
await vendor_changed(vendor, EventType.DELETED)
async def vendor_changed(vendor: models.Vendor) -> None: async def vendor_changed(vendor: models.Vendor, typ: EventType) -> None:
"""Notify websocket clients that a vendor has changed.""" """Notify websocket clients that a vendor has changed."""
await websocket_manager.send(("vendor", str(vendor.id)), Vendor.from_db(vendor).json()) await websocket_manager.send(
("vendor", str(vendor.id)),
VendorEvent(
type=typ,
date=datetime.utcnow(),
payload=Vendor.from_db(vendor),
),
)

View File

@@ -4,6 +4,8 @@ import logging
from fastapi import WebSocket from fastapi import WebSocket
from spoolman.api.v1.models import Event
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -37,12 +39,12 @@ class WebsocketManager:
pool_str, pool_str,
) )
async def send(self, pool: tuple[str, ...], message: str) -> None: async def send(self, pool: tuple[str, ...], evt: Event) -> None:
"""Send a message to all websockets in a pool.""" """Send a message to all websockets in a pool."""
pool_str = ",".join(pool) pool_str = ",".join(pool)
if pool_str in self.connections: if pool_str in self.connections:
for websocket in self.connections[pool_str]: for websocket in self.connections[pool_str]:
await websocket.send_text(message) await websocket.send_text(evt.json())
websocket_manager = WebsocketManager() websocket_manager = WebsocketManager()