Added API for fetching external database, removed parameters structure,
This commit is contained in:
@@ -53,6 +53,14 @@ SPOOLMAN_PORT=7912
|
|||||||
# Default: FALSE
|
# Default: FALSE
|
||||||
#SPOOLMAN_METRICS_ENABLED=TRUE
|
#SPOOLMAN_METRICS_ENABLED=TRUE
|
||||||
|
|
||||||
|
# Collect items (filaments, materials, etc.) from an external database
|
||||||
|
# Set this to a URL of an external database. Set to an empty string to disable
|
||||||
|
# Default: https://donkie.github.io/SpoolmanDB/
|
||||||
|
#EXTERNAL_DB_URL=https://myhost.com/spoolmandb/
|
||||||
|
# Sync interval in seconds, set to 0 to disable automatic sync. It will only sync on start-up then.
|
||||||
|
# Default: 3600
|
||||||
|
#EXTERNAL_DB_SYNC_INTERVAL=3600
|
||||||
|
|
||||||
# Enable debug mode
|
# Enable debug mode
|
||||||
# If enabled, the client will accept requests from any host
|
# If enabled, the client will accept requests from any host
|
||||||
# This can be useful when developing, but is also a security risk
|
# This can be useful when developing, but is also a security risk
|
||||||
|
|||||||
39
spoolman/api/v1/externaldb.py
Normal file
39
spoolman/api/v1/externaldb.py
Normal 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")
|
||||||
@@ -15,7 +15,7 @@ from spoolman.database.database import backup_global_db
|
|||||||
from spoolman.exceptions import ItemNotFoundError
|
from spoolman.exceptions import ItemNotFoundError
|
||||||
from spoolman.ws import websocket_manager
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -110,3 +110,4 @@ app.include_router(vendor.router)
|
|||||||
app.include_router(setting.router)
|
app.include_router(setting.router)
|
||||||
app.include_router(field.router)
|
app.include_router(field.router)
|
||||||
app.include_router(other.router)
|
app.include_router(other.router)
|
||||||
|
app.include_router(externaldb.router)
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ def check_write_permissions() -> None:
|
|||||||
|
|
||||||
# Try fixing it by chowning the directory to the current user
|
# Try fixing it by chowning the directory to the current user
|
||||||
logger.warning("Data directory is not writable, trying to fix it...")
|
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()
|
uid = os.getuid()
|
||||||
gid = os.getgid()
|
gid = os.getgid()
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import hishel
|
import hishel
|
||||||
from pydantic import BaseModel, ConfigDict, Extra, Field
|
from pydantic import BaseModel, Field
|
||||||
from scheduler.asyncio.scheduler import Scheduler
|
from scheduler.asyncio.scheduler import Scheduler
|
||||||
|
|
||||||
|
from spoolman import filecache
|
||||||
|
|
||||||
DEFAULT_EXTERNAL_DB_URL = "https://donkie.github.io/SpoolmanDB/"
|
DEFAULT_EXTERNAL_DB_URL = "https://donkie.github.io/SpoolmanDB/"
|
||||||
DEFAULT_SYNC_INTERVAL = 3600
|
DEFAULT_SYNC_INTERVAL = 3600
|
||||||
|
|
||||||
@@ -18,26 +21,18 @@ cache_storage = hishel.AsyncFileStorage()
|
|||||||
logger = logging.getLogger(__name__)
|
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):
|
class ExternalFilament(BaseModel):
|
||||||
id: str = Field()
|
id: str = Field(description="A unique ID for this filament.", example="polymaker_pla_polysonicblack_1000_175")
|
||||||
manufacturer: str = Field()
|
manufacturer: str = Field(description="Filament manufacturer.", example="Polymaker")
|
||||||
name: str = Field()
|
name: str = Field(description="Filament name.", example="Polysonic\u2122 Black")
|
||||||
material: str = Field()
|
material: str = Field(description="Filament material.", example="PLA")
|
||||||
density: Optional[float] = Field(default=None)
|
density: Optional[float] = Field(default=None, description="Density in g/cm3.", example=1.23)
|
||||||
weight: float = Field(default=None)
|
weight: float = Field(description="Net weight of a single spool.", example=1000)
|
||||||
spool_weight: Optional[float] = Field(default=None)
|
spool_weight: Optional[float] = Field(default=None, description="Weight of an empty spool.", example=140)
|
||||||
diameter: float = Field(default=None)
|
diameter: float = Field(description="Filament in mm.", example=1.75)
|
||||||
color_hex: str = Field()
|
color_hex: str = Field(description="Filament color code in hex format.", example="#2c3232")
|
||||||
parameters: ExternalFilamentParameters = Field(ExternalFilamentParameters())
|
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):
|
class ExternalFilamentsFile(BaseModel):
|
||||||
@@ -45,27 +40,14 @@ class ExternalFilamentsFile(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ExternalMaterial(BaseModel):
|
class ExternalMaterial(BaseModel):
|
||||||
material: str = Field()
|
material: str = Field(example="PLA")
|
||||||
density: float = Field()
|
density: float = Field(example=1.24)
|
||||||
|
|
||||||
|
|
||||||
class ExternalMaterialsFile(BaseModel):
|
class ExternalMaterialsFile(BaseModel):
|
||||||
__root__: list[ExternalMaterial]
|
__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:
|
def get_external_db_url() -> str:
|
||||||
"""Get the external database URL from environment variables. Defaults to DEFAULT_EXTERNAL_DB_URL."""
|
"""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("/") + "/"
|
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()
|
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."""
|
"""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."""
|
"""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:
|
def _write_to_local_cache(filename: str, data: bytes) -> None:
|
||||||
"""Get the external database."""
|
"""Write data to the local cache."""
|
||||||
return _externaldb
|
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:
|
async def _sync() -> None:
|
||||||
logger.info("Syncing external DB.")
|
logger.info("Syncing external DB.")
|
||||||
|
|
||||||
url = get_external_db_url()
|
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(
|
logger.info(
|
||||||
"External DB synced. Filaments: %d, Materials: %d",
|
"External DB synced. Filaments: %d, Materials: %d",
|
||||||
len(_externaldb.filaments),
|
len(filaments.__root__),
|
||||||
len(_externaldb.materials),
|
len(materials.__root__),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +118,10 @@ def schedule_tasks(scheduler: Scheduler) -> None:
|
|||||||
Args:
|
Args:
|
||||||
scheduler: The scheduler to use for scheduling tasks.
|
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.")
|
logger.info("Scheduling external DB sync.")
|
||||||
|
|
||||||
# Run once on startup
|
# Run once on startup
|
||||||
|
|||||||
30
spoolman/filecache.py
Normal file
30
spoolman/filecache.py
Normal 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()
|
||||||
Reference in New Issue
Block a user