Added API for fetching external database, removed parameters structure,

This commit is contained in:
Donkie
2024-05-12 12:14:25 +02:00
parent 595fd82b56
commit cbaa7b81df
6 changed files with 127 additions and 48 deletions

View File

@@ -0,0 +1,39 @@
"""External database API."""
import logging
from fastapi import APIRouter
from fastapi.responses import FileResponse
from spoolman.externaldb import ExternalFilament, ExternalMaterial, get_filaments_file, get_materials_file
router = APIRouter(
prefix="/external",
tags=["external"],
)
# ruff: noqa: D103,B008
logger = logging.getLogger(__name__)
@router.get(
"/filaments",
name="Get all external filaments",
response_model_exclude_none=True,
response_model=list[ExternalFilament],
)
async def filaments() -> FileResponse:
"""Get all external filaments."""
return FileResponse(path=get_filaments_file(), media_type="application/json")
@router.get(
"/materials",
name="Get all external materials",
response_model_exclude_none=True,
response_model=list[ExternalMaterial],
)
async def materials() -> FileResponse:
"""Get all external materials."""
return FileResponse(path=get_materials_file(), media_type="application/json")

View File

@@ -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 field, filament, models, other, setting, spool, vendor
from . import externaldb, field, filament, models, other, setting, spool, vendor
logger = logging.getLogger(__name__)
@@ -110,3 +110,4 @@ app.include_router(vendor.router)
app.include_router(setting.router)
app.include_router(field.router)
app.include_router(other.router)
app.include_router(externaldb.router)

View File

@@ -354,7 +354,7 @@ def check_write_permissions() -> None:
# Try fixing it by chowning the directory to the current user
logger.warning("Data directory is not writable, trying to fix it...")
if not chown_dir(get_data_dir()) or not can_write_to_data_dir():
if not chown_dir(str(get_data_dir())) or not can_write_to_data_dir():
uid = os.getuid()
gid = os.getgid()

View File

