Compare commits

..

No commits in common. "9551ee7729a1d7e577ab9e23393afafb74f0bb5e" and "b6798df29c14eaaa5e329e043630ce1d453519ae" have entirely different histories.

13 changed files with 149 additions and 156 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 = logging.getLogger(__name__)
_logger = getLogger(__name__)
type StrippedStr = Annotated[str, StringConstraints(strip_whitespace=True)]
@functools.total_ordering
@total_ordering
class CalEvent(BaseModel):
"""
A CalDAV calendar event.

View file

@ -2,12 +2,18 @@
Python representation of the "config.txt" file inside the WebDAV directory.
"""
import logging
import tomllib
from logging import getLogger
from typing import Any
from pydantic import BaseModel
from webdav3.exceptions import RemoteResourceNotFound
_logger = logging.getLogger(__name__)
# from .caldav import CalDAV
from .settings import SETTINGS
from .webdav import WebDAV
_logger = getLogger(__name__)
class TickerUIConfig(BaseModel):
@ -101,3 +107,28 @@ 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.
"""
import logging
from logging import getLogger
from os import path
from pathlib import Path
from .. import __file__ as OVD_INIT
from .webdav import WebDAV
_logger = logging.getLogger(__name__)
_logger = 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,
) -> requests.Response:
) -> Response:
res = super().execute_request(action, path, data, headers_ext)
# the "Content-Length" header can randomly be missing on txt files,

View file

@ -2,21 +2,19 @@
Dependables for defining Routers.
"""
import logging
import re
import tomllib
from dataclasses import dataclass, field
from logging import getLogger
from typing import Awaitable, Callable, Generic, ParamSpec, Self, TypeVar
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
from ...core.settings import SETTINGS
from ...core.config import Config, get_config
from ...core.webdav import WebDAV
from ._list_manager import Dependable, DependableFn, ListManager
_logger = logging.getLogger(__name__)
_logger = getLogger(__name__)
_RESPONSE_OK = {
status.HTTP_200_OK: {
@ -24,30 +22,78 @@ _RESPONSE_OK = {
},
}
Params = ParamSpec("Params")
Return = TypeVar("Return")
async def get_config() -> Config:
"""
Load the configuration instance from the server using `TOML`.
"""
type DependableFn[**Params, Return] = Callable[Params, Awaitable[Return]]
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 ..."
@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,
},
},
),
)
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
@classmethod
def from_lister_fn(cls, lister_fn: DependableFn[[], list[str]]) -> Self:
return cls.from_lister(Dependable(lister_fn))
def get_remote_path(

View file

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

View file

@ -6,15 +6,15 @@ Router "calendar" provides:
- getting calendar events by calendar name prefix
"""
import logging
from logging import getLogger
from fastapi import APIRouter, Depends
from ...core.caldav import CalDAV, CalEvent
from ...core.config import CalendarUIConfig, Config
from ._common import LM_CALENDARS, get_config
from ...core.config import CalendarUIConfig, Config, get_config
from ._common import LM_CALENDARS
_logger = logging.getLogger(__name__)
_logger = 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 = logging.getLogger(__name__)
_logger = getLogger(__name__)
_magic = Magic(mime=True)
router = APIRouter(prefix="/file", tags=["file"])

View file

@ -6,19 +6,20 @@ 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
from ...core.config import Config, ImageUIConfig, get_config
from ...core.dav_common import webdav_ensure_files, webdav_ensure_path
from ...core.webdav import WebDAV
from ._common import LM_IMAGE, RP_IMAGE, get_config
from ._common import LM_IMAGE, RP_IMAGE
_logger = logging.getLogger(__name__)
_logger = getLogger(__name__)
_PATH_NAME = "image_dir"
router = APIRouter(prefix="/image", tags=["image"])

View file

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

View file

@ -7,16 +7,17 @@ Router "text" provides:
- getting text file HTML content by name prefix (using Markdown)
"""
import logging
from logging import getLogger
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 = logging.getLogger(__name__)
_logger = getLogger(__name__)
_PATH_NAME = "text_dir"
router = APIRouter(prefix="/text", tags=["text"])
@ -79,4 +80,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.markdown(text)
return markdown(text)

View file

@ -6,18 +6,18 @@ Router "ticker" provides:
- getting the ticker's UI config
"""
import logging
from logging import getLogger
from typing import Iterator
import markdown
from fastapi import APIRouter, Depends
from markdown import markdown
from ...core.config import Config, TickerUIConfig
from ...core.config import Config, TickerUIConfig, get_config
from ...core.dav_common import webdav_ensure_files, webdav_ensure_path
from ...core.webdav import WebDAV
from ._common import LM_TEXT, RP_TEXT, get_config
from ._common import LM_TEXT, RP_TEXT
_logger = logging.getLogger(__name__)
_logger = 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.markdown(ticker_content)
return markdown(ticker_content)
@router.get("/raw")