DocStrings

This commit is contained in:
Jörn-Michael Miehe 2022-09-05 12:54:02 +00:00
parent d8ca1da9cb
commit 7a0925d60f
12 changed files with 220 additions and 12 deletions

View file

@ -1,3 +1,10 @@
"""
Package `ovdashboard_api`: Contains the API powering the
"OVDashboard" application.
This file: Sets up logging.
"""
import logging.config import logging.config
from .config import LogConfig from .config import LogConfig

View file

@ -1,3 +1,7 @@
"""
Some useful helpers for working in async contexts.
"""
from asyncio import get_running_loop from asyncio import get_running_loop
from functools import partial, wraps from functools import partial, wraps
from time import time from time import time
@ -7,7 +11,7 @@ from async_lru import alru_cache
def run_in_executor(f): def run_in_executor(f):
""" """
Decorator to make blocking function call asyncio compatible 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/questions/41063331/how-to-use-asyncio-with-existing-blocking-library/
""" """
@ -24,13 +28,19 @@ def run_in_executor(f):
def get_ttl_hash(seconds: int = 20) -> int: def get_ttl_hash(seconds: int = 20) -> int:
""" """
Return the same value within `seconds` time period Return the same value within `seconds` time period.
https://stackoverflow.com/a/55900800 https://stackoverflow.com/a/55900800
""" """
return round(time() / seconds) return round(time() / seconds)
def timed_alru_cache(**decorator_kwargs): def timed_alru_cache(**decorator_kwargs):
"""
Decorator which adds an (unused) param `ttl_hash`
and the `@alru_cache` annotation to a function.
"""
def decorate(f): def decorate(f):
@alru_cache(**decorator_kwargs) @alru_cache(**decorator_kwargs)
@wraps(f) @wraps(f)

View file