@@ -3,12 +3,15 @@
import datetime
import logging
import os
from pathlib import Path
from typing import Optional
import hishel
from pydantic import BaseModel, ConfigDict, Extra, Field
from pydantic import BaseModel, Field
from scheduler.asyncio.scheduler import Scheduler
from spoolman import filecache
DEFAULT_EXTERNAL_DB_URL = "https://donkie.github.io/SpoolmanDB/"
DEFAULT_SYNC_INTERVAL = 3600
@@ -18,26 +21,18 @@ cache_storage = hishel.AsyncFileStorage()
logger = logging.getLogger(__name__)
class ExternalFilamentParameters(BaseModel):
model_config = ConfigDict(
extra=Extra.allow,
)
extruder_temp: Optional[float] = Field(default=None)
bed_temp: Optional[float] = Field(default=None)
class ExternalFilament(BaseModel):
id: str = Field()
manufacturer: str = Field()
name: str = Field()
material: str = Field()
density: Optional[float] = Field(default=None)
weight: float = Field(default=None)
spool_weight: Optional[float] = Field(default=None)
diameter: float = Field(default=None)
color_hex: str = Field()
parameters: ExternalFilamentParameters = Field(ExternalFilamentParameters())
id: str = Field(description="A unique ID for this filament.", example="polymaker_pla_polysonicblack_1000_175")
manufacturer: str = Field(description="Filament manufacturer.", example="Polymaker")
name: str = Field(description="Filament name.", example="Polysonic\u2122 Black")
material: str = Field(description="Filament material.", example="PLA")
density: Optional[float] = Field(default=None, description="Density in g/cm3.", example=1.23)
weight: float = Field(description="Net weight of a single spool.", example=1000)
spool_weight: Optional[float] = Field(default=None, description="Weight of an empty spool.", example=140)
diameter: float = Field(description="Filament in mm.", example=1.75)
color_hex: str = Field(description="Filament color code in hex format.", example="#2c3232")
extruder_temp: Optional[int] = Field(default=None, description="Extruder/nozzle temperature in °C.", example=210)
bed_temp: Optional[int] = Field(default=None, description="Bed temperature in °C.", example=50)
class ExternalFilamentsFile(BaseModel):
@@ -45,27 +40,14 @@ class ExternalFilamentsFile(BaseModel):
class ExternalMaterial(BaseModel):
material: str = Field()
density: float = Field()
material: str = Field(example="PLA")
density: float = Field(example=1.24)
class ExternalMaterialsFile(BaseModel):
__root__: list[ExternalMaterial]
class ExternalDB:
filaments: list[ExternalFilament]
materials: list[ExternalMaterial]
def __init__(self) -> None:
"""Initialize the ExternalDB."""
self.filaments = []
self.materials = []
_externaldb = ExternalDB()
def get_external_db_url() -> str:
"""Get the external database URL from environment variables. Defaults to DEFAULT_EXTERNAL_DB_URL."""
return os.getenv("EXTERNAL_DB_URL", DEFAULT_EXTERNAL_DB_URL).strip("/") + "/"
@@ -87,31 +69,46 @@ async def _download_file(url: str) -> bytes:
return response.read()
def _parse_filaments_from_bytes(data: bytes) -> list[ExternalFilament]:
def _parse_filaments_from_bytes(data: bytes) -> ExternalFilamentsFile:
"""Parse a bytes string into a list of ExternalFilament objects."""
return ExternalFilamentsFile.parse_raw(data).__root__
return ExternalFilamentsFile.parse_raw(data)
def _parse_materials_from_bytes(data: bytes) -> list[ExternalMaterial]:
def _parse_materials_from_bytes(data: bytes) -> ExternalMaterialsFile:
"""Parse a bytes string into a list of ExternalMaterial objects."""
return ExternalMaterialsFile.parse_raw(data).__root__
return ExternalMaterialsFile.parse_raw(data)
def get_external_db() -> ExternalDB:
"""Get the external database."""
return _externaldb
def _write_to_local_cache(filename: str, data: bytes) -> None:
"""Write data to the local cache."""
filecache.update_file(filename, data)
def get_filaments_file() -> Path:
"""Get the path to the filaments file."""
return filecache.get_file("filaments.json")
def get_materials_file() -> Path:
"""Get the path to the materials file."""
return filecache.get_file("materials.json")
async def _sync() -> None:
logger.info("Syncing external DB.")
url = get_external_db_url()
_externaldb.filaments = _parse_filaments_from_bytes(await _download_file(url + "filaments.json"))
_externaldb.materials = _parse_materials_from_bytes(await _download_file(url + "materials.json"))
filaments = _parse_filaments_from_bytes(await _download_file(url + "filaments.json"))
materials = _parse_materials_from_bytes(await _download_file(url + "materials.json"))
_write_to_local_cache("filaments.json", filaments.json().encode())
_write_to_local_cache("materials.json", materials.json().encode())
logger.info(
"External DB synced. Filaments: %d, Materials: %d",
len(_externaldb.filaments),
len(_externaldb.materials),
len(filaments.__root__),
len(materials.__root__),
)
@@ -121,6 +118,10 @@ def schedule_tasks(scheduler: Scheduler) -> None:
Args:
scheduler: The scheduler to use for scheduling tasks.
"""
if len(get_external_db_url().strip()) == 0:
logger.info("External DB URL is empty. Skipping sync.")
return
logger.info("Scheduling external DB sync.")
# Run once on startup

30
spoolman/filecache.py Normal file
View File

@@ -0,0 +1,30 @@
"""A file-based cache system for reading/writing files."""
from pathlib import Path
from spoolman.env import get_data_dir
def _get_cache_dir() -> Path:
"""Get the cache directory."""
return get_data_dir() / "cache"
def get_file(name: str) -> Path:
"""Get the path to a file."""
return _get_cache_dir() / name
def update_file(name: str, data: bytes) -> None:
"""Update a file if it differs from the given data."""
path = get_file(name)
if path.exists() and path.read_bytes() == data:
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
def get_file_contents(name: str) -> bytes:
"""Get the contents of a file."""
path = get_file(name)
return path.read_bytes()