Compare commits

...

2 commits

13 changed files with 156 additions and 149 deletions

View file

@ -4,19 +4,19 @@ Definition of an asyncio compatible CalDAV calendar.
Caches events using `timed_alru_cache`. Caches events using `timed_alru_cache`.
""" """
import functools
import logging
from datetime import datetime from datetime import datetime
from functools import total_ordering
from logging import getLogger
from typing import Annotated, Self from typing import Annotated, Self
from pydantic import BaseModel, ConfigDict, StringConstraints from pydantic import BaseModel, ConfigDict, StringConstraints
from vobject.base import Component from vobject.base import Component
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
type StrippedStr = Annotated[str, StringConstraints(strip_whitespace=True)] type StrippedStr = Annotated[str, StringConstraints(strip_whitespace=True)]
@total_ordering @functools.total_ordering
class CalEvent(BaseModel): class CalEvent(BaseModel):
""" """
A CalDAV calendar event. A CalDAV calendar event.

View file

@ -2,18 +2,12 @@
Python representation of the "config.txt" file inside the WebDAV directory. Python representation of the "config.txt" file inside the WebDAV directory.
""" """
import tomllib import logging
from logging import getLogger
from typing import Any from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
from webdav3.exceptions import RemoteResourceNotFound
# from .caldav import CalDAV _logger = logging.getLogger(__name__)
from .settings import SETTINGS
from .webdav import WebDAV
_logger = getLogger(__name__)
class TickerUIConfig(BaseModel): class TickerUIConfig(BaseModel):
@ -107,28 +101,3 @@ class Config(BaseModel):
server: ServerUIConfig = ServerUIConfig() server: ServerUIConfig = ServerUIConfig()
ticker: TickerConfig = TickerConfig() ticker: TickerConfig = TickerConfig()
calendar: CalendarConfig = CalendarConfig() calendar: CalendarConfig = CalendarConfig()
async def get_config() -> Config:
"""
Load the configuration instance from the server using `TOML`.
"""
try:
cfg_str = await WebDAV.read_str(SETTINGS.webdav.config_filename)
cfg = Config.model_validate(tomllib.loads(cfg_str))
except RemoteResourceNotFound:
_logger.warning(
f"Config file {SETTINGS.webdav.config_filename!r} not found, creating ..."
)
cfg = Config()
# cfg.calendar.aggregates["All Events"] = list(await CalDAV.calendars)
# await WebDAV.write_str(
# SETTINGS.webdav.config_filename,
# tomli_w.dumps(cfg.model_dump()),
# )
return cfg

View file

@ -2,14 +2,14 @@
Definition of WebDAV and CalDAV clients. Definition of WebDAV and CalDAV clients.
""" """
from logging import getLogger import logging
from os import path from os import path
from pathlib import Path from pathlib import Path
from .. import __file__ as OVD_INIT from .. import __file__ as OVD_INIT
from .webdav import WebDAV from .webdav import WebDAV
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
def webdav_ensure_path(remote_path: str) -> bool: def webdav_ensure_path(remote_path: str) -> bool:

View file

@ -2,10 +2,10 @@ import logging
import re import re
from io import BytesIO from io import BytesIO
import requests
from asyncify import asyncify from asyncify import asyncify
from cache import AsyncTTL from cache import AsyncTTL
from cache.key import KEY from cache.key import KEY
from requests import Response
from webdav3.client import Client as WebDAVclient from webdav3.client import Client as WebDAVclient
from .settings import SETTINGS from .settings import SETTINGS
@ -21,7 +21,7 @@ class WebDAV:
path, path,
data=None, data=None,
headers_ext=None, headers_ext=None,
) -> Response: ) -> requests.Response:
res = super().execute_request(action, path, data, headers_ext) res = super().execute_request(action, path, data, headers_ext)
# the "Content-Length" header can randomly be missing on txt files, # the "Content-Length" header can randomly be missing on txt files,

View file

