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`.
"""
import functools
import logging
from datetime import datetime
from functools import total_ordering
from logging import getLogger
from typing import Annotated, Self
from pydantic import BaseModel, ConfigDict, StringConstraints
from vobject.base import Component
_logger = getLogger(__name__)
_logger = logging.getLogger(__name__)
type StrippedStr = Annotated[str, StringConstraints(strip_whitespace=True)]
@total_ordering
@functools.total_ordering
class CalEvent(BaseModel):
"""
A CalDAV calendar event.

View file

@ -2,18 +2,12 @@
Python representation of the "config.txt" file inside the WebDAV directory.
"""
import tomllib
from logging import getLogger
import logging
from typing import Any
from pydantic import BaseModel
from webdav3.exceptions import RemoteResourceNotFound
# from .caldav import CalDAV
from .settings import SETTINGS
from .webdav import WebDAV
_logger = getLogger(__name__)
_logger = logging.getLogger(__name__)
class TickerUIConfig(BaseModel):
@ -107,28 +101,3 @@ class Config(BaseModel):
server: ServerUIConfig = ServerUIConfig()
ticker: TickerConfig = TickerConfig()
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.
"""
from logging import getLogger
import logging
from os import path
from pathlib import Path
from .. import __file__ as OVD_INIT
from .webdav import WebDAV
_logger = getLogger(__name__)
_logger = logging.getLogger(__name__)
def webdav_ensure_path(remote_path: str) -> bool:

View file

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

View file

@ -2,19 +2,21 @@
Dependables for defining Routers.
"""
import logging
import re
from dataclasses import dataclass, field
from logging import getLogger
from typing import Awaitable, Callable, Generic, ParamSpec, Self, TypeVar
import tomllib
import tomli_w
from fastapi import Depends, HTTPException, params, status
from webdav3.exceptions import RemoteResourceNotFound
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 ._list_manager import Dependable, DependableFn, ListManager
_logger = getLogger(__name__)
_logger = logging.getLogger(__name__)
_RESPONSE_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)
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,
},
},
),
except RemoteResourceNotFound:
_logger.warning(
f"Config file {SETTINGS.webdav.config_filename!r} not found, creating ..."
)
@classmethod
def from_lister_fn(cls, lister_fn: DependableFn[[], list[str]]) -> Self:
return cls.from_lister(Dependable(lister_fn))
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
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
"""
from logging import getLogger
from typing import Iterator
import logging
from fastapi import APIRouter, Depends
from ...core.caldav import CalDAV
from ...core.calevent import CalEvent
from ...core.config import Config, get_config
from ._common import LM_AGGREGATES, LM_CALENDARS
from ...core.config import Config
from ._common import LM_AGGREGATES, LM_CALENDARS, get_config
_logger = getLogger(__name__)
_logger = logging.getLogger(__name__)
router = APIRouter(prefix="/aggregate", tags=["calendar"])
@ -31,9 +30,9 @@ async def start_router() -> None:
responses=LM_AGGREGATES.lister.responses,
)
async def list_aggregate_calendars(
names: Iterator[str] = Depends(LM_AGGREGATES.lister.func),
names: list[str] = Depends(LM_AGGREGATES.lister.func),
) -> list[str]:
return list(names)
return names
@router.get(
@ -41,9 +40,9 @@ async def list_aggregate_calendars(
responses=LM_AGGREGATES.filter.responses,
)
async def find_aggregate_calendars(
names: Iterator[str] = Depends(LM_AGGREGATES.filter.func),
names: list[str] = Depends(LM_AGGREGATES.filter.func),
) -> list[str]:
return list(names)
return names
@router.get(

View file

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

View file

@ -6,8 +6,8 @@ Router "file" provides:
- getting files by name prefix
"""
import logging
from io import BytesIO
from logging import getLogger
from fastapi import APIRouter, Depends
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 ._common import LM_FILE, RP_FILE
_logger = getLogger(__name__)
_logger = logging.getLogger(__name__)
_magic = Magic(mime=True)
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
"""
import logging
from io import BytesIO
from logging import getLogger
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
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.webdav import WebDAV
from ._common import LM_IMAGE, RP_IMAGE
from ._common import LM_IMAGE, RP_IMAGE, get_config
_logger = getLogger(__name__)
_PATH_NAME = "image_dir"
_logger = logging.getLogger(__name__)
router = APIRouter(prefix="/image", tags=["image"])

View file

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

View file

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

View file

@ -6,18 +6,18 @@ Router "ticker" provides:
- getting the ticker's UI config
"""
from logging import getLogger
import logging
from typing import Iterator
import markdown
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.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"])
@ -73,7 +73,7 @@ async def get_ticker_content(
async def get_ticker(
ticker_content: str = Depends(get_ticker_content),
) -> str:
return markdown(ticker_content)
return markdown.markdown(ticker_content)
@router.get("/raw")