🚧 webdav rework

- use instance instead of class methods
- prettier cache keys
This commit is contained in:
Jörn-Michael Miehe 2026-02-22 17:36:35 +01:00
parent 7b65d8c9b5
commit 049ae8fc56
10 changed files with 110 additions and 75 deletions

View file

@ -21,7 +21,7 @@
"${workspaceFolder}/advent22_api"
],
"env": {
"ADVENT22__WEBDAV__CACHE_TTL": "30"
"ADVENT22__REDIS__CACHE_TTL": "30"
},
"justMyCode": true
}

View file

@ -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))

View file

@ -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))

View file

@ -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

View file

@ -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))

View file

@ -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):

View file

@ -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,
)

View file

@ -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,
},
}

View file

@ -140,9 +140,6 @@
</BulmaSecret>
</dd>
<dt>Cache-Dauer</dt>
<dd>{{ admin_config_model.webdav.cache_ttl }} s</dd>
<dt>Konfigurationsdatei</dt>
<dd>{{ admin_config_model.webdav.config_file }}</dd>
</dl>
@ -152,10 +149,11 @@
<h3>Sonstige</h3>
<dl>
<dt>Redis</dt>
<dd>Cache-Dauer: {{ admin_config_model.redis.cache_ttl }} s</dd>
<dd>Host: {{ admin_config_model.redis.host }}</dd>
<dd>Port: {{ admin_config_model.redis.port }}</dd>
<dd>Datenbank: {{ admin_config_model.redis.db }}</dd>
<dd>Protokoll: {{ admin_config_model.redis.protocol }}</dd>
<dd>Protokoll: {{ admin_config_model.redis.protocol_version }}</dd>
<dt>UI-Admin</dt>
<dd class="is-family-monospace">
@ -219,14 +217,14 @@ const admin_config_model = ref<AdminConfigModel>({
},
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",
},
});

View file

@ -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;
};
}