diff --git a/api/.vscode/settings.json b/api/.vscode/settings.json index 2607a64..2de9c9c 100644 --- a/api/.vscode/settings.json +++ b/api/.vscode/settings.json @@ -12,5 +12,6 @@ "editor.codeActionsOnSave": { "source.organizeImports": true }, - "git.closeDiffOnOperation": true + "git.closeDiffOnOperation": true, + "python.analysis.typeCheckingMode": "basic" } \ No newline at end of file diff --git a/api/ovdashboard_api/async_helpers.py b/api/ovdashboard_api/async_helpers.py index 82677df..d765086 100644 --- a/api/ovdashboard_api/async_helpers.py +++ b/api/ovdashboard_api/async_helpers.py @@ -5,25 +5,30 @@ Some useful helpers for working in async contexts. from asyncio import get_running_loop from functools import partial, wraps from time import time +from typing import Awaitable, Callable, TypeVar from async_lru import alru_cache from .settings import SETTINGS +RT = TypeVar("RT") -def run_in_executor(f): + +def run_in_executor( + function: Callable[..., RT] +) -> Callable[..., Awaitable[RT]]: """ Decorator to make blocking a function call asyncio compatible. https://stackoverflow.com/questions/41063331/how-to-use-asyncio-with-existing-blocking-library/ https://stackoverflow.com/a/53719009 """ - @wraps(f) - async def wrapper(*args, **kwargs): + @wraps(function) + async def wrapper(*args, **kwargs) -> RT: loop = get_running_loop() return await loop.run_in_executor( None, - partial(f, *args, **kwargs), + partial(function, *args, **kwargs), ) return wrapper diff --git a/api/ovdashboard_api/config.py b/api/ovdashboard_api/config.py index 0f5229e..77f2bd7 100644 --- a/api/ovdashboard_api/config.py +++ b/api/ovdashboard_api/config.py @@ -69,7 +69,7 @@ class Config(BaseModel): try: return cls.parse_obj( - toml_loads(await dav_file.string) + toml_loads(await dav_file.as_string) ) except RemoteResourceNotFound: diff --git a/api/ovdashboard_api/dav_calendar.py b/api/ovdashboard_api/dav_calendar.py index 98bd885..88c302a 100644 --- a/api/ovdashboard_api/dav_calendar.py +++ b/api/ovdashboard_api/dav_calendar.py @@ -13,7 +13,7 @@ from typing import Iterator from caldav import Calendar from caldav.lib.error import ReportError from pydantic import BaseModel, validator -from vobject.icalendar import VEvent +from vobject.base import Component from .async_helpers import get_ttl_hash, run_in_executor, timed_alru_cache from .config import Config @@ -76,7 +76,7 @@ class CalEvent(BaseModel): )(_string_strip) @classmethod - def from_vevent(cls, event: VEvent) -> "CalEvent": + def from_vevent(cls, event: Component) -> "CalEvent": """ Create a CalEvent instance from a `VObject.VEvent` object. """ @@ -85,7 +85,7 @@ class CalEvent(BaseModel): for key in cls().dict().keys(): try: - data[key] = event.contents[key][0].value + data[key] = event.contents[key][0].value # type: ignore except KeyError: pass @@ -123,7 +123,7 @@ async def _get_calendar_events( search_span = timedelta(days=cfg.calendar.future_days) @run_in_executor - def _inner() -> Iterator[VEvent]: + def _inner() -> Iterator[Component]: """ Get events by CalDAV calendar name. @@ -156,11 +156,9 @@ async def _get_calendar_events( expand=False, ) - return ( - vevent - for event in search_result - for vevent in event.vobject_instance.contents["vevent"] - ) + for event in search_result: + vobject: Component = event.vobject_instance # type: ignore + yield from vobject.vevent_list return sorted([ CalEvent.from_vevent(vevent) @@ -183,7 +181,7 @@ class DavCalendar: """ return await _get_calendar( - ttl_hash=get_ttl_hash(), + ttl_hash=get_ttl_hash(), # type: ignore calendar_name=self.calendar_name, ) @@ -194,6 +192,6 @@ class DavCalendar: """ return await _get_calendar_events( - ttl_hash=get_ttl_hash(), + ttl_hash=get_ttl_hash(), # type: ignore calendar_name=self.calendar_name, ) diff --git a/api/ovdashboard_api/dav_common.py b/api/ovdashboard_api/dav_common.py index 5c7a61a..f93db1e 100644 --- a/api/ovdashboard_api/dav_common.py +++ b/api/ovdashboard_api/dav_common.py @@ -112,6 +112,6 @@ def caldav_list() -> Iterator[str]: """ return ( - cal.name + str(cal.name) for cal in caldav_principal().calendars() ) diff --git a/api/ovdashboard_api/dav_file.py b/api/ovdashboard_api/dav_file.py index ca914fb..f6d8887 100644 --- a/api/ovdashboard_api/dav_file.py +++ b/api/ovdashboard_api/dav_file.py @@ -61,12 +61,12 @@ class DavFile: """ return await _get_buffer( - ttl_hash=get_ttl_hash(), + ttl_hash=get_ttl_hash(), # type: ignore remote_path=self.remote_path, ) @property - async def bytes(self) -> bytes: + async def as_bytes(self) -> bytes: """ File contents as binary data. """ @@ -77,12 +77,12 @@ class DavFile: return buffer.read() @property - async def string(self) -> str: + async def as_string(self) -> str: """ File contents as string. """ - bytes = await self.bytes + bytes = await self.as_bytes return bytes.decode(encoding="utf-8") async def write(self, content: bytes) -> None: diff --git a/api/ovdashboard_api/routers/_common.py b/api/ovdashboard_api/routers/_common.py index f633299..ce55049 100644 --- a/api/ovdashboard_api/routers/_common.py +++ b/api/ovdashboard_api/routers/_common.py @@ -13,7 +13,6 @@ from ..config import Config from ..dav_common import caldav_list, webdav_list -@dataclass(frozen=True) class NameLister(Protocol): """ Can be called to create an iterator containing some names. diff --git a/api/ovdashboard_api/routers/cal_aggregate.py b/api/ovdashboard_api/routers/cal_aggregate.py index 577d83b..036b3cf 100644 --- a/api/ovdashboard_api/routers/cal_aggregate.py +++ b/api/ovdashboard_api/routers/cal_aggregate.py @@ -50,6 +50,6 @@ async def get_aggregate_calendar( return sorted([ event - async for calendar in calendars + async for calendar in calendars # type: ignore for event in (await calendar.events) ]) diff --git a/api/ovdashboard_api/routers/image.py b/api/ovdashboard_api/routers/image.py index b9d70fa..2ef8ab9 100644 --- a/api/ovdashboard_api/routers/image.py +++ b/api/ovdashboard_api/routers/image.py @@ -62,12 +62,12 @@ async def find_images( async def get_image( prefix: str, name: str = Depends(image_unique), -) -> str: +) -> StreamingResponse: cfg = await Config.get() dav_file = DavFile(f"{image_lister.remote_path}/{name}") img = Image.open( - BytesIO(await dav_file.bytes) + BytesIO(await dav_file.as_bytes) ).convert( cfg.image.mode ) diff --git a/api/ovdashboard_api/routers/text.py b/api/ovdashboard_api/routers/text.py index 323ad7b..30eb511 100644 --- a/api/ovdashboard_api/routers/text.py +++ b/api/ovdashboard_api/routers/text.py @@ -34,7 +34,7 @@ text_unique = PrefixUnique(text_finder) async def get_ticker_lines() -> Iterator[str]: - ticker = await DavFile("text/ticker.txt").string + ticker = await DavFile("text/ticker.txt").as_string return ( line.strip() @@ -59,8 +59,9 @@ async def get_ticker_content( ticker_content_lines: Iterator[str] = Depends(get_ticker_content_lines), ) -> str: cfg = await Config.get() - ticker_content_lines = ["", *ticker_content_lines, ""] - ticker_content = cfg.ticker.separator.join(ticker_content_lines) + ticker_content = cfg.ticker.separator.join( + ["", *ticker_content_lines, ""], + ) return ticker_content.strip() @@ -104,7 +105,7 @@ async def find_texts( async def get_text_content( name: str = Depends(text_unique), ) -> str: - return await DavFile(f"{text_lister.remote_path}/{name}").string + return await DavFile(f"{text_lister.remote_path}/{name}").as_string @router.get( diff --git a/api/ovdashboard_api/settings.py b/api/ovdashboard_api/settings.py index 5a6ef44..c41967e 100644 --- a/api/ovdashboard_api/settings.py +++ b/api/ovdashboard_api/settings.py @@ -7,7 +7,7 @@ Converts per-run (environment) variables and config files into the Pydantic models might have convenience methods attached. """ -from typing import Optional +from typing import Any, Optional from pydantic import BaseModel, BaseSettings, root_validator @@ -17,11 +17,11 @@ class DavSettings(BaseModel): Connection to a DAV server. """ - protocol: Optional[str] - host: Optional[str] - username: Optional[str] - password: Optional[str] - path: Optional[str] + protocol: Optional[str] = None + host: Optional[str] = None + username: Optional[str] = None + password: Optional[str] = None + path: Optional[str] = None @property def url(self) -> str: @@ -65,11 +65,13 @@ class Settings(BaseSettings): caldav: DavSettings = DavSettings() class Config: + env_file = ".env" + env_file_encoding = "utf-8" env_nested_delimiter = "__" @root_validator(pre=True) @classmethod - def validate_dav_settings(cls, values): + def validate_dav_settings(cls, values: dict[str, Any]) -> dict[str, Any]: # ensure both settings dicts are created for key in ("webdav", "caldav"): if key not in values: @@ -96,4 +98,4 @@ class Settings(BaseSettings): return values -SETTINGS = Settings(_env_file=".env") +SETTINGS = Settings()