Renamed to Spoolman

Spoolson just didn't sound that good after all
This commit is contained in:
Donkie
2023-04-02 19:19:15 +02:00
parent c31c3dea42
commit 6fd9662c42
19 changed files with 24 additions and 24 deletions

View File

View File

@@ -0,0 +1,54 @@
"""SQLAlchemy database setup."""
from collections.abc import AsyncGenerator
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from spoolman.database.models import Base
class Database:
connection_url: str
engine: AsyncEngine
session_maker: async_sessionmaker[AsyncSession]
def __init__(self: "Database", connection_url: str) -> None:
"""Construct the Database wrapper and set config parameters."""
self.connection_url = connection_url
def connect(self: "Database") -> None:
"""Connect to the database."""
self.engine = create_async_engine(self.connection_url, connect_args={"check_same_thread": False})
self.session_maker = async_sessionmaker(self.engine, autocommit=False, autoflush=True)
async def create_tables(self: "Database") -> None:
"""Create tables for all defined models."""
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
__db: Optional[Database] = None
async def setup_db(connection_url: str) -> None:
"""Connect the singleton DB object."""
global __db # noqa: PLW0603
__db = Database(connection_url)
__db.connect()
await __db.create_tables()
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
"""Get a DB session to be used with FastAPI's dependency system."""
if __db is None:
raise RuntimeError("DB is not setup.")
async with __db.session_maker() as session:
try:
yield session
await session.commit()
except Exception as exc:
await session.rollback()
raise exc
finally:
await session.close()

View File

@@ -0,0 +1,78 @@
"""Helper functions for interacting with filament database objects."""
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.database import models, vendor
from spoolman.exceptions import ItemNotFoundError
async def create(
*,
db: AsyncSession,
density: float,
diameter: float,
name: Optional[str] = None,
vendor_id: Optional[int] = None,
material: Optional[str] = None,
price: Optional[float] = None,
weight: Optional[float] = None,
spool_weight: Optional[float] = None,
article_number: Optional[str] = None,
comment: Optional[str] = None,
) -> models.Filament:
"""Add a new filament to the database."""
vendor_item: Optional[models.Vendor] = None
if vendor_id is not None:
vendor_item = await vendor.get_by_id(db, vendor_id)
db_item = models.Filament(
name=name,
vendor=vendor_item,
material=material,
price=price,
density=density,
diameter=diameter,
weight=weight,
spool_weight=spool_weight,
article_number=article_number,
comment=comment,
)
db.add(db_item)
await db.flush()
return db_item
async def get_by_id(db: AsyncSession, filament_id: int) -> models.Filament:
"""Get a filament object from the database by the unique ID."""
filament = await db.get(models.Filament, filament_id)
if filament is None:
raise ItemNotFoundError(f"No filament with ID {filament_id} found.")
return filament
async def update(
*,
db: AsyncSession,
filament_id: int,
data: dict,
) -> models.Filament:
"""Update the fields of a filament object."""
filament = await get_by_id(db, filament_id)
for k, v in data.items():
if k == "vendor_id":
if v is None:
filament.vendor = None
else:
filament.vendor = await vendor.get_by_id(db, v)
else:
setattr(filament, k, v)
await db.flush()
return filament
async def delete(db: AsyncSession, filament_id: int) -> None:
"""Delete a filament object."""
filament = await get_by_id(db, filament_id)
await db.delete(filament)

View File

@@ -0,0 +1,56 @@
"""SQLAlchemy data models."""
from datetime import datetime
from typing import Optional
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.sql import func
class Base(DeclarativeBase):
pass
class Vendor(Base):
__tablename__ = "vendor"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String(64))
comment: Mapped[Optional[str]] = mapped_column(String(1024))
filaments: Mapped[list["Filament"]] = relationship(back_populates="vendor")
class Filament(Base):
__tablename__ = "filament"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
name: Mapped[Optional[str]] = mapped_column(String(64))
vendor_id: Mapped[Optional[int]] = mapped_column(ForeignKey("vendor.id"))
vendor: Mapped[Optional["Vendor"]] = relationship(back_populates="filaments")
spools: Mapped[list["Spool"]] = relationship(back_populates="filament")
material: Mapped[Optional[str]] = mapped_column(String(64))
price: Mapped[Optional[float]] = mapped_column()
density: Mapped[float] = mapped_column()
diameter: Mapped[float] = mapped_column()
weight: Mapped[Optional[float]] = mapped_column(comment="The filament weight of a full spool (net weight).")
spool_weight: Mapped[Optional[float]] = mapped_column(comment="The weight of an empty spool.")
article_number: Mapped[Optional[str]] = mapped_column(String(64))
comment: Mapped[Optional[str]] = mapped_column(String(1024))
# TODO: Print settings
# TODO: Color?
class Spool(Base):
__tablename__ = "spool"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
registered: Mapped[datetime] = mapped_column(default=func.now())
first_used: Mapped[Optional[datetime]] = mapped_column()
last_used: Mapped[Optional[datetime]] = mapped_column()
filament_id: Mapped[int] = mapped_column(ForeignKey("filament.id"))
filament: Mapped["Filament"] = relationship(back_populates="spools")
weight: Mapped[float] = mapped_column()
location: Mapped[Optional[str]] = mapped_column(String(64))
lot_nr: Mapped[Optional[str]] = mapped_column(String(64))
comment: Mapped[Optional[str]] = mapped_column(String(1024))

120
spoolman/database/spool.py Normal file
View File

@@ -0,0 +1,120 @@
"""Helper functions for interacting with spool database objects."""
from datetime import datetime
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.database import filament, models
from spoolman.exceptions import ItemCreateError, ItemNotFoundError
from spoolman.math import weight_from_length
async def create(
*,
db: AsyncSession,
filament_id: int,
weight: Optional[float] = None,
first_used: Optional[datetime] = None,
last_used: Optional[datetime] = None,
location: Optional[str] = None,
lot_nr: Optional[str] = None,
comment: Optional[str] = None,
) -> models.Spool:
"""Add a new spool to the database. Leave weight empty to assume full spool."""
filament_item = await filament.get_by_id(db, filament_id)
if weight is None:
if filament_item.weight is None:
raise ItemCreateError("The weight for neither the spool nor its filament type is set.")
weight = filament_item.weight
db_item = models.Spool(
filament=filament_item,
weight=weight,
first_used=first_used,
last_used=last_used,
location=location,
lot_nr=lot_nr,
comment=comment,
)
db.add(db_item)
await db.flush()
return db_item
async def get_by_id(db: AsyncSession, spool_id: int, with_for_update: Optional[bool] = None) -> models.Spool:
"""Get a spool object from the database by the unique ID."""
spool = await db.get(models.Spool, spool_id, with_for_update=with_for_update) # type: ignore # noqa: PGH003
if spool is None:
raise ItemNotFoundError(f"No spool with ID {spool_id} found.")
return spool
async def update(
*,
db: AsyncSession,
spool_id: int,
data: dict,
) -> models.Spool:
"""Update the fields of a spool object."""
spool = await get_by_id(db, spool_id)
for k, v in data.items():
if k == "filament_id":
spool.filament = await filament.get_by_id(db, v)
else:
setattr(spool, k, v)
await db.flush()
return spool
async def delete(db: AsyncSession, spool_id: int) -> None:
"""Delete a spool object."""
spool = await get_by_id(db, spool_id)
await db.delete(spool)
# TODO: Make unit tests for race conditions on these
async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.Spool:
"""Reduce the weight of a spool.
Does nothing if the spool is empty.
Args:
db (AsyncSession): Database session
spool_id (int): Spool ID
weight (float): Weight loss in grams
Returns:
models.Spool: Updated spool object
"""
spool = await get_by_id(db, spool_id, with_for_update=True)
spool.weight -= min(spool.weight, weight)
await db.flush()
return spool
async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.Spool:
"""Reduce the weight of a spool by using a length of filament.
Does nothing if the spool is empty.
Args:
db (AsyncSession): Database session
spool_id (int): Spool ID
length (float): Length of filament to reduce by
Returns:
models.Spool: Updated spool object
"""
spool = await get_by_id(db, spool_id, with_for_update=True)
filament = spool.filament
weight = weight_from_length(
length=length,
radius=filament.diameter / 2,
density=filament.density,
)
spool.weight -= min(spool.weight, weight)
await db.flush()
return spool

View File

@@ -0,0 +1,52 @@
"""Helper functions for interacting with vendor database objects."""
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.database import models
from spoolman.exceptions import ItemNotFoundError
async def create(
*,
db: AsyncSession,
name: Optional[str] = None,
comment: Optional[str] = None,
) -> models.Vendor:
"""Add a new vendor to the database."""
db_item = models.Vendor(
name=name,
comment=comment,
)
db.add(db_item)
await db.flush()
return db_item
async def get_by_id(db: AsyncSession, vendor_id: int) -> models.Vendor:
"""Get a vendor object from the database by the unique ID."""
vendor = await db.get(models.Vendor, vendor_id)
if vendor is None:
raise ItemNotFoundError(f"No vendor with ID {vendor_id} found.")
return vendor
async def update(
*,
db: AsyncSession,
vendor_id: int,
data: dict,
) -> models.Vendor:
"""Update the fields of a vendor object."""
vendor = await get_by_id(db, vendor_id)
for k, v in data.items():
setattr(vendor, k, v)
await db.flush()
return vendor
async def delete(db: AsyncSession, vendor_id: int) -> None:
"""Delete a vendor object."""
vendor = await get_by_id(db, vendor_id)
await db.delete(vendor)