Files
spoolman2/spoolman/api/v1/setting.py
Donkie 44da991d47 Added a new backend settings system
This can be used for saving runtime-configurable settings in the database.
2024-01-16 19:17:06 +01:00

195 lines
5.7 KiB
Python

"""Vendor related endpoints."""
import asyncio
import logging
from typing import Annotated, Union
from fastapi import APIRouter, Body, Depends, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.api.v1.models import Message, SettingEvent, SettingResponse
from spoolman.database import setting
from spoolman.database.database import get_db_session
from spoolman.exceptions import ItemNotFoundError
from spoolman.settings import SETTINGS, parse_setting
from spoolman.ws import websocket_manager
router = APIRouter(
prefix="/setting",
tags=["setting"],
)
# ruff: noqa: D103,B008
logger = logging.getLogger(__name__)
@router.websocket(
"",
name="Listen to setting changes",
)
async def notify_any(
websocket: WebSocket,
) -> None:
await websocket.accept()
websocket_manager.connect(("setting",), websocket)
try:
while True:
await asyncio.sleep(0.5)
if await websocket.receive_text():
await websocket.send_json({"status": "healthy"})
except WebSocketDisconnect:
websocket_manager.disconnect(("setting",), websocket)
@router.get(
"/{key}",
name="Get setting",
description=(
"Get a specific setting. If the setting has not been set, the default value will be returned."
"A websocket is served on the same path to listen for changes to the setting. "
"See the HTTP Response code 299 for the content of the websocket messages."
),
response_model_exclude_none=True,
response_model=SettingResponse,
responses={404: {"model": Message}, 299: {"model": SettingEvent, "description": "Websocket message"}},
)
async def get(
db: Annotated[AsyncSession, Depends(get_db_session)],
key: str,
) -> Union[SettingResponse, JSONResponse]:
try:
definition = parse_setting(key)
except ValueError as e:
return JSONResponse(status_code=404, content=Message(message=str(e)).dict())
try:
db_item = await setting.get(db, definition)
value = db_item.value
is_set = True
except ItemNotFoundError:
value = definition.default
is_set = False
return SettingResponse(
value=value,
is_set=is_set,
type=definition.type,
)
@router.get(
"/",
name="Get all settings",
description=("Get all settings, set or not. If the setting has not been set, 'value' will be the default value."),
response_model_exclude_none=True,
response_model=dict[str, SettingResponse],
)
async def find(
db: Annotated[AsyncSession, Depends(get_db_session)],
) -> Union[dict[str, SettingResponse], JSONResponse]:
settings: dict[str, SettingResponse] = {}
# First get all settings that have been set.
db_items = await setting.get_all(db)
for db_item in db_items:
try:
definition = parse_setting(db_item.key)
except ValueError:
continue # Ignore settings that have been removed.
settings[db_item.key] = SettingResponse(
value=db_item.value,
is_set=True,
type=definition.type,
)
# Then get all settings that have not been set.
for settingdef in SETTINGS.values():
if settingdef.key not in settings:
settings[settingdef.key] = SettingResponse(
value=settingdef.default,
is_set=False,
type=settingdef.type,
)
return settings
@router.websocket(
"/{key}",
name="Listen to setting changes",
)
async def notify(
websocket: WebSocket,
key: str,
) -> None:
try:
parse_setting(key)
except ValueError as e:
await websocket.close(code=4040, reason=str(e))
return
await websocket.accept()
websocket_manager.connect(("setting", str(key)), websocket)
try:
while True:
await asyncio.sleep(0.5)
if await websocket.receive_text():
await websocket.send_json({"status": "healthy"})
except WebSocketDisconnect:
websocket_manager.disconnect(("setting", str(key)), websocket)
@router.post(
"/{key}",
name="Set setting",
description=(
"Set the value of a setting. The body must match the JSON type of the setting. "
"An empty body or a body containing only 'null' will reset the setting to its default value. "
"The new value will be returned."
),
response_model_exclude_none=True,
response_model=SettingResponse,
responses={404: {"model": Message}},
)
async def update(
db: Annotated[AsyncSession, Depends(get_db_session)],
key: str,
body: Annotated[str, Body()],
) -> Union[SettingResponse, JSONResponse]:
try:
definition = parse_setting(key)
except ValueError as e:
return JSONResponse(status_code=404, content=Message(message=str(e)).dict())
if body and body != "null":
try:
definition.validate_type(body)
except ValueError as e:
return JSONResponse(status_code=400, content=Message(message=str(e)).dict())
await setting.update(db=db, definition=definition, value=body)
logger.info('Setting "%s" has been set to "%s".', key, body)
else:
await setting.delete(db=db, definition=definition)
logger.info('Setting "%s" has been unset.', key)
await db.commit()
# Get the new value of the setting.
try:
db_item = await setting.get(db, definition)
value = db_item.value
is_set = True
except ItemNotFoundError:
value = definition.default
is_set = False
return SettingResponse(
value=value,
is_set=is_set,
type=definition.type,
)