Added custom extra fields to backend
No integration tests yet, TBD
This commit is contained in:
98
spoolman/api/v1/field.py
Normal file
98
spoolman/api/v1/field.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Vendor related endpoints."""
|
||||
|
||||
import logging
|
||||
from typing import Annotated, Union
|
||||
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from spoolman.api.v1.models import Message
|
||||
from spoolman.database.database import get_db_session
|
||||
from spoolman.exceptions import ItemNotFoundError
|
||||
from spoolman.extra_fields import (
|
||||
EntityType,
|
||||
ExtraField,
|
||||
ExtraFieldParameters,
|
||||
add_or_update_extra_field,
|
||||
delete_extra_field,
|
||||
get_extra_fields,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/field",
|
||||
tags=["field"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103,B008
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{entity_type}",
|
||||
name="Get extra fields",
|
||||
description="Get all extra fields for a specific entity type.",
|
||||
response_model_exclude_none=True,
|
||||
)
|
||||
async def get(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
entity_type: Annotated[EntityType, Path(description="Entity type this field is for")],
|
||||
) -> list[ExtraField]:
|
||||
return await get_extra_fields(db, entity_type)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{entity_type}/{key}",
|
||||
name="Add or update extra field",
|
||||
description=(
|
||||
"Add or update an extra field for a specific entity type. "
|
||||
"Returns the full list of extra fields for the entity type."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
response_model=list[ExtraField],
|
||||
responses={400: {"model": Message}},
|
||||
)
|
||||
async def update(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
entity_type: Annotated[EntityType, Path(description="Entity type this field is for")],
|
||||
key: Annotated[str, Path(min_length=1, max_length=64, regex="^[a-z0-9_]+$")],
|
||||
body: ExtraFieldParameters,
|
||||
) -> Union[list[ExtraField], JSONResponse]:
|
||||
body_with_key = ExtraField.parse_obj(body.copy(update={"key": key}))
|
||||
|
||||
try:
|
||||
await add_or_update_extra_field(db, entity_type, body_with_key)
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
|
||||
|
||||
return await get_extra_fields(db, entity_type)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{entity_type}/{key}",
|
||||
name="Delete extra field",
|
||||
description=(
|
||||
"Delete an extra field for a specific entity type. "
|
||||
"Returns the full list of extra fields for the entity type."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
response_model=list[ExtraField],
|
||||
responses={404: {"model": Message}},
|
||||
)
|
||||
async def delete(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
entity_type: Annotated[EntityType, Path(description="Entity type this field is for")],
|
||||
key: Annotated[str, Path(min_length=1, max_length=64, regex="^[a-z0-9_]+$")],
|
||||
) -> Union[list[ExtraField], JSONResponse]:
|
||||
try:
|
||||
await delete_extra_field(db, entity_type, key)
|
||||
except ItemNotFoundError:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content=Message(
|
||||
message=f"Extra field with key {key} does not exist for entity type {entity_type.name}",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
return await get_extra_fields(db, entity_type)
|
||||
@@ -17,6 +17,7 @@ from spoolman.database import filament
|
||||
from spoolman.database.database import get_db_session
|
||||
from spoolman.database.utils import SortOrder
|
||||
from spoolman.exceptions import ItemDeleteError
|
||||
from spoolman.extra_fields import EntityType, get_extra_fields, validate_extra_field_dict
|
||||
from spoolman.ws import websocket_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -81,6 +82,10 @@ class FilamentParameters(BaseModel):
|
||||
description="Hexadecimal color code of the filament, e.g. FF0000 for red. Supports alpha channel at the end.",
|
||||
example="FF0000",
|
||||
)
|
||||
extra: Optional[dict[str, str]] = Field(
|
||||
None,
|
||||
description="Extra fields for this filament.",
|
||||
)
|
||||
|
||||
@validator("color_hex")
|
||||
@classmethod
|
||||
@@ -323,11 +328,20 @@ async def notify(
|
||||
name="Add filament",
|
||||
description="Add a new filament to the database.",
|
||||
response_model_exclude_none=True,
|
||||
response_model=Filament,
|
||||
responses={400: {"model": Message}},
|
||||
)
|
||||
async def create(
|
||||
async def create( # noqa: ANN201
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
body: FilamentParameters,
|
||||
) -> Filament:
|
||||
):
|
||||
if body.extra:
|
||||
all_fields = await get_extra_fields(db, EntityType.filament)
|
||||
try:
|
||||
validate_extra_field_dict(all_fields, body.extra)
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
|
||||
|
||||
db_item = await filament.create(
|
||||
db=db,
|
||||
density=body.density,
|
||||
@@ -343,6 +357,7 @@ async def create(
|
||||
settings_extruder_temp=body.settings_extruder_temp,
|
||||
settings_bed_temp=body.settings_bed_temp,
|
||||
color_hex=body.color_hex,
|
||||
extra=body.extra,
|
||||
)
|
||||
|
||||
return Filament.from_db(db_item)
|
||||
@@ -351,15 +366,22 @@ async def create(
|
||||
@router.patch(
|
||||
"/{filament_id}",
|
||||
name="Update filament",
|
||||
description="Update any attribute of a filament. Only fields specified in the request will be affected.",
|
||||
description=(
|
||||
"Update any attribute of a filament. Only fields specified in the request will be affected. "
|
||||
"If extra is set, all existing extra fields will be removed and replaced with the new ones."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
responses={404: {"model": Message}},
|
||||
response_model=Filament,
|
||||
responses={
|
||||
400: {"model": Message},
|
||||
404: {"model": Message},
|
||||
},
|
||||
)
|
||||
async def update(
|
||||
async def update( # noqa: ANN201
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
filament_id: int,
|
||||
body: FilamentUpdateParameters,
|
||||
) -> Filament:
|
||||
):
|
||||
patch_data = body.dict(exclude_unset=True)
|
||||
|
||||
if "density" in patch_data and body.density is None:
|
||||
@@ -367,6 +389,13 @@ async def update(
|
||||
if "diameter" in patch_data and body.diameter is None:
|
||||
raise RequestValidationError([ErrorWrapper(ValueError("diameter cannot be unset"), ("query", "diameter"))])
|
||||
|
||||
if body.extra:
|
||||
all_fields = await get_extra_fields(db, EntityType.filament)
|
||||
try:
|
||||
validate_extra_field_dict(all_fields, body.extra)
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
|
||||
|
||||
db_item = await filament.update(
|
||||
db=db,
|
||||
filament_id=filament_id,
|
||||
|
||||
@@ -60,6 +60,12 @@ class Vendor(BaseModel):
|
||||
registered: datetime = Field(description="When the vendor was registered in the database. UTC Timezone.")
|
||||
name: str = Field(max_length=64, description="Vendor name.", example="Polymaker")
|
||||
comment: Optional[str] = Field(max_length=1024, description="Free text comment about this vendor.", example="")
|
||||
extra: dict[str, str] = Field(
|
||||
description=(
|
||||
"Extra fields for this vendor. All values are JSON-encoded data. "
|
||||
"Query the /fields endpoint for more details about the fields."
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_db(item: models.Vendor) -> "Vendor":
|
||||
@@ -69,6 +75,7 @@ class Vendor(BaseModel):
|
||||
registered=item.registered,
|
||||
name=item.name,
|
||||
comment=item.comment,
|
||||
extra={field.key: field.value for field in item.extra},
|
||||
)
|
||||
|
||||
|
||||
@@ -128,6 +135,12 @@ class Filament(BaseModel):
|
||||
description="Hexadecimal color code of the filament, e.g. FF0000 for red. Supports alpha channel at the end.",
|
||||
example="FF0000",
|
||||
)
|
||||
extra: dict[str, str] = Field(
|
||||
description=(
|
||||
"Extra fields for this filament. All values are JSON-encoded data. "
|
||||
"Query the /fields endpoint for more details about the fields."
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_db(item: models.Filament) -> "Filament":
|
||||
@@ -148,6 +161,7 @@ class Filament(BaseModel):
|
||||
settings_extruder_temp=item.settings_extruder_temp,
|
||||
settings_bed_temp=item.settings_bed_temp,
|
||||
color_hex=item.color_hex,
|
||||
extra={field.key: field.value for field in item.extra},
|
||||
)
|
||||
|
||||
|
||||
@@ -198,6 +212,12 @@ class Spool(BaseModel):
|
||||
example="",
|
||||
)
|
||||
archived: bool = Field(description="Whether this spool is archived and should not be used anymore.")
|
||||
extra: dict[str, str] = Field(
|
||||
description=(
|
||||
"Extra fields for this spool. All values are JSON-encoded data. "
|
||||
"Query the /fields endpoint for more details about the fields."
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_db(item: models.Spool) -> "Spool":
|
||||
@@ -235,6 +255,7 @@ class Spool(BaseModel):
|
||||
lot_nr=item.lot_nr,
|
||||
comment=item.comment,
|
||||
archived=item.archived if item.archived is not None else False,
|
||||
extra={field.key: field.value for field in item.extra},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from spoolman.database.database import backup_global_db
|
||||
from spoolman.exceptions import ItemNotFoundError
|
||||
from spoolman.ws import websocket_manager
|
||||
|
||||
from . import filament, models, other, setting, spool, vendor
|
||||
from . import field, filament, models, other, setting, spool, vendor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -108,4 +108,5 @@ app.include_router(filament.router)
|
||||
app.include_router(spool.router)
|
||||
app.include_router(vendor.router)
|
||||
app.include_router(setting.router)
|
||||
app.include_router(field.router)
|
||||
app.include_router(other.router)
|
||||
|
||||
@@ -18,6 +18,7 @@ from spoolman.database import spool
|
||||
from spoolman.database.database import get_db_session
|
||||
from spoolman.database.utils import SortOrder
|
||||
from spoolman.exceptions import ItemCreateError
|
||||
from spoolman.extra_fields import EntityType, get_extra_fields, validate_extra_field_dict
|
||||
from spoolman.ws import websocket_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -59,6 +60,10 @@ class SpoolParameters(BaseModel):
|
||||
example="",
|
||||
)
|
||||
archived: bool = Field(default=False, description="Whether this spool is archived and should not be used anymore.")
|
||||
extra: Optional[dict[str, str]] = Field(
|
||||
None,
|
||||
description="Extra fields for this spool.",
|
||||
)
|
||||
|
||||
|
||||
class SpoolUpdateParameters(SpoolParameters):
|
||||
@@ -337,6 +342,13 @@ async def create( # noqa: ANN201
|
||||
content={"message": "Only specify either remaining_weight or used_weight."},
|
||||
)
|
||||
|
||||
if body.extra:
|
||||
all_fields = await get_extra_fields(db, EntityType.spool)
|
||||
try:
|
||||
validate_extra_field_dict(all_fields, body.extra)
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
|
||||
|
||||
try:
|
||||
db_item = await spool.create(
|
||||
db=db,
|
||||
@@ -350,6 +362,7 @@ async def create( # noqa: ANN201
|
||||
lot_nr=body.lot_nr,
|
||||
comment=body.comment,
|
||||
archived=body.archived,
|
||||
extra=body.extra,
|
||||
)
|
||||
return Spool.from_db(db_item)
|
||||
except ItemCreateError:
|
||||
@@ -366,7 +379,8 @@ async def create( # noqa: ANN201
|
||||
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."
|
||||
"remaining_weight and used_weight can't be set at the same time. "
|
||||
"If extra is set, all existing extra fields will be removed and replaced with the new ones."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
response_model=Spool,
|
||||
@@ -388,6 +402,13 @@ async def update( # noqa: ANN201
|
||||
content={"message": "Only specify either remaining_weight or used_weight."},
|
||||
)
|
||||
|
||||
if body.extra:
|
||||
all_fields = await get_extra_fields(db, EntityType.spool)
|
||||
try:
|
||||
validate_extra_field_dict(all_fields, body.extra)
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
|
||||
|
||||
if "filament_id" in patch_data and body.filament_id is None:
|
||||
raise RequestValidationError(
|
||||
[ErrorWrapper(ValueError("filament_id cannot be unset"), ("query", "filament_id"))],
|
||||
|
||||
@@ -15,6 +15,7 @@ from spoolman.api.v1.models import Message, Vendor, VendorEvent
|
||||
from spoolman.database import vendor
|
||||
from spoolman.database.database import get_db_session
|
||||
from spoolman.database.utils import SortOrder
|
||||
from spoolman.extra_fields import EntityType, get_extra_fields, validate_extra_field_dict
|
||||
from spoolman.ws import websocket_manager
|
||||
|
||||
router = APIRouter(
|
||||
@@ -32,6 +33,10 @@ class VendorParameters(BaseModel):
|
||||
description="Free text comment about this vendor.",
|
||||
example="",
|
||||
)
|
||||
extra: Optional[dict[str, str]] = Field(
|
||||
None,
|
||||
description="Extra fields for this vendor.",
|
||||
)
|
||||
|
||||
|
||||
class VendorUpdateParameters(VendorParameters):
|
||||
@@ -167,15 +172,25 @@ async def notify(
|
||||
name="Add vendor",
|
||||
description="Add a new vendor to the database.",
|
||||
response_model_exclude_none=True,
|
||||
response_model=Vendor,
|
||||
responses={400: {"model": Message}},
|
||||
)
|
||||
async def create(
|
||||
async def create( # noqa: ANN201
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
body: VendorParameters,
|
||||
) -> Vendor:
|
||||
):
|
||||
if body.extra:
|
||||
all_fields = await get_extra_fields(db, EntityType.vendor)
|
||||
try:
|
||||
validate_extra_field_dict(all_fields, body.extra)
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
|
||||
|
||||
db_item = await vendor.create(
|
||||
db=db,
|
||||
name=body.name,
|
||||
comment=body.comment,
|
||||
extra=body.extra,
|
||||
)
|
||||
|
||||
return Vendor.from_db(db_item)
|
||||
@@ -184,20 +199,34 @@ async def create(
|
||||
@router.patch(
|
||||
"/{vendor_id}",
|
||||
name="Update vendor",
|
||||
description="Update any attribute of a vendor. Only fields specified in the request will be affected.",
|
||||
description=(
|
||||
"Update any attribute of a vendor. Only fields specified in the request will be affected. "
|
||||
"If extra is set, all existing extra fields will be removed and replaced with the new ones."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
responses={404: {"model": Message}},
|
||||
response_model=Vendor,
|
||||
responses={
|
||||
400: {"model": Message},
|
||||
404: {"model": Message},
|
||||
},
|
||||
)
|
||||
async def update(
|
||||
async def update( # noqa: ANN201
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
vendor_id: int,
|
||||
body: VendorUpdateParameters,
|
||||
) -> Vendor:
|
||||
):
|
||||
patch_data = body.dict(exclude_unset=True)
|
||||
|
||||
if "name" in patch_data and body.name is None:
|
||||
raise RequestValidationError([ErrorWrapper(ValueError("name cannot be unset"), ("query", "name"))])
|
||||
|
||||
if body.extra:
|
||||
all_fields = await get_extra_fields(db, EntityType.vendor)
|
||||
try:
|
||||
validate_extra_field_dict(all_fields, body.extra)
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
|
||||
|
||||
db_item = await vendor.update(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
|
||||
Reference in New Issue
Block a user