@ -1,3 +1,7 @@
"""
Python representation of the "config.txt" file inside the WebDAV directory.
"""
from io import BytesIO from io import BytesIO
from typing import Any from typing import Any
@ -11,8 +15,8 @@ from .dav_file import DavFile
class LogConfig(BaseModel): class LogConfig(BaseModel):
""" """
Logging configuration to be set for the server.
https://stackoverflow.com/a/67937084 https://stackoverflow.com/a/67937084
Logging configuration to be set for the server
""" """
LOG_FORMAT: str = "%(levelprefix)s [%(asctime)s] %(name)s: %(message)s" LOG_FORMAT: str = "%(levelprefix)s [%(asctime)s] %(name)s: %(message)s"
@ -41,6 +45,10 @@ class LogConfig(BaseModel):
class ImageConfig(BaseModel): class ImageConfig(BaseModel):
"""
Sections "[image.*]" in "config.txt".
"""
mode: str = "RGB" mode: str = "RGB"
save_params: dict[str, Any] = { save_params: dict[str, Any] = {
"format": "JPEG", "format": "JPEG",
@ -49,12 +57,20 @@ class ImageConfig(BaseModel):
class Config(BaseModel): class Config(BaseModel):
"""
Main representation of "config.txt".
"""
ticker_separator: str = " +++ " ticker_separator: str = " +++ "
image: ImageConfig = ImageConfig() image: ImageConfig = ImageConfig()
@classmethod @classmethod
async def get(cls) -> "Config": async def get(cls) -> "Config":
"""
Load the configuration instance from the server using `TOML`.
"""
dav_file = DavFile("config.txt") dav_file = DavFile("config.txt")
try: try:

View file

@ -1,3 +1,9 @@
"""
Definition of an asyncio compatible CalDAV calendar.
Caches events using `timed_alru_cache`.
"""
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -17,11 +23,25 @@ _logger = logging.getLogger(__name__)
def _string_strip(in_str: str) -> str: def _string_strip(in_str: str) -> str:
"""
Wrapper for str.strip().
Used to define `pydantic` validators.
"""
return in_str.strip() return in_str.strip()
@total_ordering @total_ordering
class CalEvent(BaseModel): class CalEvent(BaseModel):
"""
A CalDAV calendar event.
Properties are to be named as in the EVENT component of
RFC5545 (iCalendar).
https://icalendar.org/iCalendar-RFC-5545/3-6-1-event-component.html
"""
summary: str = "" summary: str = ""
description: str = "" description: str = ""
dtstart: datetime = datetime.utcnow() dtstart: datetime = datetime.utcnow()
@ -31,9 +51,17 @@ class CalEvent(BaseModel):
frozen = True frozen = True
def __lt__(self, other: "CalEvent") -> bool: def __lt__(self, other: "CalEvent") -> bool:
"""
Order Events by start time.
"""
return self.dtstart < other.dtstart return self.dtstart < other.dtstart
def __eq__(self, other: "CalEvent") -> bool: def __eq__(self, other: "CalEvent") -> bool:
"""
Compare all properties.
"""
return self.dict() == other.dict() return self.dict() == other.dict()
_validate_summary = validator( _validate_summary = validator(
@ -48,6 +76,10 @@ class CalEvent(BaseModel):
@classmethod @classmethod
def from_vevent(cls, event: VEvent) -> "CalEvent": def from_vevent(cls, event: VEvent) -> "CalEvent":
"""
Create a CalEvent instance from a `VObject.VEvent` object.
"""
data = {} data = {}
for key in cls().dict().keys(): for key in cls().dict().keys():
@ -64,6 +96,9 @@ class CalEvent(BaseModel):
async def _get_calendar( async def _get_calendar(
calendar_name: str, calendar_name: str,
) -> Calendar: ) -> Calendar:
"""
Get a calendar by name using the CalDAV principal object.
"""
@run_in_executor @run_in_executor
def _inner() -> Calendar: def _inner() -> Calendar:
@ -76,9 +111,21 @@ async def _get_calendar(
async def _get_calendar_events( async def _get_calendar_events(
calendar_name: str, calendar_name: str,
) -> list[CalEvent]: ) -> list[CalEvent]:
"""
Get a sorted list of events by CalDAV calendar name.
Do not return an iterator here - this result is cached and
an iterator would get consumed.
"""
@run_in_executor @run_in_executor
def _inner() -> Iterator[VEvent]: def _inner() -> Iterator[VEvent]:
"""
Get events by CalDAV calendar name.
This can return an iterator - only the outer function is
cached.
"""
_logger.info(f"updating {calendar_name!r} ...") _logger.info(f"updating {calendar_name!r} ...")
calendar = caldav_principal().calendar(calendar_name) calendar = caldav_principal().calendar(calendar_name)
@ -119,10 +166,18 @@ async def _get_calendar_events(
@dataclass(frozen=True) @dataclass(frozen=True)
class DavCalendar: class DavCalendar:
"""
Object representation of a CalDAV calendar.
"""
calendar_name: str calendar_name: str
@property @property
async def calendar(self) -> Calendar: async def calendar(self) -> Calendar:
"""
Calendar as `caldav` library representation.
"""
return await _get_calendar( return await _get_calendar(
ttl_hash=get_ttl_hash(SETTINGS.cache_seconds), ttl_hash=get_ttl_hash(SETTINGS.cache_seconds),
calendar_name=self.calendar_name, calendar_name=self.calendar_name,
@ -130,6 +185,10 @@ class DavCalendar:
@property @property
async def events(self) -> list[CalEvent]: async def events(self) -> list[CalEvent]:
"""
Calendar events in object representation.
"""
return await _get_calendar_events( return await _get_calendar_events(
ttl_hash=get_ttl_hash(SETTINGS.cache_seconds), ttl_hash=get_ttl_hash(SETTINGS.cache_seconds),
calendar_name=self.calendar_name, calendar_name=self.calendar_name,

View file

@ -1,3 +1,7 @@
"""
Definition of WebDAV and CalDAV clients.
"""
from functools import lru_cache from functools import lru_cache
from typing import Any from typing import Any
@ -18,11 +22,19 @@ _WEBDAV_CLIENT = WebDAVclient({
@lru_cache @lru_cache
def webdav_resource(remote_path: Any) -> WebDAVResource: def webdav_resource(remote_path: Any) -> WebDAVResource:
"""
Gets a resource using the main WebDAV client.
"""
return _WEBDAV_CLIENT.resource(remote_path) return _WEBDAV_CLIENT.resource(remote_path)
@run_in_executor @run_in_executor
def webdav_list(remote_path: str) -> list: def webdav_list(remote_path: str) -> list[str]:
"""
Asynchroneously lists a WebDAV path using the main WebDAV client.
"""
return _WEBDAV_CLIENT.list(remote_path) return _WEBDAV_CLIENT.list(remote_path)
@ -34,4 +46,8 @@ _CALDAV_CLIENT = CalDAVclient(
def caldav_principal() -> CalDAVPrincipal: def caldav_principal() -> CalDAVPrincipal:
"""
Gets the `Principal` object of the main CalDAV client.
"""
return _CALDAV_CLIENT.principal() return _CALDAV_CLIENT.principal()

View file

@ -1,3 +1,9 @@
"""
Definition of an asyncio compatible WebDAV file.
Caches files using `timed_alru_cache`.
"""
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from io import BytesIO from io import BytesIO
@ -16,6 +22,9 @@ _logger = logging.getLogger(__name__)
async def _get_buffer( async def _get_buffer(
remote_path: Any, remote_path: Any,
) -> BytesIO: ) -> BytesIO:
"""
Download file contents into a new `BytesIO` object.
"""
@run_in_executor @run_in_executor
def _inner(resource: Resource) -> BytesIO: def _inner(resource: Resource) -> BytesIO:
@ -30,21 +39,37 @@ async def _get_buffer(
@dataclass(frozen=True) @dataclass(frozen=True)
class DavFile: class DavFile:
"""
Object representation of a WebDAV file.
"""
remote_path: str remote_path: str
@property
def resource(self) -> Resource:
"""
WebDAV file handle.
"""
return webdav_resource(self.remote_path)
@property @property
async def __buffer(self) -> BytesIO: async def __buffer(self) -> BytesIO:
"""
File contents as binary stream.
"""
return await _get_buffer( return await _get_buffer(
ttl_hash=get_ttl_hash(SETTINGS.cache_seconds), ttl_hash=get_ttl_hash(SETTINGS.cache_seconds),
remote_path=self.remote_path, remote_path=self.remote_path,
) )
@property
def resource(self) -> Resource:
return webdav_resource(self.remote_path)
@property @property
async def bytes(self) -> bytes: async def bytes(self) -> bytes:
"""
File contents as binary data.
"""
buffer = await self.__buffer buffer = await self.__buffer
buffer.seek(0) buffer.seek(0)
@ -52,10 +77,18 @@ class DavFile:
@property @property
async def string(self) -> str: async def string(self) -> str:
"""
File contents as string.
"""
bytes = await self.bytes bytes = await self.bytes
return bytes.decode(encoding="utf-8") return bytes.decode(encoding="utf-8")
async def dump(self, content: bytes) -> None: async def dump(self, content: bytes) -> None:
"""
Write bytes into file.
"""
@run_in_executor @run_in_executor
def _inner() -> None: def _inner() -> None:
buffer = BytesIO(content) buffer = BytesIO(content)

View file

@ -1,11 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Main executable of `ovdashboard_api`. Main script for `ovdashboard_api` module.
Creates the main `FastAPI` app. Creates the main `FastAPI` app.
If run directly, uses `uvicorn` to run the app.
""" """
import uvicorn import uvicorn
@ -30,6 +28,10 @@ app.include_router(main_router)
def main() -> None: def main() -> None:
"""
If the `main` script is run, `uvicorn` is used to run the app.
"""
uvicorn.run( uvicorn.run(
app="ovdashboard_api.main:app", app="ovdashboard_api.main:app",
host="0.0.0.0", host="0.0.0.0",

View file

@ -1,3 +1,7 @@
"""
Dependables for defining Routers.
"""
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator, Protocol from typing import Iterator, Protocol
@ -10,7 +14,11 @@ from ..dav_common import caldav_principal, webdav_list
@dataclass(frozen=True) @dataclass(frozen=True)
class NameLister(Protocol): class NameLister(Protocol):
def __call__(self) -> Iterator[str]: """
Can be called to create an iterator containing some names.
"""
async def __call__(self) -> Iterator[str]:
... ...
@ -23,6 +31,12 @@ _RESPONSE_OK = {
@dataclass(frozen=True) @dataclass(frozen=True)
class FileNameLister: class FileNameLister:
"""
Can be called to create an iterator containing file names.
File names listed will be in `remote_path` and will match the RegEx `re`.
"""
remote_path: str remote_path: str
re: re.Pattern[str] re: re.Pattern[str]
@ -52,6 +66,10 @@ class FileNameLister:
@dataclass(frozen=True) @dataclass(frozen=True)
class CalendarNameLister: class CalendarNameLister:
"""
Can be called to create an iterator containing calendar names.
"""
async def __call__(self) -> Iterator[str]: async def __call__(self) -> Iterator[str]:
return ( return (
cal.name cal.name
@ -61,6 +79,13 @@ class CalendarNameLister:
@dataclass(frozen=True) @dataclass(frozen=True)
class PrefixFinder: class PrefixFinder:
"""
Can be called to create an iterator containing some names, all starting
with a given prefix.
All names will be taken from the list produced by the called `lister`.
"""
lister: NameLister lister: NameLister
@property @property
@ -84,6 +109,14 @@ class PrefixFinder:
@dataclass(frozen=True) @dataclass(frozen=True)
class PrefixUnique: class PrefixUnique:
"""
Can be called to determine if a given prefix is unique in the list
produced by the called `finder`.
On success, produces the unique name with that prefix. Otherwise,
throws a HTTPException.
"""
finder: PrefixFinder finder: PrefixFinder
@property @property

View file

@ -1,3 +1,11 @@
"""
Router "calendar" provides:
- listing calendars
- finding calendars by name prefix
- getting calendar events by calendar name prefix
"""
from typing import Iterator from typing import Iterator
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends

View file

@ -1,3 +1,11 @@
"""
Router "image" provides:
- listing image files
- finding image files by name prefix
- getting image files in a uniform format by name prefix
"""
import re import re
from io import BytesIO from io import BytesIO
from typing import Iterator from typing import Iterator

View file

@ -1,3 +1,11 @@
"""
Router "text" provides:
- listing text files
- finding text files by name prefix
- getting image file content as converted Markdown by name prefix
"""
import re import re
from typing import Iterator from typing import Iterator

View file

@ -35,11 +35,19 @@ class Settings(BaseSettings):
@property @property
def caldav_url(self) -> str: def caldav_url(self) -> str:
"""
Combined CalDAV URL.
"""
return f"{self.dav_protocol}://" + \ return f"{self.dav_protocol}://" + \
f"{self.dav_host}{self.caldav_base_url}" f"{self.dav_host}{self.caldav_base_url}"
@property @property
def webdav_url(self) -> str: def webdav_url(self) -> str:
"""
Combined WebDAV URL.
"""
return f"{self.dav_protocol}://" + \ return f"{self.dav_protocol}://" + \
f"{self.dav_host}{self.webdav_base_url}" f"{self.dav_host}{self.webdav_base_url}"