diff --git a/api/.vscode/launch.json b/api/.vscode/launch.json index 924c9c3..3fddcaa 100644 --- a/api/.vscode/launch.json +++ b/api/.vscode/launch.json @@ -21,7 +21,7 @@ "${workspaceFolder}/advent22_api" ], "env": { - "ADVENT22__WEBDAV__CACHE_TTL": "30" + "ADVENT22__REDIS__CACHE_TTL": "30" }, "justMyCode": true } diff --git a/api/advent22_api/core/calendar_config.py b/api/advent22_api/core/calendar_config.py index 8745232..0939d78 100644 --- a/api/advent22_api/core/calendar_config.py +++ b/api/advent22_api/core/calendar_config.py @@ -5,7 +5,7 @@ from fastapi import Depends from pydantic import BaseModel from .config import Config, get_config -from .dav.webdav import WebDAV +from .settings import WEBDAV class DoorSaved(BaseModel): @@ -37,7 +37,7 @@ class CalendarConfig(BaseModel): Kalender Konfiguration ändern """ - await WebDAV.write_str( + await WEBDAV.write_str( path=f"files/{cfg.calendar}", content=tomli_w.dumps(self.model_dump()), ) @@ -50,5 +50,5 @@ async def get_calendar_config( Kalender Konfiguration lesen """ - txt = await WebDAV.read_str(path=f"files/{cfg.calendar}") + txt = await WEBDAV.read_str(path=f"files/{cfg.calendar}") return CalendarConfig.model_validate(tomllib.loads(txt)) diff --git a/api/advent22_api/core/config.py b/api/advent22_api/core/config.py index 03c1730..f4c6b94 100644 --- a/api/advent22_api/core/config.py +++ b/api/advent22_api/core/config.py @@ -3,8 +3,7 @@ import tomllib from markdown import markdown from pydantic import BaseModel, ConfigDict, field_validator -from .dav.webdav import WebDAV -from .settings import SETTINGS, Credentials +from .settings import SETTINGS, WEBDAV, Credentials from .transformed_string import TransformedString @@ -77,5 +76,5 @@ async def get_config() -> Config: Globale Konfiguration lesen """ - txt = await WebDAV.read_str(path=SETTINGS.webdav.config_filename) + txt = await WEBDAV.read_str(path=SETTINGS.webdav.config_filename) return Config.model_validate(tomllib.loads(txt)) diff --git a/api/advent22_api/core/dav/helpers.py b/api/advent22_api/core/dav/helpers.py index e3e728a..c089003 100644 --- a/api/advent22_api/core/dav/helpers.py +++ b/api/advent22_api/core/dav/helpers.py @@ -1,8 +1,8 @@ +from itertools import chain from json import JSONDecodeError -from typing import Callable, Hashable +from typing import Any, Callable import requests -from cachetools.keys import hashkey from CacheToolsUtils import RedisCache as __RedisCache from redis.typing import EncodableT, ResponseT from webdav3.client import Client as __WebDAVclient @@ -11,12 +11,18 @@ from webdav3.client import Client as __WebDAVclient def davkey( name: str, slice: slice = slice(1, None), -) -> Callable[..., tuple[Hashable, ...]]: - def func(*args, **kwargs) -> tuple[Hashable, ...]: +) -> Callable[..., str]: + def func(*args: Any, **kwargs: Any) -> str: """Return a cache key for use with cached methods.""" - key = hashkey(name, *args[slice], **kwargs) - return hashkey(*(str(key_item) for key_item in key)) + call_args = chain( + # positional args + (f"{arg!r}" for arg in args[slice]), + # keyword args + (f"{k}:{v!r}" for k, v in kwargs.items()), + ) + + return f"{name}({', '.join(call_args)})" return func diff --git a/api/advent22_api/core/dav/webdav.py b/api/advent22_api/core/dav/webdav.py index a5d8b67..26125be 100644 --- a/api/advent22_api/core/dav/webdav.py +++ b/api/advent22_api/core/dav/webdav.py @@ -1,108 +1,113 @@ import logging import re +from dataclasses import dataclass from io import BytesIO from asyncify import asyncify from cachetools import cachedmethod from redis import Redis -from ..settings import SETTINGS from .helpers import RedisCache, WebDAVclient, davkey _logger = logging.getLogger(__name__) +@dataclass(kw_only=True, frozen=True, slots=True) +class Settings: + url: str + username: str = "johndoe" + password: str = "s3cr3t!" + + class WebDAV: - _webdav_client = WebDAVclient( - { - "webdav_hostname": SETTINGS.webdav.url, - "webdav_login": SETTINGS.webdav.auth.username, - "webdav_password": SETTINGS.webdav.auth.password, - } - ) + _webdav_client: WebDAVclient + _cache: RedisCache - _cache = RedisCache( - cache=Redis( - host=SETTINGS.redis.host, - port=SETTINGS.redis.port, - db=SETTINGS.redis.db, - protocol=SETTINGS.redis.protocol, - ), - ttl=SETTINGS.webdav.cache_ttl, - ) + def __init__(self, settings: Settings, redis: Redis, ttl_sec: int) -> None: + try: + self._webdav_client = WebDAVclient( + { + "webdav_hostname": settings.url, + "webdav_login": settings.username, + "webdav_password": settings.password, + } + ) + assert self._webdav_client.check() is True + + except AssertionError: + raise RuntimeError("WebDAV connection failed!") + + self._cache = RedisCache(cache=redis, ttl=ttl_sec) - @classmethod @asyncify - @cachedmethod(cache=lambda cls: cls._cache, key=davkey("list_files")) - def list_files( - cls, - directory: str = "", - *, - regex: re.Pattern[str] = re.compile(""), - ) -> list[str]: + @cachedmethod(cache=lambda self: self._cache, key=davkey("list_files")) + def _list_files(self, directory: str = "") -> list[str]: """ List files in directory `directory` matching RegEx `regex` """ - _logger.debug(f"list_files {directory!r}") - ls = cls._webdav_client.list(directory) + return self._webdav_client.list(directory) + async def list_files( + self, + directory: str = "", + *, + regex: re.Pattern[str] = re.compile(""), + ) -> list[str]: + _logger.debug(f"list_files {directory!r} ({regex!r})") + + ls = await self._list_files(directory) return [path for path in ls if regex.search(path)] - @classmethod @asyncify - @cachedmethod(cache=lambda cls: cls._cache, key=davkey("exists")) - def exists(cls, path: str) -> bool: + @cachedmethod(cache=lambda self: self._cache, key=davkey("exists")) + def exists(self, path: str) -> bool: """ `True` iff there is a WebDAV resource at `path` """ _logger.debug(f"file_exists {path!r}") - return cls._webdav_client.check(path) + return self._webdav_client.check(path) - @classmethod @asyncify - @cachedmethod(cache=lambda cls: cls._cache, key=davkey("read_bytes")) - def read_bytes(cls, path: str) -> bytes: + @cachedmethod(cache=lambda self: self._cache, key=davkey("read_bytes")) + def read_bytes(self, path: str) -> bytes: """ Load WebDAV file from `path` as bytes """ _logger.debug(f"read_bytes {path!r}") buffer = BytesIO() - cls._webdav_client.download_from(buffer, path) + self._webdav_client.download_from(buffer, path) buffer.seek(0) return buffer.read() - @classmethod - async def read_str(cls, path: str, encoding="utf-8") -> str: + async def read_str(self, path: str, encoding="utf-8") -> str: """ Load WebDAV file from `path` as string """ _logger.debug(f"read_str {path!r}") - return (await cls.read_bytes(path)).decode(encoding=encoding).strip() + return (await self.read_bytes(path)).decode(encoding=encoding).strip() - @classmethod @asyncify - def write_bytes(cls, path: str, buffer: bytes) -> None: + def write_bytes(self, path: str, buffer: bytes) -> None: """ Write bytes from `buffer` into WebDAV file at `path` """ _logger.debug(f"write_bytes {path!r}") - cls._webdav_client.upload_to(buffer, path) + self._webdav_client.upload_to(buffer, path) # invalidate cache entry - # explicit slice as there is no "cls" argument - del cls._cache[davkey("read_bytes", slice(0, None))(path)] + # begin slice at 0 (there is no "self" argument) + del self._cache[davkey("read_bytes", slice(0, None))(path)] - @classmethod - async def write_str(cls, path: str, content: str, encoding="utf-8") -> None: + async def write_str(self, path: str, content: str, encoding="utf-8") -> None: """ Write string from `content` into WebDAV file at `path` """ _logger.debug(f"write_str {path!r}") - await cls.write_bytes(path, content.encode(encoding=encoding)) + await self.write_bytes(path, content.encode(encoding=encoding)) diff --git a/api/advent22_api/core/helpers.py b/api/advent22_api/core/helpers.py index bbf7260..e9b6cd4 100644 --- a/api/advent22_api/core/helpers.py +++ b/api/advent22_api/core/helpers.py @@ -11,7 +11,7 @@ from PIL.Image import Image, Resampling from pydantic import BaseModel from .config import get_config -from .dav.webdav import WebDAV +from .settings import WEBDAV T = TypeVar("T") RE_IMG = re.compile(r"\.(gif|jpe?g|tiff?|png|bmp)$", flags=re.IGNORECASE) @@ -94,7 +94,7 @@ def list_helper( async def _list_helper() -> list[str]: return [ f"{directory}/{file}" - for file in await WebDAV.list_files(directory=directory, regex=regex) + for file in await WEBDAV.list_files(directory=directory, regex=regex) ] return _list_helper @@ -110,10 +110,10 @@ async def load_image(file_name: str) -> Image: Versuche, Bild aus Datei zu laden """ - if not await WebDAV.exists(file_name): + if not await WEBDAV.exists(file_name): raise RuntimeError(f"DAV-File {file_name} does not exist!") - return PILImage.open(BytesIO(await WebDAV.read_bytes(file_name))) + return PILImage.open(BytesIO(await WEBDAV.read_bytes(file_name))) class ImageData(BaseModel): diff --git a/api/advent22_api/core/settings.py b/api/advent22_api/core/settings.py index d4d9d39..c2e210b 100644 --- a/api/advent22_api/core/settings.py +++ b/api/advent22_api/core/settings.py @@ -1,5 +1,9 @@ from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict +from redis import ConnectionError, Redis + +from .dav.webdav import Settings as WebDAVSettings +from .dav.webdav import WebDAV class Credentials(BaseModel): @@ -22,7 +26,6 @@ class DavSettings(BaseModel): password="password", ) - cache_ttl: int = 60 * 10 config_filename: str = "config.toml" @property @@ -39,10 +42,12 @@ class RedisSettings(BaseModel): Connection to a redis server. """ + cache_ttl: int = 60 * 10 + host: str = "localhost" port: int = 6379 db: int = 0 - protocol: int = 3 + protocol_version: int = 3 class Settings(BaseSettings): @@ -98,3 +103,26 @@ class Settings(BaseSettings): SETTINGS = Settings() + +try: + _REDIS = Redis( + host=SETTINGS.redis.host, + port=SETTINGS.redis.port, + db=SETTINGS.redis.db, + protocol=SETTINGS.redis.protocol_version, + ) + _REDIS.ping() + +except ConnectionError: + raise RuntimeError("Redis connection failed!") + + +WEBDAV = WebDAV( + WebDAVSettings( + url=SETTINGS.webdav.url, + username=SETTINGS.webdav.auth.username, + password=SETTINGS.webdav.auth.password, + ), + _REDIS, + SETTINGS.redis.cache_ttl, +) diff --git a/api/advent22_api/routers/admin.py b/api/advent22_api/routers/admin.py index cc7fd8a..931d32e 100644 --- a/api/advent22_api/routers/admin.py +++ b/api/advent22_api/routers/admin.py @@ -59,7 +59,6 @@ class AdminConfigModel(BaseModel): class __WebDAV(BaseModel): url: str - cache_ttl: int config_file: str solution: __Solution @@ -113,7 +112,7 @@ async def get_config_model( "redis": SETTINGS.redis, "webdav": { "url": SETTINGS.webdav.url, - "cache_ttl": SETTINGS.webdav.cache_ttl, + "cache_ttl": SETTINGS.redis.cache_ttl, "config_file": SETTINGS.webdav.config_filename, }, } diff --git a/ui/src/components/admin/ConfigView.vue b/ui/src/components/admin/ConfigView.vue index 139095e..d0e5f7c 100644 --- a/ui/src/components/admin/ConfigView.vue +++ b/ui/src/components/admin/ConfigView.vue @@ -140,9 +140,6 @@ -
Cache-Dauer
-
{{ admin_config_model.webdav.cache_ttl }} s
-
Konfigurationsdatei
{{ admin_config_model.webdav.config_file }}
@@ -152,10 +149,11 @@

Sonstige

Redis
+
Cache-Dauer: {{ admin_config_model.redis.cache_ttl }} s
Host: {{ admin_config_model.redis.host }}
Port: {{ admin_config_model.redis.port }}
Datenbank: {{ admin_config_model.redis.db }}
-
Protokoll: {{ admin_config_model.redis.protocol }}
+
Protokoll: {{ admin_config_model.redis.protocol_version }}
UI-Admin
@@ -219,14 +217,14 @@ const admin_config_model = ref({ }, fonts: [{ file: "consetetur", size: 0 }], redis: { + cache_ttl: 0, host: "0.0.0.0", port: 6379, db: 0, - protocol: 3, + protocol_version: 3, }, webdav: { url: "sadipscing elitr", - cache_ttl: 0, config_file: "sed diam nonumy", }, }); diff --git a/ui/src/lib/model.ts b/ui/src/lib/model.ts index f2e021a..972e35d 100644 --- a/ui/src/lib/model.ts +++ b/ui/src/lib/model.ts @@ -26,14 +26,14 @@ export interface AdminConfigModel { }; fonts: { file: string; size: number }[]; redis: { + cache_ttl: number; host: string; port: number; db: number; - protocol: number; + protocol_version: number; }; webdav: { url: string; - cache_ttl: number; config_file: string; }; }