mirror of
https://code.lenaisten.de/Lenaisten/advent22.git
synced 2026-02-25 02:20:17 +00:00
🚧 webdav rework
- use instance instead of class methods - prettier cache keys
This commit is contained in:
parent
7b65d8c9b5
commit
049ae8fc56
10 changed files with 110 additions and 75 deletions
2
api/.vscode/launch.json
vendored
2
api/.vscode/launch.json
vendored
|
|
@ -21,7 +21,7 @@
|
||||||
"${workspaceFolder}/advent22_api"
|
"${workspaceFolder}/advent22_api"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"ADVENT22__WEBDAV__CACHE_TTL": "30"
|
"ADVENT22__REDIS__CACHE_TTL": "30"
|
||||||
},
|
},
|
||||||
"justMyCode": true
|
"justMyCode": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from fastapi import Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from .config import Config, get_config
|
from .config import Config, get_config
|
||||||
from .dav.webdav import WebDAV
|
from .settings import WEBDAV
|
||||||
|
|
||||||
|
|
||||||
class DoorSaved(BaseModel):
|
class DoorSaved(BaseModel):
|
||||||
|
|
@ -37,7 +37,7 @@ class CalendarConfig(BaseModel):
|
||||||
Kalender Konfiguration ändern
|
Kalender Konfiguration ändern
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await WebDAV.write_str(
|
await WEBDAV.write_str(
|
||||||
path=f"files/{cfg.calendar}",
|
path=f"files/{cfg.calendar}",
|
||||||
content=tomli_w.dumps(self.model_dump()),
|
content=tomli_w.dumps(self.model_dump()),
|
||||||
)
|
)
|
||||||
|
|
@ -50,5 +50,5 @@ async def get_calendar_config(
|
||||||
Kalender Konfiguration lesen
|
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))
|
return CalendarConfig.model_validate(tomllib.loads(txt))
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import tomllib
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
from pydantic import BaseModel, ConfigDict, field_validator
|
from pydantic import BaseModel, ConfigDict, field_validator
|
||||||
|
|
||||||
from .dav.webdav import WebDAV
|
from .settings import SETTINGS, WEBDAV, Credentials
|
||||||
from .settings import SETTINGS, Credentials
|
|
||||||
from .transformed_string import TransformedString
|
from .transformed_string import TransformedString
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -77,5 +76,5 @@ async def get_config() -> Config:
|
||||||
Globale Konfiguration lesen
|
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))
|
return Config.model_validate(tomllib.loads(txt))
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
from itertools import chain
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from typing import Callable, Hashable
|
from typing import Any, Callable
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from cachetools.keys import hashkey
|
|
||||||
from CacheToolsUtils import RedisCache as __RedisCache
|
from CacheToolsUtils import RedisCache as __RedisCache
|
||||||
from redis.typing import EncodableT, ResponseT
|
from redis.typing import EncodableT, ResponseT
|
||||||
from webdav3.client import Client as __WebDAVclient
|
from webdav3.client import Client as __WebDAVclient
|
||||||
|
|
@ -11,12 +11,18 @@ from webdav3.client import Client as __WebDAVclient
|
||||||
def davkey(
|
def davkey(
|
||||||
name: str,
|
name: str,
|
||||||
slice: slice = slice(1, None),
|
slice: slice = slice(1, None),
|
||||||
) -> Callable[..., tuple[Hashable, ...]]:
|
) -> Callable[..., str]:
|
||||||
def func(*args, **kwargs) -> tuple[Hashable, ...]:
|
def func(*args: Any, **kwargs: Any) -> str:
|
||||||
"""Return a cache key for use with cached methods."""
|
"""Return a cache key for use with cached methods."""
|
||||||
|
|
||||||
key = hashkey(name, *args[slice], **kwargs)
|
call_args = chain(
|
||||||
return hashkey(*(str(key_item) for key_item in key))
|
# 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
|
return func
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,113 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from asyncify import asyncify
|
from asyncify import asyncify
|
||||||
from cachetools import cachedmethod
|
from cachetools import cachedmethod
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
from ..settings import SETTINGS
|
|
||||||
from .helpers import RedisCache, WebDAVclient, davkey
|
from .helpers import RedisCache, WebDAVclient, davkey
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True, slots=True)
|
||||||
|
class Settings:
|
||||||
|
url: str
|
||||||
|
username: str = "johndoe"
|
||||||
|
password: str = "s3cr3t!"
|
||||||
|
|
||||||
|
|
||||||
class WebDAV:
|
class WebDAV:
|
||||||
_webdav_client = WebDAVclient(
|
_webdav_client: WebDAVclient
|
||||||
{
|
_cache: RedisCache
|
||||||
"webdav_hostname": SETTINGS.webdav.url,
|
|
||||||
"webdav_login": SETTINGS.webdav.auth.username,
|
|
||||||
"webdav_password": SETTINGS.webdav.auth.password,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
_cache = RedisCache(
|
def __init__(self, settings: Settings, redis: Redis, ttl_sec: int) -> None:
|
||||||
cache=Redis(
|
try:
|
||||||
host=SETTINGS.redis.host,
|
self._webdav_client = WebDAVclient(
|
||||||
port=SETTINGS.redis.port,
|
{
|
||||||
db=SETTINGS.redis.db,
|
"webdav_hostname": settings.url,
|
||||||
protocol=SETTINGS.redis.protocol,
|
"webdav_login": settings.username,
|
||||||
),
|
"webdav_password": settings.password,
|
||||||
ttl=SETTINGS.webdav.cache_ttl,
|
}
|
||||||
)
|
)
|
||||||
|
assert self._webdav_client.check() is True
|
||||||
|
|
||||||
|
except AssertionError:
|
||||||
|
raise RuntimeError("WebDAV connection failed!")
|
||||||
|
|
||||||
|
self._cache = RedisCache(cache=redis, ttl=ttl_sec)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@asyncify
|
@asyncify
|
||||||
@cachedmethod(cache=lambda cls: cls._cache, key=davkey("list_files"))
|
@cachedmethod(cache=lambda self: self._cache, key=davkey("list_files"))
|
||||||
def list_files(
|
def _list_files(self, directory: str = "") -> list[str]:
|
||||||
cls,
|
|
||||||
directory: str = "",
|
|
||||||
*,
|
|
||||||
regex: re.Pattern[str] = re.compile(""),
|
|
||||||
) -> list[str]:
|
|
||||||
"""
|
"""
|
||||||
List files in directory `directory` matching RegEx `regex`
|
List files in directory `directory` matching RegEx `regex`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_logger.debug(f"list_files {directory!r}")
|
return self._webdav_client.list(directory)
|
||||||
ls = cls._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)]
|
return [path for path in ls if regex.search(path)]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@asyncify
|
@asyncify
|
||||||
@cachedmethod(cache=lambda cls: cls._cache, key=davkey("exists"))
|
@cachedmethod(cache=lambda self: self._cache, key=davkey("exists"))
|
||||||
def exists(cls, path: str) -> bool:
|
def exists(self, path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
`True` iff there is a WebDAV resource at `path`
|
`True` iff there is a WebDAV resource at `path`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_logger.debug(f"file_exists {path!r}")
|
_logger.debug(f"file_exists {path!r}")
|
||||||
return cls._webdav_client.check(path)
|
return self._webdav_client.check(path)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@asyncify
|
@asyncify
|
||||||
@cachedmethod(cache=lambda cls: cls._cache, key=davkey("read_bytes"))
|
@cachedmethod(cache=lambda self: self._cache, key=davkey("read_bytes"))
|
||||||
def read_bytes(cls, path: str) -> bytes:
|
def read_bytes(self, path: str) -> bytes:
|
||||||
"""
|
"""
|
||||||
Load WebDAV file from `path` as bytes
|
Load WebDAV file from `path` as bytes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_logger.debug(f"read_bytes {path!r}")
|
_logger.debug(f"read_bytes {path!r}")
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
cls._webdav_client.download_from(buffer, path)
|
self._webdav_client.download_from(buffer, path)
|
||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
|
|
||||||
return buffer.read()
|
return buffer.read()
|
||||||
|
|
||||||
@classmethod
|
async def read_str(self, path: str, encoding="utf-8") -> str:
|
||||||
async def read_str(cls, path: str, encoding="utf-8") -> str:
|
|
||||||
"""
|
"""
|
||||||
Load WebDAV file from `path` as string
|
Load WebDAV file from `path` as string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_logger.debug(f"read_str {path!r}")
|
_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
|
@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`
|
Write bytes from `buffer` into WebDAV file at `path`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_logger.debug(f"write_bytes {path!r}")
|
_logger.debug(f"write_bytes {path!r}")
|
||||||
cls._webdav_client.upload_to(buffer, path)
|
self._webdav_client.upload_to(buffer, path)
|
||||||
|
|
||||||
# invalidate cache entry
|
# invalidate cache entry
|
||||||
# explicit slice as there is no "cls" argument
|
# begin slice at 0 (there is no "self" argument)
|
||||||
del cls._cache[davkey("read_bytes", slice(0, None))(path)]
|
del self._cache[davkey("read_bytes", slice(0, None))(path)]
|
||||||
|
|
||||||
@classmethod
|
async def write_str(self, path: str, content: str, encoding="utf-8") -> None:
|
||||||
async def write_str(cls, path: str, content: str, encoding="utf-8") -> None:
|
|
||||||
"""
|
"""
|
||||||
Write string from `content` into WebDAV file at `path`
|
Write string from `content` into WebDAV file at `path`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_logger.debug(f"write_str {path!r}")
|
_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))
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from PIL.Image import Image, Resampling
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from .config import get_config
|
from .config import get_config
|
||||||
from .dav.webdav import WebDAV
|
from .settings import WEBDAV
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
RE_IMG = re.compile(r"\.(gif|jpe?g|tiff?|png|bmp)$", flags=re.IGNORECASE)
|
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]:
|
async def _list_helper() -> list[str]:
|
||||||
return [
|
return [
|
||||||
f"{directory}/{file}"
|
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
|
return _list_helper
|
||||||
|
|
@ -110,10 +110,10 @@ async def load_image(file_name: str) -> Image:
|
||||||
Versuche, Bild aus Datei zu laden
|
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!")
|
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):
|
class ImageData(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
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):
|
class Credentials(BaseModel):
|
||||||
|
|
@ -22,7 +26,6 @@ class DavSettings(BaseModel):
|
||||||
password="password",
|
password="password",
|
||||||
)
|
)
|
||||||
|
|
||||||
cache_ttl: int = 60 * 10
|
|
||||||
config_filename: str = "config.toml"
|
config_filename: str = "config.toml"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -39,10 +42,12 @@ class RedisSettings(BaseModel):
|
||||||
Connection to a redis server.
|
Connection to a redis server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
cache_ttl: int = 60 * 10
|
||||||
|
|
||||||
host: str = "localhost"
|
host: str = "localhost"
|
||||||
port: int = 6379
|
port: int = 6379
|
||||||
db: int = 0
|
db: int = 0
|
||||||
protocol: int = 3
|
protocol_version: int = 3
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
|
@ -98,3 +103,26 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
|
|
||||||
SETTINGS = Settings()
|
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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ class AdminConfigModel(BaseModel):
|
||||||
|
|
||||||
class __WebDAV(BaseModel):
|
class __WebDAV(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
cache_ttl: int
|
|
||||||
config_file: str
|
config_file: str
|
||||||
|
|
||||||
solution: __Solution
|
solution: __Solution
|
||||||
|
|
@ -113,7 +112,7 @@ async def get_config_model(
|
||||||
"redis": SETTINGS.redis,
|
"redis": SETTINGS.redis,
|
||||||
"webdav": {
|
"webdav": {
|
||||||
"url": SETTINGS.webdav.url,
|
"url": SETTINGS.webdav.url,
|
||||||
"cache_ttl": SETTINGS.webdav.cache_ttl,
|
"cache_ttl": SETTINGS.redis.cache_ttl,
|
||||||
"config_file": SETTINGS.webdav.config_filename,
|
"config_file": SETTINGS.webdav.config_filename,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,9 +140,6 @@
|
||||||
</BulmaSecret>
|
</BulmaSecret>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>Cache-Dauer</dt>
|
|
||||||
<dd>{{ admin_config_model.webdav.cache_ttl }} s</dd>
|
|
||||||
|
|
||||||
<dt>Konfigurationsdatei</dt>
|
<dt>Konfigurationsdatei</dt>
|
||||||
<dd>{{ admin_config_model.webdav.config_file }}</dd>
|
<dd>{{ admin_config_model.webdav.config_file }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
@ -152,10 +149,11 @@
|
||||||
<h3>Sonstige</h3>
|
<h3>Sonstige</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Redis</dt>
|
<dt>Redis</dt>
|
||||||
|
<dd>Cache-Dauer: {{ admin_config_model.redis.cache_ttl }} s</dd>
|
||||||
<dd>Host: {{ admin_config_model.redis.host }}</dd>
|
<dd>Host: {{ admin_config_model.redis.host }}</dd>
|
||||||
<dd>Port: {{ admin_config_model.redis.port }}</dd>
|
<dd>Port: {{ admin_config_model.redis.port }}</dd>
|
||||||
<dd>Datenbank: {{ admin_config_model.redis.db }}</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>
|
<dt>UI-Admin</dt>
|
||||||
<dd class="is-family-monospace">
|
<dd class="is-family-monospace">
|
||||||
|
|
@ -219,14 +217,14 @@ const admin_config_model = ref<AdminConfigModel>({
|
||||||
},
|
},
|
||||||
fonts: [{ file: "consetetur", size: 0 }],
|
fonts: [{ file: "consetetur", size: 0 }],
|
||||||
redis: {
|
redis: {
|
||||||
|
cache_ttl: 0,
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 6379,
|
port: 6379,
|
||||||
db: 0,
|
db: 0,
|
||||||
protocol: 3,
|
protocol_version: 3,
|
||||||
},
|
},
|
||||||
webdav: {
|
webdav: {
|
||||||
url: "sadipscing elitr",
|
url: "sadipscing elitr",
|
||||||
cache_ttl: 0,
|
|
||||||
config_file: "sed diam nonumy",
|
config_file: "sed diam nonumy",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,14 @@ export interface AdminConfigModel {
|
||||||
};
|
};
|
||||||
fonts: { file: string; size: number }[];
|
fonts: { file: string; size: number }[];
|
||||||
redis: {
|
redis: {
|
||||||
|
cache_ttl: number;
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
db: number;
|
db: number;
|
||||||
protocol: number;
|
protocol_version: number;
|
||||||
};
|
};
|
||||||
webdav: {
|
webdav: {
|
||||||
url: string;
|
url: string;
|
||||||
cache_ttl: number;
|
|
||||||
config_file: string;
|
config_file: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue