implement timeout instead of scheduling

This commit is contained in:
Jörn-Michael Miehe 2022-09-02 12:20:29 +00:00
parent c65aba82a4
commit 72e33238c4
3 changed files with 69 additions and 127 deletions

View file

@ -1,17 +1,19 @@
import asyncio
import functools
import logging
import time
from io import BytesIO
from threading import Lock
from typing import Optional
from typing import Any, Optional
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from async_lru import alru_cache
from webdav3.client import Resource
from . import CLIENT
_logger = logging.getLogger(__name__)
def run_in_executor(f):
def _run_in_executor(f):
"""
Decorator to make blocking function call asyncio compatible
https://stackoverflow.com/questions/41063331/how-to-use-asyncio-with-existing-blocking-library/
@ -28,64 +30,52 @@ def run_in_executor(f):
return inner
def _get_ttl_hash(seconds: int = 20) -> int:
"""
Return the same value within `seconds` time period
https://stackoverflow.com/a/55900800
"""
return round(time.time() / seconds)
@alru_cache(maxsize=20)
async def _get_buffer(
remote_path: Any,
ttl_hash: Optional[int] = None,
) -> BytesIO:
del ttl_hash
@_run_in_executor
def buffer_inner(resource: Resource) -> BytesIO:
_logger.info(f"updating {resource}")
print(f"updating {resource}")
buffer = BytesIO()
resource.write_to(buffer)
return buffer
resource = CLIENT.resource(remote_path)
return await buffer_inner(resource)
class DavFile:
__instances: Optional[list["DavFile"]] = None
__scheduler = None
def __init__(self, remote_path: Any) -> None:
self.__remote_path = remote_path
def __init__(self, resource: Resource, refresh: bool = True) -> None:
self.__resource: Resource = resource
self.__buffer = BytesIO()
self.__lock = Lock()
# register
if DavFile.__instances is None:
DavFile.__instances = []
if refresh:
DavFile.__instances.append(self)
async def download(self) -> None:
@run_in_executor
def download_inner() -> None:
self.__resource.write_to(self.__buffer)
_logger.info(f"updating {self.__resource}")
with self.__lock:
self.__buffer.seek(0)
self.__buffer.truncate(0)
await download_inner()
@classmethod
def refresh(cls, refresh_interval: int = 60) -> None:
if cls.__scheduler is not None:
cls.__scheduler.reschedule_job(
job_id=cls.__name__,
trigger="interval",
seconds=refresh_interval,
)
return
async def tick() -> None:
for davfile in DavFile.__instances:
await davfile.download()
cls.__scheduler = AsyncIOScheduler()
cls.__scheduler.start()
cls.__scheduler.add_job(tick)
cls.__scheduler.add_job(
tick,
id=cls.__name__,
trigger="interval",
seconds=refresh_interval,
@property
async def __buffer(self) -> BytesIO:
return await _get_buffer(
remote_path=self.__remote_path,
ttl_hash=_get_ttl_hash(20),
)
@property
def bytes(self) -> bytes:
with self.__lock:
self.__buffer.seek(0)
return self.__buffer.read()
async def bytes(self) -> bytes:
buffer = await self.__buffer
def __str__(self) -> str:
return self.bytes.decode(encoding="utf-8")
buffer.seek(0)
return buffer.read()
@property
async def string(self) -> str:
bytes = await self.bytes
return bytes.decode(encoding="utf-8")

View file

@ -1,10 +1,7 @@
import io
import logging
import re
import time
from typing import Iterator, Optional
from io import BytesIO
from typing import Iterator
from async_lru import alru_cache
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from PIL import Image
@ -13,16 +10,9 @@ from webdav3.exceptions import RemoteResourceNotFound
from .. import CLIENT
from ..dav_file import DavFile
_logger = logging.getLogger(__name__)
router = APIRouter(prefix="/image", tags=["image"])
@router.on_event("startup")
async def on_startup():
_logger.debug("image router started")
_re_image_file = re.compile(
r"\.(gif|jpe?g|tiff?|png|bmp)$",
flags=re.IGNORECASE,
@ -68,32 +58,6 @@ async def find_images(
return list(file_names)
async def get_ttl_hash(seconds: int = 20) -> int:
"""
Return the same value withing `seconds` time period
https://stackoverflow.com/a/55900800
"""
return round(time.time() / seconds)
@alru_cache(maxsize=20)
async def get_image_by_name(
file_name: str,
ttl_hash: Optional[int] = None,
) -> io.BytesIO:
del ttl_hash
file = DavFile(CLIENT.resource(f"img/{file_name}"), refresh=False)
print(f"Downloading {file_name}")
await file.download()
img = Image.open(io.BytesIO(file.bytes)).convert("RGB")
img_buffer = io.BytesIO()
img.save(img_buffer, format='JPEG', quality=85)
return img_buffer
@router.get(
"/get/{prefix}",
response_class=StreamingResponse,
@ -114,7 +78,6 @@ async def get_image_by_name(
async def get_image(
prefix: str,
file_names: Iterator[str] = Depends(find_file_names),
ttl_hash: int = Depends(get_ttl_hash),
) -> StreamingResponse:
file_names = list(file_names)
@ -124,7 +87,11 @@ async def get_image(
elif len(file_names) > 1:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
img_buffer = await get_image_by_name(file_names[0], ttl_hash)
img_file = DavFile(f"img/{file_names[0]}")
img = Image.open(BytesIO(await img_file.bytes)).convert("RGB")
img_buffer = BytesIO()
img.save(img_buffer, format='JPEG', quality=85)
img_buffer.seek(0)
return StreamingResponse(

View file

@ -1,48 +1,31 @@
import logging
import re
from typing import Iterator
from fastapi import APIRouter, Depends
from markdown import Markdown
from markdown import markdown
from pydantic import BaseModel
from .. import CLIENT
from ..config import SETTINGS
from ..dav_file import DavFile
router = APIRouter(prefix="/text", tags=["text"])
_logger = logging.getLogger(__name__)
_md = Markdown()
_message = ""
_ticker = ""
_title = ""
@router.on_event("startup")
async def on_startup() -> None:
global _message, _ticker, _title
_message = DavFile(CLIENT.resource("message.txt"))
_ticker = DavFile(CLIENT.resource("ticker.txt"))
_title = DavFile(CLIENT.resource("title.txt"))
DavFile.refresh(60)
_logger.debug("text router started")
@router.get("/message")
async def get_message() -> str:
return _md.convert(
str(_message)
message = await DavFile("message.txt").string
return markdown(
message
)
async def get_ticker_lines() -> Iterator[str]:
ticker = await DavFile("ticker.txt").string
return (
line.strip()
for line in str(_ticker).split("\n")
for line in ticker.split("\n")
if line.strip()
)
@ -61,7 +44,7 @@ async def get_ticker_content_lines(
async def get_ticker_content(
ticker_content_lines: Iterator[str] = Depends(get_ticker_content_lines),
) -> str:
return _md.convert(
return markdown(
SETTINGS.ticker_separator.join(ticker_content_lines)
)
@ -98,6 +81,8 @@ async def get_ticker_commands(
@router.get("/title")
async def get_title() -> str:
return _md.convert(
str(_title)
title = await DavFile("title.txt").string
return markdown(
title
)