Compare commits
No commits in common. "feature/webdav-rework" and "master" have entirely different histories.
feature/we
...
master
15 changed files with 90 additions and 245 deletions
2
api/.vscode/launch.json
vendored
2
api/.vscode/launch.json
vendored
|
|
@ -21,7 +21,7 @@
|
|||
"${workspaceFolder}/advent22_api"
|
||||
],
|
||||
"env": {
|
||||
"ADVENT22__REDIS__CACHE_TTL": "30"
|
||||
"ADVENT22__WEBDAV__CACHE_TTL": "30"
|
||||
},
|
||||
"justMyCode": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from fastapi import Depends
|
|||
from pydantic import BaseModel
|
||||
|
||||
from .config import Config, get_config
|
||||
from .settings import WEBDAV
|
||||
from .dav.webdav 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))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import tomllib
|
|||
from markdown import markdown
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
from .settings import SETTINGS, WEBDAV, Credentials
|
||||
from .dav.webdav import WebDAV
|
||||
from .settings import SETTINGS, Credentials
|
||||
from .transformed_string import TransformedString
|
||||
|
||||
|
||||
|
|
@ -76,5 +77,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))
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import inspect
|
||||
from base64 import urlsafe_b64encode
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from itertools import chain
|
||||
from typing import Iterable, ParamSpec, TypeVar
|
||||
|
||||
from redis import Redis
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||
class Config:
|
||||
redis: Redis
|
||||
prefix: str | None = "cache"
|
||||
ttl_fresh: int = 600
|
||||
ttl_stale: int | None = None
|
||||
|
||||
|
||||
def qualified_name(callable_obj) -> str:
|
||||
# callable classes/instances
|
||||
if hasattr(callable_obj, "__call__") and not inspect.isroutine(callable_obj):
|
||||
# callable instance: use its class
|
||||
cls = callable_obj.__class__
|
||||
module = getattr(cls, "__module__", None)
|
||||
qual = getattr(cls, "__qualname__", cls.__name__)
|
||||
return f"{module}.{qual}" if module else qual
|
||||
|
||||
# functions, methods, builtins
|
||||
# unwrap descriptors like staticmethod/classmethod
|
||||
if isinstance(callable_obj, staticmethod | classmethod):
|
||||
callable_obj = callable_obj.__func__
|
||||
|
||||
# bound method
|
||||
if inspect.ismethod(callable_obj):
|
||||
func = callable_obj.__func__
|
||||
owner = getattr(func, "__qualname__", func.__name__).rsplit(".", 1)[0]
|
||||
module = getattr(func, "__module__", None)
|
||||
qual = f"{owner}.{func.__name__}"
|
||||
return f"{module}.{qual}" if module else qual
|
||||
|
||||
# regular function or builtin
|
||||
if inspect.isfunction(callable_obj) or inspect.isbuiltin(callable_obj):
|
||||
module = getattr(callable_obj, "__module__", None)
|
||||
qual = getattr(callable_obj, "__qualname__", callable_obj.__name__)
|
||||
return f"{module}.{qual}" if module else qual
|
||||
|
||||
# fallback for other callables (functors, functools.partial, etc.)
|
||||
try:
|
||||
module = getattr(callable_obj, "__module__", None)
|
||||
qual = getattr(callable_obj, "__qualname__", None) or getattr(
|
||||
callable_obj, "__name__", type(callable_obj).__name__
|
||||
)
|
||||
return f"{module}.{qual}" if module else qual
|
||||
|
||||
except Exception:
|
||||
return urlsafe_b64encode(repr(callable_obj).encode("utf-8")).decode("utf-8")
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def args_slice(func: Callable[P, R], *args: Iterable) -> tuple:
|
||||
if hasattr(func, "__call__") and not inspect.isroutine(func):
|
||||
return args[1:]
|
||||
|
||||
if isinstance(func, staticmethod | classmethod):
|
||||
func = func.__func__
|
||||
|
||||
if inspect.ismethod(func):
|
||||
return args[1:]
|
||||
|
||||
return tuple(*args)
|
||||
|
||||
|
||||
def cache_key(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> str:
|
||||
"""Return a cache key for use with cached methods."""
|
||||
|
||||
kwargs_by_key = sorted(kwargs.items(), key=lambda kv: kv[0])
|
||||
|
||||
parts = chain(
|
||||
(qualified_name(func),),
|
||||
# positional args
|
||||
(repr(arg) for arg in args_slice(func, args)),
|
||||
# keyword args
|
||||
(f"{k}={v!r}" for k, v in kwargs_by_key),
|
||||
)
|
||||
|
||||
return ":".join(parts)
|
||||
|
||||
|
||||
def redis_cached(cfg: Config) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
""" """
|
||||
|
||||
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
||||
""" """
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
key = cache_key(func, *args, **kwargs)
|
||||
|
||||
if cfg.prefix is not None:
|
||||
key = f"{cfg.prefix}:{key}"
|
||||
|
||||
# pre-hook
|
||||
result = func(*args, **kwargs)
|
||||
# post-hook
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
from itertools import chain
|
||||
from json import JSONDecodeError
|
||||
from typing import Any, Callable
|
||||
from typing import Callable, Hashable
|
||||
|
||||
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,18 +11,12 @@ from webdav3.client import Client as __WebDAVclient
|
|||
def davkey(
|
||||
name: str,
|
||||
slice: slice = slice(1, None),
|
||||
) -> Callable[..., str]:
|
||||
def func(*args: Any, **kwargs: Any) -> str:
|
||||
) -> Callable[..., tuple[Hashable, ...]]:
|
||||
def func(*args, **kwargs) -> tuple[Hashable, ...]:
|
||||
"""Return a cache key for use with cached methods."""
|
||||
|
||||
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)})"
|
||||
key = hashkey(name, *args[slice], **kwargs)
|
||||
return hashkey(*(str(key_item) for key_item in key))
|
||||
|
||||
return func
|
||||
|
||||
|
|
|
|||
|
|
@ -1,113 +1,108 @@
|
|||
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
|
||||
_cache: RedisCache
|
||||
|
||||
def __init__(self, settings: Settings, redis: Redis, ttl_sec: int) -> None:
|
||||
try:
|
||||
self._webdav_client = WebDAVclient(
|
||||
_webdav_client = WebDAVclient(
|
||||
{
|
||||
"webdav_hostname": settings.url,
|
||||
"webdav_login": settings.username,
|
||||
"webdav_password": settings.password,
|
||||
"webdav_hostname": SETTINGS.webdav.url,
|
||||
"webdav_login": SETTINGS.webdav.auth.username,
|
||||
"webdav_password": SETTINGS.webdav.auth.password,
|
||||
}
|
||||
)
|
||||
assert self._webdav_client.check() is True
|
||||
|
||||
except AssertionError:
|
||||
raise RuntimeError("WebDAV connection failed!")
|
||||
|
||||
self._cache = RedisCache(cache=redis, ttl=ttl_sec)
|
||||
_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,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@asyncify
|
||||
@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`
|
||||
"""
|
||||
|
||||
return self._webdav_client.list(directory)
|
||||
|
||||
async def list_files(
|
||||
self,
|
||||
@cachedmethod(cache=lambda cls: cls._cache, key=davkey("list_files"))
|
||||
def list_files(
|
||||
cls,
|
||||
directory: str = "",
|
||||
*,
|
||||
regex: re.Pattern[str] = re.compile(""),
|
||||
) -> list[str]:
|
||||
_logger.debug(f"list_files {directory!r} ({regex!r})")
|
||||
"""
|
||||
List files in directory `directory` matching RegEx `regex`
|
||||
"""
|
||||
|
||||
_logger.debug(f"list_files {directory!r}")
|
||||
ls = cls._webdav_client.list(directory)
|
||||
|
||||
ls = await self._list_files(directory)
|
||||
return [path for path in ls if regex.search(path)]
|
||||
|
||||
@classmethod
|
||||
@asyncify
|
||||
@cachedmethod(cache=lambda self: self._cache, key=davkey("exists"))
|
||||
def exists(self, path: str) -> bool:
|
||||
@cachedmethod(cache=lambda cls: cls._cache, key=davkey("exists"))
|
||||
def exists(cls, path: str) -> bool:
|
||||
"""
|
||||
`True` iff there is a WebDAV resource at `path`
|
||||
"""
|
||||
|
||||
_logger.debug(f"file_exists {path!r}")
|
||||
return self._webdav_client.check(path)
|
||||
return cls._webdav_client.check(path)
|
||||
|
||||
@classmethod
|
||||
@asyncify
|
||||
@cachedmethod(cache=lambda self: self._cache, key=davkey("read_bytes"))
|
||||
def read_bytes(self, path: str) -> bytes:
|
||||
@cachedmethod(cache=lambda cls: cls._cache, key=davkey("read_bytes"))
|
||||
def read_bytes(cls, path: str) -> bytes:
|
||||
"""
|
||||
Load WebDAV file from `path` as bytes
|
||||
"""
|
||||
|
||||
_logger.debug(f"read_bytes {path!r}")
|
||||
buffer = BytesIO()
|
||||
self._webdav_client.download_from(buffer, path)
|
||||
cls._webdav_client.download_from(buffer, path)
|
||||
buffer.seek(0)
|
||||
|
||||
return buffer.read()
|
||||
|
||||
async def read_str(self, path: str, encoding="utf-8") -> str:
|
||||
@classmethod
|
||||
async def read_str(cls, path: str, encoding="utf-8") -> str:
|
||||
"""
|
||||
Load WebDAV file from `path` as string
|
||||
"""
|
||||
|
||||
_logger.debug(f"read_str {path!r}")
|
||||
return (await self.read_bytes(path)).decode(encoding=encoding).strip()
|
||||
return (await cls.read_bytes(path)).decode(encoding=encoding).strip()
|
||||
|
||||
@classmethod
|
||||
@asyncify
|
||||
def write_bytes(self, path: str, buffer: bytes) -> None:
|
||||
def write_bytes(cls, path: str, buffer: bytes) -> None:
|
||||
"""
|
||||
Write bytes from `buffer` into WebDAV file at `path`
|
||||
"""
|
||||
|
||||
_logger.debug(f"write_bytes {path!r}")
|
||||
self._webdav_client.upload_to(buffer, path)
|
||||
cls._webdav_client.upload_to(buffer, path)
|
||||
|
||||
# invalidate cache entry
|
||||
# begin slice at 0 (there is no "self" argument)
|
||||
del self._cache[davkey("read_bytes", slice(0, None))(path)]
|
||||
# explicit slice as there is no "cls" argument
|
||||
del cls._cache[davkey("read_bytes", slice(0, None))(path)]
|
||||
|
||||
async def write_str(self, path: str, content: str, encoding="utf-8") -> None:
|
||||
@classmethod
|
||||
async def write_str(cls, 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 self.write_bytes(path, content.encode(encoding=encoding))
|
||||
await cls.write_bytes(path, content.encode(encoding=encoding))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from PIL.Image import Image, Resampling
|
|||
from pydantic import BaseModel
|
||||
|
||||
from .config import get_config
|
||||
from .settings import WEBDAV
|
||||
from .dav.webdav 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):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
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):
|
||||
|
|
@ -26,6 +22,7 @@ class DavSettings(BaseModel):
|
|||
password="password",
|
||||
)
|
||||
|
||||
cache_ttl: int = 60 * 10
|
||||
config_filename: str = "config.toml"
|
||||
|
||||
@property
|
||||
|
|
@ -42,12 +39,10 @@ class RedisSettings(BaseModel):
|
|||
Connection to a redis server.
|
||||
"""
|
||||
|
||||
cache_ttl: int = 60 * 10
|
||||
|
||||
host: str = "localhost"
|
||||
port: int = 6379
|
||||
db: int = 0
|
||||
protocol_version: int = 3
|
||||
protocol: int = 3
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
|
@ -103,26 +98,3 @@ 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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import os
|
|||
|
||||
from granian import Granian
|
||||
from granian.constants import Interfaces, Loops
|
||||
from pydantic import BaseModel, PositiveInt
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class WorkersSettings(BaseModel):
|
||||
per_core: PositiveInt = 1
|
||||
max: PositiveInt | None = None
|
||||
exact: PositiveInt | None = None
|
||||
per_core: int = Field(1, ge=1)
|
||||
max: int | None = Field(None, ge=1)
|
||||
exact: int | None = Field(None, ge=1)
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from datetime import date
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
from advent22_api.core.helpers import EventDates
|
||||
|
|
@ -60,6 +59,7 @@ class AdminConfigModel(BaseModel):
|
|||
|
||||
class __WebDAV(BaseModel):
|
||||
url: str
|
||||
cache_ttl: int
|
||||
config_file: str
|
||||
|
||||
solution: __Solution
|
||||
|
|
@ -113,7 +113,7 @@ async def get_config_model(
|
|||
"redis": SETTINGS.redis,
|
||||
"webdav": {
|
||||
"url": SETTINGS.webdav.url,
|
||||
"cache_ttl": SETTINGS.redis.cache_ttl,
|
||||
"cache_ttl": SETTINGS.webdav.cache_ttl,
|
||||
"config_file": SETTINGS.webdav.config_filename,
|
||||
},
|
||||
}
|
||||
|
|
@ -174,21 +174,16 @@ async def put_doors(
|
|||
await cal_cfg.change(cfg)
|
||||
|
||||
|
||||
class CredentialsName(str, Enum):
|
||||
DAV = "dav"
|
||||
UI = "ui"
|
||||
|
||||
|
||||
@router.get("/credentials/{name}")
|
||||
async def get_credentials(
|
||||
name: CredentialsName,
|
||||
name: str,
|
||||
_: None = Depends(require_admin),
|
||||
cfg: Config = Depends(get_config),
|
||||
) -> Credentials:
|
||||
|
||||
if name == CredentialsName.DAV:
|
||||
if name == "dav":
|
||||
return SETTINGS.webdav.auth
|
||||
elif name == CredentialsName.UI:
|
||||
elif name == "ui":
|
||||
return cfg.admin
|
||||
else:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
return Credentials()
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.13.5",
|
||||
"bulma": "^1.0.4",
|
||||
"bulma-toast": "2.4.4",
|
||||
"bulma-toast": "2.4.3",
|
||||
"eslint": "^10.0.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-oxlint": "~1.49.0",
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@
|
|||
</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>
|
||||
|
|
@ -149,11 +152,10 @@
|
|||
<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_version }}</dd>
|
||||
<dd>Protokoll: {{ admin_config_model.redis.protocol }}</dd>
|
||||
|
||||
<dt>UI-Admin</dt>
|
||||
<dd class="is-family-monospace">
|
||||
|
|
@ -217,14 +219,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_version: 3,
|
||||
protocol: 3,
|
||||
},
|
||||
webdav: {
|
||||
url: "sadipscing elitr",
|
||||
cache_ttl: 0,
|
||||
config_file: "sed diam nonumy",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ export interface AdminConfigModel {
|
|||
};
|
||||
fonts: { file: string; size: number }[];
|
||||
redis: {
|
||||
cache_ttl: number;
|
||||
host: string;
|
||||
port: number;
|
||||
db: number;
|
||||
protocol_version: number;
|
||||
protocol: number;
|
||||
};
|
||||
webdav: {
|
||||
url: string;
|
||||
cache_ttl: number;
|
||||
config_file: string;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ advent22Store().init();
|
|||
app.mount("#app");
|
||||
|
||||
toast_set_defaults({
|
||||
message: "",
|
||||
duration: 10e3,
|
||||
pauseOnHover: true,
|
||||
dismissible: true,
|
||||
|
|
|
|||
10
ui/yarn.lock
10
ui/yarn.lock
|
|
@ -2075,7 +2075,7 @@ __metadata:
|
|||
animate.css: "npm:^4.1.1"
|
||||
axios: "npm:^1.13.5"
|
||||
bulma: "npm:^1.0.4"
|
||||
bulma-toast: "npm:2.4.4"
|
||||
bulma-toast: "npm:2.4.3"
|
||||
eslint: "npm:^10.0.1"
|
||||
eslint-config-prettier: "npm:^10.1.8"
|
||||
eslint-plugin-oxlint: "npm:~1.49.0"
|
||||
|
|
@ -2311,10 +2311,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bulma-toast@npm:2.4.4":
|
||||
version: 2.4.4
|
||||
resolution: "bulma-toast@npm:2.4.4"
|
||||
checksum: 10c0/ccb36b5c632585e9e5bca4b7da7fa5f5e0e87da6244cca580bbb95fc8f3d0dc78d8b279fe0dfe024818baa1c47c2139e50d052447b130fb525ae5ffdb297acfd
|
||||
"bulma-toast@npm:2.4.3":
|
||||
version: 2.4.3
|
||||
resolution: "bulma-toast@npm:2.4.3"
|
||||
checksum: 10c0/40dd9668643338496eb28caca9b772a6002d9c6fbdc5d76237cbdaaa8f56c8ced39965705f92f5d5a29f3e6df57f70a8fa311cf05c14075aacf93f96f7338470
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue