Spool weight now counts upwards instead
The used filament weight is now kept track of instead of remaining weight. This adds robustness to measurement errors which could accumulate and then the filament usage wouldn't get tracked anymore once the weight hit 0. Now it will just keep going.
This commit is contained in:
@@ -91,7 +91,15 @@ class Spool(BaseModel):
|
||||
first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.")
|
||||
last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage.")
|
||||
filament: Filament = Field(description="The filament type of this spool.")
|
||||
weight: float = Field(ge=0, description="Remaining weight of filament on the spool.", example=500)
|
||||
remaining_weight: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description=(
|
||||
"Estimated remaining weight of filament on the spool. Only set if the filament type has a weight set."
|
||||
),
|
||||
example=500,
|
||||
)
|
||||
used_weight: float = Field(ge=0, description="Consumed weight of filament from the spool.", example=500)
|
||||
location: Optional[str] = Field(max_length=64, description="Where this spool can be found.", example="Shelf A")
|
||||
lot_nr: Optional[str] = Field(
|
||||
max_length=64,
|
||||
@@ -107,13 +115,19 @@ class Spool(BaseModel):
|
||||
@staticmethod
|
||||
def from_db(item: models.Spool) -> "Spool":
|
||||
"""Create a new Pydantic spool object from a database spool object."""
|
||||
filament = Filament.from_db(item.filament)
|
||||
remaining_weight: Optional[float] = None
|
||||
if filament.weight is not None:
|
||||
remaining_weight = max(filament.weight - item.used_weight, 0)
|
||||
|
||||
return Spool(
|
||||
id=item.id,
|
||||
registered=item.registered,
|
||||
first_used=item.first_used,
|
||||
last_used=item.last_used,
|
||||
filament=Filament.from_db(item.filament),
|
||||
weight=item.weight,
|
||||
filament=filament,
|
||||
used_weight=item.used_weight,
|
||||
remaining_weight=remaining_weight,
|
||||
location=item.location,
|
||||
lot_nr=item.lot_nr,
|
||||
comment=item.comment,
|
||||
|
||||
@@ -27,14 +27,14 @@ class SpoolParameters(BaseModel):
|
||||
first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.")
|
||||
last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage.")
|
||||
filament_id: int = Field(description="The ID of the filament type of this spool.")
|
||||
weight: Optional[float] = Field(
|
||||
remaining_weight: Optional[float] = Field(
|
||||
ge=0,
|
||||
description=(
|
||||
"Remaining weight of filament on the spool. "
|
||||
"Leave empty to assume a full spool based on the weight parameters of the filament type."
|
||||
"Remaining weight of filament on the spool. Can only be used if the filament type has a weight set."
|
||||
),
|
||||
example=500,
|
||||
example=800,
|
||||
)
|
||||
used_weight: Optional[float] = Field(ge=0, description="Used weight of filament on the spool.", example=200)
|
||||
location: Optional[str] = Field(max_length=64, description="Where this spool can be found.", example="Shelf A")
|
||||
lot_nr: Optional[str] = Field(
|
||||
max_length=64,
|
||||
@@ -132,7 +132,11 @@ async def get(
|
||||
@router.post(
|
||||
"/",
|
||||
name="Add spool",
|
||||
description="Add a new spool to the database.",
|
||||
description=(
|
||||
"Add a new spool to the database. "
|
||||
"Only specify either remaining_weight or used_weight. "
|
||||
"If no weight is set, the spool will be assumed to be full."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
response_model=Spool,
|
||||
responses={
|
||||
@@ -143,11 +147,18 @@ async def create( # noqa: ANN201
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
body: SpoolParameters,
|
||||
):
|
||||
if body.remaining_weight is not None and body.used_weight is not None:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"message": "Only specify either remaining_weight or used_weight."},
|
||||
)
|
||||
|
||||
try:
|
||||
db_item = await spool.create(
|
||||
db=db,
|
||||
filament_id=body.filament_id,
|
||||
weight=body.weight,
|
||||
remaining_weight=body.remaining_weight,
|
||||
used_weight=body.used_weight,
|
||||
first_used=body.first_used,
|
||||
last_used=body.last_used,
|
||||
location=body.location,
|
||||
@@ -165,26 +176,46 @@ async def create( # noqa: ANN201
|
||||
@router.patch(
|
||||
"/{spool_id}",
|
||||
name="Update spool",
|
||||
description="Update any attribute of a spool. Only fields specified in the request will be affected.",
|
||||
description=(
|
||||
"Update any attribute of a spool. "
|
||||
"Only fields specified in the request will be affected. "
|
||||
"remaining_weight and used_weight can't be set at the same time."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
response_model=Spool,
|
||||
responses={
|
||||
400: {"model": Message},
|
||||
},
|
||||
)
|
||||
async def update(
|
||||
async def update( # noqa: ANN201
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
spool_id: int,
|
||||
body: SpoolUpdateParameters,
|
||||
) -> Spool:
|
||||
):
|
||||
patch_data = body.dict(exclude_unset=True)
|
||||
|
||||
if body.remaining_weight is not None and body.used_weight is not None:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"message": "Only specify either remaining_weight or used_weight."},
|
||||
)
|
||||
|
||||
if "filament_id" in patch_data and body.filament_id is None:
|
||||
raise RequestValidationError(
|
||||
[ErrorWrapper(ValueError("filament_id cannot be unset"), ("query", "filament_id"))],
|
||||
)
|
||||
|
||||
try:
|
||||
db_item = await spool.update(
|
||||
db=db,
|
||||
spool_id=spool_id,
|
||||
data=patch_data,
|
||||
)
|
||||
except ItemCreateError as exc:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
return Spool.from_db(db_item)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class Spool(Base):
|
||||
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()
|
||||
used_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))
|
||||
|
||||
@@ -16,7 +16,8 @@ async def create(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
filament_id: int,
|
||||
weight: Optional[float] = None,
|
||||
remaining_weight: Optional[float] = None,
|
||||
used_weight: Optional[float] = None,
|
||||
first_used: Optional[datetime] = None,
|
||||
last_used: Optional[datetime] = None,
|
||||
location: Optional[str] = None,
|
||||
@@ -25,14 +26,17 @@ async def create(
|
||||
) -> 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 used_weight is None:
|
||||
if remaining_weight is not 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
|
||||
raise ItemCreateError("remaining_weight can only be used if the filament type has a weight set.")
|
||||
used_weight = max(filament_item.weight - remaining_weight, 0)
|
||||
else:
|
||||
used_weight = 0
|
||||
|
||||
db_item = models.Spool(
|
||||
filament=filament_item,
|
||||
weight=weight,
|
||||
used_weight=used_weight,
|
||||
first_used=first_used,
|
||||
last_used=last_used,
|
||||
location=location,
|
||||
@@ -105,6 +109,10 @@ async def update(
|
||||
for k, v in data.items():
|
||||
if k == "filament_id":
|
||||
spool.filament = await filament.get_by_id(db, v)
|
||||
elif k == "remaining_weight":
|
||||
if spool.filament.weight is None:
|
||||
raise ItemCreateError("remaining_weight can only be used if the filament type has a weight set.")
|
||||
spool.used_weight = max(spool.filament.weight - v, 0)
|
||||
else:
|
||||
setattr(spool, k, v)
|
||||
await db.flush()
|
||||
@@ -119,33 +127,33 @@ async def delete(db: AsyncSession, spool_id: int) -> None:
|
||||
|
||||
# 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.
|
||||
"""Consume filament from a spool by weight.
|
||||
|
||||
Does nothing if the spool is empty.
|
||||
|
||||
Args:
|
||||
db (AsyncSession): Database session
|
||||
spool_id (int): Spool ID
|
||||
weight (float): Weight loss in grams
|
||||
weight (float): Filament weight to consume, 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)
|
||||
spool.used_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.
|
||||
"""Consume filament from a spool by length.
|
||||
|
||||
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
|
||||
length (float): Length of filament to consume, in mm
|
||||
|
||||
Returns:
|
||||
models.Spool: Updated spool object
|
||||
@@ -154,11 +162,10 @@ async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.S
|
||||
|
||||
filament = spool.filament
|
||||
|
||||
weight = weight_from_length(
|
||||
spool.used_weight += weight_from_length(
|
||||
length=length,
|
||||
radius=filament.diameter / 2,
|
||||
density=filament.density,
|
||||
)
|
||||
spool.weight -= min(spool.weight, weight)
|
||||
await db.flush()
|
||||
return spool
|
||||
|
||||
Reference in New Issue
Block a user