@ -2,19 +2,21 @@
Dependables for defining Routers. Dependables for defining Routers.
""" """
import logging
import re import re
from dataclasses import dataclass, field import tomllib
from logging import getLogger
from typing import Awaitable, Callable, Generic, ParamSpec, Self, TypeVar
import tomli_w
from fastapi import Depends, HTTPException, params, status from fastapi import Depends, HTTPException, params, status
from webdav3.exceptions import RemoteResourceNotFound from webdav3.exceptions import RemoteResourceNotFound
from ...core.caldav import CalDAV from ...core.caldav import CalDAV
from ...core.config import Config, get_config from ...core.config import Config
from ...core.settings import SETTINGS
from ...core.webdav import WebDAV from ...core.webdav import WebDAV
from ._list_manager import Dependable, DependableFn, ListManager
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
_RESPONSE_OK = { _RESPONSE_OK = {
status.HTTP_200_OK: { status.HTTP_200_OK: {
@ -22,78 +24,30 @@ _RESPONSE_OK = {
}, },
} }
Params = ParamSpec("Params")
Return = TypeVar("Return")
type DependableFn[**Params, Return] = Callable[Params, Awaitable[Return]] async def get_config() -> Config:
"""
Load the configuration instance from the server using `TOML`.
"""
try:
cfg_str = await WebDAV.read_str(SETTINGS.webdav.config_filename)
cfg = Config.model_validate(tomllib.loads(cfg_str))
@dataclass(slots=True, frozen=True) except RemoteResourceNotFound:
class Dependable(Generic[Params, Return]): _logger.warning(
func: DependableFn[Params, Return] f"Config file {SETTINGS.webdav.config_filename!r} not found, creating ..."
responses: dict = field(default_factory=lambda: _RESPONSE_OK.copy())
@dataclass(slots=True, frozen=True)
class ListManager:
lister: Dependable[[], list[str]]
filter: Dependable[[str], list[str]]
getter: Dependable[[str], str]
@classmethod
def from_lister(cls, lister: Dependable[[], list[str]]) -> Self:
async def _filter_fn(
prefix: str,
names: list[str] = Depends(lister.func),
) -> list[str]:
if isinstance(names, params.Depends):
names = await lister.func()
_logger.debug("filter %s from %s", repr(prefix), repr(names))
return [item for item in names if item.lower().startswith(prefix.lower())]
async def _getter_fn(
prefix: str,
names: list[str] = Depends(_filter_fn),
) -> str:
if isinstance(names, params.Depends):
names = await _filter_fn(prefix)
_logger.debug("get %s from %s", repr(prefix), repr(names))
match names:
case [name]:
return name
case []:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
case _:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
return cls(
lister=lister,
filter=Dependable(_filter_fn),
getter=Dependable(
func=_getter_fn,
responses={
**_RESPONSE_OK,
status.HTTP_404_NOT_FOUND: {
"description": "Prefix not found",
"content": None,
},
status.HTTP_409_CONFLICT: {
"description": "Ambiguous prefix",
"content": None,
},
},
),
) )
@classmethod cfg = Config()
def from_lister_fn(cls, lister_fn: DependableFn[[], list[str]]) -> Self: cfg.calendar.aggregates["All Events"] = list(await CalDAV.calendars)
return cls.from_lister(Dependable(lister_fn))
await WebDAV.write_str(
SETTINGS.webdav.config_filename,
tomli_w.dumps(cfg.model_dump()),
)
return cfg
def get_remote_path( def get_remote_path(

View file

@ -0,0 +1,86 @@
import logging
from dataclasses import dataclass, field
from typing import Awaitable, Callable, Generic, ParamSpec, Self, TypeVar
from fastapi import Depends, HTTPException, params, status
_logger = logging.getLogger(__name__)
_RESPONSE_OK = {
status.HTTP_200_OK: {
"description": "Operation successful",
},
}
Params = ParamSpec("Params")
Return = TypeVar("Return")
type DependableFn[**Params, Return] = Callable[Params, Awaitable[Return]]
@dataclass(slots=True, frozen=True)
class Dependable(Generic[Params, Return]):
func: DependableFn[Params, Return]
responses: dict = field(default_factory=lambda: _RESPONSE_OK.copy())
@dataclass(slots=True, frozen=True)
class ListManager:
lister: Dependable[[], list[str]]
filter: Dependable[[str], list[str]]
getter: Dependable[[str], str]
@classmethod
def from_lister(cls, lister: Dependable[[], list[str]]) -> Self:
async def _filter_fn(
prefix: str,
names: list[str] = Depends(lister.func),
) -> list[str]:
if isinstance(names, params.Depends):
names = await lister.func()
_logger.debug("filter %s from %s", repr(prefix), repr(names))
return [item for item in names if item.lower().startswith(prefix.lower())]
async def _getter_fn(
prefix: str,
names: list[str] = Depends(_filter_fn),
) -> str:
if isinstance(names, params.Depends):
names = await _filter_fn(prefix)
_logger.debug("get %s from %s", repr(prefix), repr(names))
match names:
case [name]:
return name
case []:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
case _:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
return cls(
lister=lister,
filter=Dependable(_filter_fn),
getter=Dependable(
func=_getter_fn,
responses={
**_RESPONSE_OK,
status.HTTP_404_NOT_FOUND: {
"description": "Prefix not found",
"content": None,
},
status.HTTP_409_CONFLICT: {
"description": "Ambiguous prefix",
"content": None,
},
},
),
)
@classmethod
def from_lister_fn(cls, lister_fn: DependableFn[[], list[str]]) -> Self:
return cls.from_lister(Dependable(lister_fn))

View file

@ -6,17 +6,16 @@ Router "aggregate" provides:
- getting aggregate calendar events by name prefix - getting aggregate calendar events by name prefix
""" """
from logging import getLogger import logging
from typing import Iterator
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ...core.caldav import CalDAV from ...core.caldav import CalDAV
from ...core.calevent import CalEvent from ...core.calevent import CalEvent
from ...core.config import Config, get_config from ...core.config import Config
from ._common import LM_AGGREGATES, LM_CALENDARS from ._common import LM_AGGREGATES, LM_CALENDARS, get_config
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
router = APIRouter(prefix="/aggregate", tags=["calendar"]) router = APIRouter(prefix="/aggregate", tags=["calendar"])
@ -31,9 +30,9 @@ async def start_router() -> None:
responses=LM_AGGREGATES.lister.responses, responses=LM_AGGREGATES.lister.responses,
) )
async def list_aggregate_calendars( async def list_aggregate_calendars(
names: Iterator[str] = Depends(LM_AGGREGATES.lister.func), names: list[str] = Depends(LM_AGGREGATES.lister.func),
) -> list[str]: ) -> list[str]:
return list(names) return names
@router.get( @router.get(
@ -41,9 +40,9 @@ async def list_aggregate_calendars(
responses=LM_AGGREGATES.filter.responses, responses=LM_AGGREGATES.filter.responses,
) )
async def find_aggregate_calendars( async def find_aggregate_calendars(
names: Iterator[str] = Depends(LM_AGGREGATES.filter.func), names: list[str] = Depends(LM_AGGREGATES.filter.func),
) -> list[str]: ) -> list[str]:
return list(names) return names
@router.get( @router.get(

View file

@ -6,15 +6,15 @@ Router "calendar" provides:
- getting calendar events by calendar name prefix - getting calendar events by calendar name prefix
""" """
from logging import getLogger import logging
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ...core.caldav import CalDAV, CalEvent from ...core.caldav import CalDAV, CalEvent
from ...core.config import CalendarUIConfig, Config, get_config from ...core.config import CalendarUIConfig, Config
from ._common import LM_CALENDARS from ._common import LM_CALENDARS, get_config
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
router = APIRouter(prefix="/calendar", tags=["calendar"]) router = APIRouter(prefix="/calendar", tags=["calendar"])

View file

@ -6,8 +6,8 @@ Router "file" provides:
- getting files by name prefix - getting files by name prefix
""" """
import logging
from io import BytesIO from io import BytesIO
from logging import getLogger
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
@ -17,7 +17,7 @@ from ...core.dav_common import webdav_ensure_files, webdav_ensure_path
from ...core.webdav import WebDAV from ...core.webdav import WebDAV
from ._common import LM_FILE, RP_FILE from ._common import LM_FILE, RP_FILE
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
_magic = Magic(mime=True) _magic = Magic(mime=True)
router = APIRouter(prefix="/file", tags=["file"]) router = APIRouter(prefix="/file", tags=["file"])

View file

@ -6,20 +6,19 @@ Router "image" provides:
- getting image files in a uniform format by name prefix - getting image files in a uniform format by name prefix
""" """
import logging
from io import BytesIO from io import BytesIO
from logging import getLogger
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from PIL import Image from PIL import Image
from ...core.config import Config, ImageUIConfig, get_config from ...core.config import Config, ImageUIConfig
from ...core.dav_common import webdav_ensure_files, webdav_ensure_path from ...core.dav_common import webdav_ensure_files, webdav_ensure_path
from ...core.webdav import WebDAV from ...core.webdav import WebDAV
from ._common import LM_IMAGE, RP_IMAGE from ._common import LM_IMAGE, RP_IMAGE, get_config
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
_PATH_NAME = "image_dir"
router = APIRouter(prefix="/image", tags=["image"]) router = APIRouter(prefix="/image", tags=["image"])

View file

@ -5,16 +5,17 @@ Router "misc" provides:
- getting the device IP - getting the device IP
""" """
from importlib.metadata import version import importlib.metadata
from logging import getLogger import logging
from socket import AF_INET, SOCK_DGRAM, socket from socket import AF_INET, SOCK_DGRAM, socket
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ...core.config import Config, LogoUIConfig, ServerUIConfig, get_config from ...core.config import Config, LogoUIConfig, ServerUIConfig
from ...core.settings import SETTINGS from ...core.settings import SETTINGS
from ._common import get_config
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
router = APIRouter(prefix="/misc", tags=["misc"]) router = APIRouter(prefix="/misc", tags=["misc"])
@ -43,7 +44,7 @@ async def get_ip() -> str:
@router.get("/version") @router.get("/version")
async def get_version() -> str: async def get_version() -> str:
return version("ovdashboard-api") return importlib.metadata.version("ovdashboard-api")
@router.get("/config/server") @router.get("/config/server")

View file

@ -7,17 +7,16 @@ Router "text" provides:
- getting text file HTML content by name prefix (using Markdown) - getting text file HTML content by name prefix (using Markdown)
""" """
from logging import getLogger import logging
import markdown
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from markdown import markdown
from ...core.dav_common import webdav_ensure_files, webdav_ensure_path from ...core.dav_common import webdav_ensure_files, webdav_ensure_path
from ...core.webdav import WebDAV from ...core.webdav import WebDAV
from ._common import LM_TEXT, RP_TEXT from ._common import LM_TEXT, RP_TEXT
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
_PATH_NAME = "text_dir"
router = APIRouter(prefix="/text", tags=["text"]) router = APIRouter(prefix="/text", tags=["text"])
@ -80,4 +79,4 @@ async def get_raw_text_by_prefix(
async def get_html_by_prefix( async def get_html_by_prefix(
text: str = Depends(_get_raw_text_by_prefix), text: str = Depends(_get_raw_text_by_prefix),
) -> str: ) -> str:
return markdown(text) return markdown.markdown(text)

View file

@ -6,18 +6,18 @@ Router "ticker" provides:
- getting the ticker's UI config - getting the ticker's UI config
""" """
from logging import getLogger import logging
from typing import Iterator from typing import Iterator
import markdown
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from markdown import markdown
from ...core.config import Config, TickerUIConfig, get_config from ...core.config import Config, TickerUIConfig
from ...core.dav_common import webdav_ensure_files, webdav_ensure_path from ...core.dav_common import webdav_ensure_files, webdav_ensure_path
from ...core.webdav import WebDAV from ...core.webdav import WebDAV
from ._common import LM_TEXT, RP_TEXT from ._common import LM_TEXT, RP_TEXT, get_config
_logger = getLogger(__name__) _logger = logging.getLogger(__name__)
router = APIRouter(prefix="/ticker", tags=["text"]) router = APIRouter(prefix="/ticker", tags=["text"])
@ -73,7 +73,7 @@ async def get_ticker_content(
async def get_ticker( async def get_ticker(
ticker_content: str = Depends(get_ticker_content), ticker_content: str = Depends(get_ticker_content),
) -> str: ) -> str:
return markdown(ticker_content) return markdown.markdown(ticker_content)
@router.get("/raw") @router.get("/raw")