diff --git a/api/ovdashboard_api/__init__.py b/api/ovdashboard_api/__init__.py index 143645b..754f205 100644 --- a/api/ovdashboard_api/__init__.py +++ b/api/ovdashboard_api/__init__.py @@ -7,40 +7,34 @@ This file: Sets up logging. from logging.config import dictConfig -from pydantic import BaseModel - from .core.settings import SETTINGS +# Logging configuration to be set for the server. +# https://stackoverflow.com/a/67937084 -class LogConfig(BaseModel): - """ - Logging configuration to be set for the server. - https://stackoverflow.com/a/67937084 - """ - - # Logging config - version: int = 1 - disable_existing_loggers: bool = False - formatters: dict = { +LOG_CONFIG = dict( + version=1, + disable_existing_loggers=False, + formatters={ "default": { "()": "uvicorn.logging.DefaultFormatter", "fmt": "%(levelprefix)s [%(asctime)s] %(name)s: %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S", }, - } - handlers: dict = { + }, + handlers={ "default": { "formatter": "default", "class": "logging.StreamHandler", "stream": "ext://sys.stderr", }, - } - loggers: dict = { + }, + loggers={ "ovdashboard_api": { "handlers": ["default"], "level": SETTINGS.log_level, }, - } + }, +) - -dictConfig(LogConfig().model_dump()) +dictConfig(LOG_CONFIG) diff --git a/api/ovdashboard_api/app.py b/api/ovdashboard_api/app.py index 5b27a66..1ee443f 100644 --- a/api/ovdashboard_api/app.py +++ b/api/ovdashboard_api/app.py @@ -6,14 +6,19 @@ Main script for `ovdashboard_api` module. Creates the main `FastAPI` app. """ +import logging +import time + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from .core.settings import SETTINGS -from .dav_common import webdav_check +from .core.webdav import WebDAV from .routers import v1_router +_logger = logging.getLogger(__name__) + app = FastAPI( title="OVDashboard API", description="This API enables the `OVDashboard` service.", @@ -31,7 +36,11 @@ app = FastAPI( ) app.include_router(v1_router) -webdav_check() + +_logger.info( + "Production mode is %s.", + "enabled" if SETTINGS.production_mode else "disabled", +) if SETTINGS.production_mode: # Mount frontend in production mode @@ -44,15 +53,27 @@ if SETTINGS.production_mode: name="frontend", ) + for _ in range(SETTINGS.webdav.retries): + if WebDAV._webdav_client.check(""): + break + + _logger.warning( + "Waiting for WebDAV connection to %s ...", + repr(SETTINGS.webdav.url), + ) + time.sleep(30) + else: + assert WebDAV._webdav_client.check("") + # Allow CORS in debug mode app.add_middleware( CORSMiddleware, - allow_origins=[ - "*", - ], allow_credentials=True, - allow_methods=["*"], allow_headers=["*"], + allow_methods=["*"], + allow_origins=["*"], expose_headers=["*"], ) + +_logger.debug("WebDAV connection ok.") diff --git a/api/ovdashboard_api/core/config.py b/api/ovdashboard_api/core/config.py index 604b56f..37001fe 100644 --- a/api/ovdashboard_api/core/config.py +++ b/api/ovdashboard_api/core/config.py @@ -4,13 +4,12 @@ Python representation of the "config.txt" file inside the WebDAV directory. import tomllib from logging import getLogger -from typing import Any, Self +from typing import Any -import tomli_w from pydantic import BaseModel from webdav3.exceptions import RemoteResourceNotFound -from .caldav import CalDAV +# from .caldav import CalDAV from .settings import SETTINGS from .webdav import WebDAV @@ -109,27 +108,27 @@ class Config(BaseModel): ticker: TickerConfig = TickerConfig() calendar: CalendarConfig = CalendarConfig() - @classmethod - async def get(cls) -> Self: - """ - Load the configuration instance from the server using `TOML`. - """ - try: - cfg_str = await WebDAV.read_str(SETTINGS.webdav.config_filename) - cfg = cls.model_validate(tomllib.loads(cfg_str)) +async def get_config() -> Config: + """ + Load the configuration instance from the server using `TOML`. + """ - except RemoteResourceNotFound: - _logger.warning( - f"Config file {SETTINGS.webdav.config_filename!r} not found, creating ..." - ) + try: + cfg_str = await WebDAV.read_str(SETTINGS.webdav.config_filename) + cfg = Config.model_validate(tomllib.loads(cfg_str)) - cfg = cls() - cfg.calendar.aggregates["All Events"] = list(await CalDAV.calendars) + except RemoteResourceNotFound: + _logger.warning( + f"Config file {SETTINGS.webdav.config_filename!r} not found, creating ..." + ) - await WebDAV.write_str( - SETTINGS.webdav.config_filename, - tomli_w.dumps(cfg.model_dump()), - ) + cfg = Config() + # cfg.calendar.aggregates["All Events"] = list(await CalDAV.calendars) - return cfg + # await WebDAV.write_str( + # SETTINGS.webdav.config_filename, + # tomli_w.dumps(cfg.model_dump()), + # ) + + return cfg diff --git a/api/ovdashboard_api/core/dav_common.py b/api/ovdashboard_api/core/dav_common.py new file mode 100644 index 0000000..f25f65a --- /dev/null +++ b/api/ovdashboard_api/core/dav_common.py @@ -0,0 +1,60 @@ +""" +Definition of WebDAV and CalDAV clients. +""" + +from logging import getLogger +from os import path +from pathlib import Path + +from .. import __file__ as OVD_INIT +from .webdav import WebDAV + +_logger = getLogger(__name__) + + +def webdav_ensure_path(remote_path: str) -> bool: + if WebDAV._webdav_client.check(remote_path): + _logger.debug( + "WebDAV path %s found.", + repr(remote_path), + ) + return True + + _logger.info( + "WebDAV path %s not found, creating ...", + repr(remote_path), + ) + WebDAV._webdav_client.mkdir(remote_path) + + return False + + +def get_skel_path(skel_file: str) -> Path: + skel_path = path.dirname(Path(OVD_INIT).absolute()) + return Path(skel_path).joinpath("skel", skel_file) + + +def webdav_upload_skel(remote_path: str, *skel_files: str) -> None: + for skel_file in skel_files: + _logger.debug( + "Creating WebDAV file %s ...", + repr(skel_file), + ) + + WebDAV._webdav_client.upload_file( + f"{remote_path}/{skel_file}", + get_skel_path(skel_file), + ) + + +def webdav_ensure_files(remote_path: str, *file_names: str) -> None: + missing_files = ( + file_name + for file_name in file_names + if not WebDAV._webdav_client.check(f"{remote_path}/{file_name}") + ) + + webdav_upload_skel( + remote_path, + *missing_files, + ) diff --git a/api/ovdashboard_api/dav_common.py b/api/ovdashboard_api/dav_common.py deleted file mode 100644 index 326cdab..0000000 --- a/api/ovdashboard_api/dav_common.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -Definition of WebDAV and CalDAV clients. -""" - -from functools import lru_cache -from logging import getLogger -from os import path -from pathlib import Path -from time import sleep -from typing import Any, Iterator - -from asyncify import asyncify -from caldav import DAVClient as CalDAVclient -from caldav import Principal as CalDAVPrincipal -from webdav3.client import Client as WebDAVclient -from webdav3.client import Resource as WebDAVResource - -from . import __file__ as OVD_INIT -from .core.settings import SETTINGS - -_WEBDAV_CLIENT = WebDAVclient( - { - "webdav_hostname": SETTINGS.webdav.url, - "webdav_login": SETTINGS.webdav.username, - "webdav_password": SETTINGS.webdav.password, - "disable_check": SETTINGS.webdav_disable_check, - } -) - -_logger = getLogger(__name__) - - -def webdav_check() -> None: - """ - Checks if base resources are available. - """ - - _logger.info( - "Production mode is %s.", - "enabled" if SETTINGS.production_mode else "disabled", - ) - - if SETTINGS.production_mode: - for _ in range(SETTINGS.webdav.retries): - if _WEBDAV_CLIENT.check(""): - break - - _logger.warning( - "Waiting for WebDAV connection to %s ...", - repr(SETTINGS.webdav.url), - ) - sleep(30) - - _logger.debug("WebDAV connection ok.") - - elif not _WEBDAV_CLIENT.check(""): - _logger.error( - "WebDAV connection to %s FAILED!", - repr(SETTINGS.webdav.url), - ) - raise ConnectionError(SETTINGS.webdav.url) - - _logger.debug("WebDAV connection ok.") - - if not _WEBDAV_CLIENT.check(SETTINGS.webdav_prefix): - _logger.error( - "WebDAV prefix directory %s NOT FOUND, please create it!", - repr(SETTINGS.webdav_prefix), - ) - raise FileNotFoundError(SETTINGS.webdav_prefix) - - _logger.debug("WebDAV prefix directory found.") - - -def webdav_ensure_path(remote_path: str) -> bool: - remote_path = f"{SETTINGS.webdav_prefix}/{remote_path}" - - if _WEBDAV_CLIENT.check(remote_path): - _logger.debug( - "WebDAV path %s found.", - repr(remote_path), - ) - return True - - _logger.info( - "WebDAV path %s not found, creating ...", - repr(remote_path), - ) - _WEBDAV_CLIENT.mkdir(remote_path) - - return False - - -def get_skel_path(skel_file: str) -> Path: - skel_path = path.dirname(Path(OVD_INIT).absolute()) - return Path(skel_path).joinpath("skel", skel_file) - - -def webdav_upload_skel(remote_path: str, *skel_files: str) -> None: - remote_path = f"{SETTINGS.webdav_prefix}/{remote_path}" - - for skel_file in skel_files: - _logger.debug( - "Creating WebDAV file %s ...", - repr(skel_file), - ) - - _WEBDAV_CLIENT.upload_file( - f"{remote_path}/{skel_file}", - get_skel_path(skel_file), - ) - - -def webdav_ensure_files(remote_path: str, *file_names: str) -> None: - missing_files = ( - file_name - for file_name in file_names - if not _WEBDAV_CLIENT.check( - path.join( - SETTINGS.webdav_prefix, - remote_path, - file_name, - ) - ) - ) - - webdav_upload_skel( - remote_path, - *missing_files, - ) - - -@lru_cache(maxsize=SETTINGS.cache_size) -def webdav_resource(remote_path: Any) -> WebDAVResource: - """ - Gets a resource using the main WebDAV client. - """ - - return _WEBDAV_CLIENT.resource(f"{SETTINGS.webdav_prefix}/{remote_path}") - - -@asyncify -def webdav_list(remote_path: str) -> list[str]: - """ - Asynchronously lists a WebDAV path using the main WebDAV client. - """ - - return _WEBDAV_CLIENT.list(f"{SETTINGS.webdav_prefix}/{remote_path}") - - -_CALDAV_CLIENT = CalDAVclient( - url=SETTINGS.caldav.url, - username=SETTINGS.caldav.username, - password=SETTINGS.caldav.password, -) - - -def caldav_principal() -> CalDAVPrincipal: - """ - Gets the `Principal` object of the main CalDAV client. - """ - - return _CALDAV_CLIENT.principal() - - -@asyncify -def caldav_list() -> Iterator[str]: - """ - Asynchronously lists all calendars using the main WebDAV client. - """ - - return (str(cal.name) for cal in caldav_principal().calendars()) diff --git a/api/ovdashboard_api/routers/v1/__init__.py b/api/ovdashboard_api/routers/v1/__init__.py index b25bb2f..38f8300 100644 --- a/api/ovdashboard_api/routers/v1/__init__.py +++ b/api/ovdashboard_api/routers/v1/__init__.py @@ -6,16 +6,19 @@ This file: Main API router definition. from fastapi import APIRouter -from . import aggregate, calendar, file, image, misc, text, ticker +# from . import aggregate, calendar, file, image, misc, text, ticker +from . import file, misc router = APIRouter(prefix="/api/v1") router.include_router(misc.router) -router.include_router(text.router) -router.include_router(ticker.router) -router.include_router(image.router) +# router.include_router(text.router) +# router.include_router(ticker.router) +# router.include_router(image.router) router.include_router(file.router) -router.include_router(calendar.router) -router.include_router(aggregate.router) +# router.include_router(calendar.router) +# router.include_router(aggregate.router) + +__all__ = ["router"] diff --git a/api/ovdashboard_api/routers/v1/_common.py b/api/ovdashboard_api/routers/v1/_common.py index efd2435..f96cb28 100644 --- a/api/ovdashboard_api/routers/v1/_common.py +++ b/api/ovdashboard_api/routers/v1/_common.py @@ -10,8 +10,9 @@ from typing import Iterator, Protocol from fastapi import HTTPException, status from webdav3.exceptions import RemoteResourceNotFound -from ...config import Config -from ...dav_common import caldav_list, webdav_list +from ...core.caldav import CalDAV +from ...core.config import get_config +from ...core.webdav import WebDAV _logger = getLogger(__name__) @@ -32,7 +33,7 @@ _RESPONSE_OK = { } -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class FileNameLister: """ Can be called to create an iterator containing file names. @@ -55,13 +56,13 @@ class FileNameLister: @property async def remote_path(self) -> str: - cfg = await Config.get() + cfg = await get_config() - return str(cfg.model_dump()[self.path_name]) + return getattr(cfg, self.path_name) async def __call__(self) -> Iterator[str]: try: - file_names = await webdav_list(await self.remote_path) + file_names = await WebDAV.list_files(await self.remote_path) return (name for name in file_names if self.re.search(name)) @@ -73,29 +74,29 @@ class FileNameLister: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class CalendarNameLister: """ Can be called to create an iterator containing calendar names. """ async def __call__(self) -> Iterator[str]: - return await caldav_list() + return iter(await CalDAV.calendars) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class AggregateNameLister: """ Can be called to create an iterator containing aggregate calendar names. """ async def __call__(self) -> Iterator[str]: - cfg = await Config.get() + cfg = await get_config() return iter(cfg.calendar.aggregates.keys()) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class PrefixFinder: """ Can be called to create an iterator containing some names, all starting @@ -124,7 +125,7 @@ class PrefixFinder: ) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class PrefixUnique: """ Can be called to determine if a given prefix is unique in the list diff --git a/api/ovdashboard_api/routers/v1/aggregate.py b/api/ovdashboard_api/routers/v1/aggregate.py index a07403c..6021d95 100644 --- a/api/ovdashboard_api/routers/v1/aggregate.py +++ b/api/ovdashboard_api/routers/v1/aggregate.py @@ -11,9 +11,9 @@ from typing import Iterator from fastapi import APIRouter, Depends -from ovdashboard_api.config import Config - -from ...dav_calendar import CalEvent, DavCalendar +from ...core.caldav import CalDAV +from ...core.calevent import CalEvent +from ...core.config import Config from ._common import AggregateNameLister, PrefixFinder, PrefixUnique from .calendar import calendar_unique diff --git a/api/ovdashboard_api/routers/v1/file.py b/api/ovdashboard_api/routers/v1/file.py index 10418bd..b49362e 100644 --- a/api/ovdashboard_api/routers/v1/file.py +++ b/api/ovdashboard_api/routers/v1/file.py @@ -15,8 +15,8 @@ from fastapi import APIRouter, Depends from fastapi.responses import StreamingResponse from magic import Magic -from ...dav_common import webdav_ensure_files, webdav_ensure_path -from ...dav_file import DavFile +# from ...core.dav_common import webdav_ensure_files, webdav_ensure_path +from ...core.webdav import WebDAV from ._common import FileNameLister, PrefixFinder, PrefixUnique _logger = getLogger(__name__) @@ -40,12 +40,12 @@ file_unique = PrefixUnique(file_finder) async def start_router() -> None: _logger.debug(f"{router.prefix} router starting.") - if not webdav_ensure_path(await file_lister.remote_path): - webdav_ensure_files( - await file_lister.remote_path, - "logo.svg", - "thw.svg", - ) + # if not webdav_ensure_path(await file_lister.remote_path): + # webdav_ensure_files( + # await file_lister.remote_path, + # "logo.svg", + # "thw.svg", + # ) @router.get( @@ -79,8 +79,7 @@ async def get_file( prefix: str, name: str = Depends(file_unique), ) -> StreamingResponse: - dav_file = DavFile(f"{await file_lister.remote_path}/{name}") - buffer = BytesIO(await dav_file.as_bytes) + buffer = BytesIO(await WebDAV.read_bytes(f"{await file_lister.remote_path}/{name}")) mime = _magic.from_buffer(buffer.read(2048)) buffer.seek(0) diff --git a/api/ovdashboard_api/routers/v1/misc.py b/api/ovdashboard_api/routers/v1/misc.py index cd89130..0657f18 100644 --- a/api/ovdashboard_api/routers/v1/misc.py +++ b/api/ovdashboard_api/routers/v1/misc.py @@ -11,8 +11,8 @@ from socket import AF_INET, SOCK_DGRAM, socket from fastapi import APIRouter, Depends -from ...config import Config, LogoUIConfig, ServerUIConfig -from ...settings import SETTINGS +from ...core.config import Config, LogoUIConfig, ServerUIConfig, get_config +from ...core.settings import SETTINGS _logger = getLogger(__name__) @@ -51,7 +51,7 @@ async def get_version() -> str: response_model=ServerUIConfig, ) async def get_server_ui_config( - cfg: Config = Depends(Config.get), + cfg: Config = Depends(get_config), ) -> ServerUIConfig: return cfg.server @@ -61,6 +61,6 @@ async def get_server_ui_config( response_model=LogoUIConfig, ) async def get_logo_ui_config( - cfg: Config = Depends(Config.get), + cfg: Config = Depends(get_config), ) -> LogoUIConfig: return cfg.logo