DocStrings
This commit is contained in:
parent
d8ca1da9cb
commit
7a0925d60f
12 changed files with 220 additions and 12 deletions
|
@ -1,3 +1,10 @@
|
|||
"""
|
||||
Package `ovdashboard_api`: Contains the API powering the
|
||||
"OVDashboard" application.
|
||||
|
||||
This file: Sets up logging.
|
||||
"""
|
||||
|
||||
import logging.config
|
||||
|
||||
from .config import LogConfig
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
Some useful helpers for working in async contexts.
|
||||
"""
|
||||
|
||||
from asyncio import get_running_loop
|
||||
from functools import partial, wraps
|
||||
from time import time
|
||||
|
@ -7,7 +11,7 @@ from async_lru import alru_cache
|
|||
|
||||
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/
|
||||
"""
|
||||
|
||||
|
@ -24,13 +28,19 @@ def run_in_executor(f):
|
|||
|
||||
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
|
||||
"""
|
||||
|
||||
return round(time() / seconds)
|
||||
|
||||
|
||||
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):
|
||||
@alru_cache(**decorator_kwargs)
|
||||
@wraps(f)
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
Python representation of the "config.txt" file inside the WebDAV directory.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
|
@ -11,8 +15,8 @@ from .dav_file import DavFile
|
|||
|
||||
class LogConfig(BaseModel):
|
||||
"""
|
||||
Logging configuration to be set for the server.
|
||||
https://stackoverflow.com/a/67937084
|
||||
Logging configuration to be set for the server
|
||||
"""
|
||||
|
||||
LOG_FORMAT: str = "%(levelprefix)s [%(asctime)s] %(name)s: %(message)s"
|
||||
|
@ -41,6 +45,10 @@ class LogConfig(BaseModel):
|
|||
|
||||
|
||||
class ImageConfig(BaseModel):
|
||||
"""
|
||||
Sections "[image.*]" in "config.txt".
|
||||
"""
|
||||
|
||||
mode: str = "RGB"
|
||||
save_params: dict[str, Any] = {
|
||||
"format": "JPEG",
|
||||
|
@ -49,12 +57,20 @@ class ImageConfig(BaseModel):
|
|||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""
|
||||
Main representation of "config.txt".
|
||||
"""
|
||||
|
||||
ticker_separator: str = " +++ "
|
||||
|
||||
image: ImageConfig = ImageConfig()
|
||||
|
||||
@classmethod
|
||||
async def get(cls) -> "Config":
|
||||
"""
|
||||
Load the configuration instance from the server using `TOML`.
|
||||
"""
|
||||
|
||||
dav_file = DavFile("config.txt")
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
"""
|
||||
Definition of an asyncio compatible CalDAV calendar.
|
||||
|
||||
Caches events using `timed_alru_cache`.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
@ -17,11 +23,25 @@ _logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def _string_strip(in_str: str) -> str:
|
||||
"""
|
||||
Wrapper for str.strip().
|
||||
|
||||
Used to define `pydantic` validators.
|
||||
"""
|
||||
return in_str.strip()
|
||||
|
||||
|
||||
@total_ordering
|
||||
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 = ""
|
||||
description: str = ""
|
||||
dtstart: datetime = datetime.utcnow()
|
||||
|
@ -31,9 +51,17 @@ class CalEvent(BaseModel):
|
|||
frozen = True
|
||||
|
||||
def __lt__(self, other: "CalEvent") -> bool:
|
||||
"""
|
||||
Order Events by start time.
|
||||
"""
|
||||
|
||||
return self.dtstart < other.dtstart
|
||||
|
||||
def __eq__(self, other: "CalEvent") -> bool:
|
||||
"""
|
||||
Compare all properties.
|
||||
"""
|
||||
|
||||
return self.dict() == other.dict()
|
||||
|
||||
_validate_summary = validator(
|
||||
|
@ -48,6 +76,10 @@ class CalEvent(BaseModel):
|
|||
|
||||
@classmethod
|
||||
def from_vevent(cls, event: VEvent) -> "CalEvent":
|
||||
"""
|
||||
Create a CalEvent instance from a `VObject.VEvent` object.
|
||||
"""
|
||||
|
||||
data = {}
|
||||
|
||||
for key in cls().dict().keys():
|
||||
|
@ -64,6 +96,9 @@ class CalEvent(BaseModel):
|
|||
async def _get_calendar(
|
||||
calendar_name: str,
|
||||
) -> Calendar:
|
||||
"""
|
||||
Get a calendar by name using the CalDAV principal object.
|
||||
"""
|
||||
|
||||
@run_in_executor
|
||||
def _inner() -> Calendar:
|
||||
|
@ -76,9 +111,21 @@ async def _get_calendar(
|
|||
async def _get_calendar_events(
|
||||
calendar_name: str,
|
||||
) -> 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
|
||||
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} ...")
|
||||
|
||||
calendar = caldav_principal().calendar(calendar_name)
|
||||
|
@ -119,10 +166,18 @@ async def _get_calendar_events(
|
|||
|
||||
@dataclass(frozen=True)
|
||||
class DavCalendar:
|
||||
"""
|
||||
Object representation of a CalDAV calendar.
|
||||
"""
|
||||
|
||||
calendar_name: str
|
||||
|
||||
@property
|
||||
async def calendar(self) -> Calendar:
|
||||
"""
|
||||
Calendar as `caldav` library representation.
|
||||
"""
|
||||
|
||||
return await _get_calendar(
|
||||
ttl_hash=get_ttl_hash(SETTINGS.cache_seconds),
|
||||
calendar_name=self.calendar_name,
|
||||
|
@ -130,6 +185,10 @@ class DavCalendar:
|
|||
|
||||
@property
|
||||
async def events(self) -> list[CalEvent]:
|
||||
"""
|
||||
Calendar events in object representation.
|
||||
"""
|
||||
|
||||
return await _get_calendar_events(
|
||||
ttl_hash=get_ttl_hash(SETTINGS.cache_seconds),
|
||||
calendar_name=self.calendar_name,
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
Definition of WebDAV and CalDAV clients.
|
||||
"""
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
|
@ -18,11 +22,19 @@ _WEBDAV_CLIENT = WebDAVclient({
|
|||
|
||||
@lru_cache
|
||||
def webdav_resource(remote_path: Any) -> WebDAVResource:
|
||||
"""
|
||||
Gets a resource using the main WebDAV client.
|
||||
"""
|
||||
|
||||
return _WEBDAV_CLIENT.resource(remote_path)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
|
@ -34,4 +46,8 @@ _CALDAV_CLIENT = CalDAVclient(
|
|||
|
||||
|
||||
def caldav_principal() -> CalDAVPrincipal:
|
||||
"""
|
||||
Gets the `Principal` object of the main CalDAV client.
|
||||
"""
|
||||
|
||||
return _CALDAV_CLIENT.principal()
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
"""
|
||||
Definition of an asyncio compatible WebDAV file.
|
||||
|
||||
Caches files using `timed_alru_cache`.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from io import BytesIO
|
||||
|
@ -16,6 +22,9 @@ _logger = logging.getLogger(__name__)
|
|||
async def _get_buffer(
|
||||
remote_path: Any,
|
||||
) -> BytesIO:
|
||||
"""
|
||||
Download file contents into a new `BytesIO` object.
|
||||
"""
|
||||
|
||||
@run_in_executor
|
||||
def _inner(resource: Resource) -> BytesIO:
|
||||
|
@ -30,21 +39,37 @@ async def _get_buffer(
|
|||
|
||||
@dataclass(frozen=True)
|
||||
class DavFile:
|
||||
"""
|
||||
Object representation of a WebDAV file.
|
||||
"""
|
||||
|
||||
remote_path: str
|
||||
|
||||
@property
|
||||
def resource(self) -> Resource:
|
||||
"""
|
||||
WebDAV file handle.
|
||||
"""
|
||||
|
||||
return webdav_resource(self.remote_path)
|
||||
|
||||
@property
|
||||
async def __buffer(self) -> BytesIO:
|
||||
"""
|
||||
File contents as binary stream.
|
||||
"""
|
||||
|
||||
return await _get_buffer(
|
||||
ttl_hash=get_ttl_hash(SETTINGS.cache_seconds),
|
||||
remote_path=self.remote_path,
|
||||
)
|
||||
|
||||
@property
|
||||
def resource(self) -> Resource:
|
||||
return webdav_resource(self.remote_path)
|
||||
|
||||
@property
|
||||
async def bytes(self) -> bytes:
|
||||
"""
|
||||
File contents as binary data.
|
||||
"""
|
||||
|
||||
buffer = await self.__buffer
|
||||
|
||||
buffer.seek(0)
|
||||
|
@ -52,10 +77,18 @@ class DavFile:
|
|||
|
||||
@property
|
||||
async def string(self) -> str:
|
||||
"""
|
||||
File contents as string.
|
||||
"""
|
||||
|
||||
bytes = await self.bytes
|
||||
return bytes.decode(encoding="utf-8")
|
||||
|
||||
async def dump(self, content: bytes) -> None:
|
||||
"""
|
||||
Write bytes into file.
|
||||
"""
|
||||
|
||||
@run_in_executor
|
||||
def _inner() -> None:
|
||||
buffer = BytesIO(content)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Main executable of `ovdashboard_api`.
|
||||
Main script for `ovdashboard_api` module.
|
||||
|
||||
Creates the main `FastAPI` app.
|
||||
|
||||
If run directly, uses `uvicorn` to run the app.
|
||||
"""
|
||||
|
||||
import uvicorn
|
||||
|
@ -30,6 +28,10 @@ app.include_router(main_router)
|
|||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
If the `main` script is run, `uvicorn` is used to run the app.
|
||||
"""
|
||||
|
||||
uvicorn.run(
|
||||
app="ovdashboard_api.main:app",
|
||||
host="0.0.0.0",
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
Dependables for defining Routers.
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, Protocol
|
||||
|
@ -10,7 +14,11 @@ from ..dav_common import caldav_principal, webdav_list
|
|||
|
||||
@dataclass(frozen=True)
|
||||
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)
|
||||
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
|
||||
re: re.Pattern[str]
|
||||
|
||||
|
@ -52,6 +66,10 @@ class FileNameLister:
|
|||
|
||||
@dataclass(frozen=True)
|
||||
class CalendarNameLister:
|
||||
"""
|
||||
Can be called to create an iterator containing calendar names.
|
||||
"""
|
||||
|
||||
async def __call__(self) -> Iterator[str]:
|
||||
return (
|
||||
cal.name
|
||||
|
@ -61,6 +79,13 @@ class CalendarNameLister:
|
|||
|
||||
@dataclass(frozen=True)
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -84,6 +109,14 @@ class PrefixFinder:
|
|||
|
||||
@dataclass(frozen=True)
|
||||
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
|
||||
|
||||
@property
|
||||
|
|
|
@ -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 fastapi import APIRouter, Depends
|
||||
|
|
|
@ -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
|
||||
from io import BytesIO
|
||||
from typing import Iterator
|
||||
|
|
|
@ -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
|
||||
from typing import Iterator
|
||||
|
||||
|
|
|
@ -35,11 +35,19 @@ class Settings(BaseSettings):
|
|||
|
||||
@property
|
||||
def caldav_url(self) -> str:
|
||||
"""
|
||||
Combined CalDAV URL.
|
||||
"""
|
||||
|
||||
return f"{self.dav_protocol}://" + \
|
||||
f"{self.dav_host}{self.caldav_base_url}"
|
||||
|
||||
@property
|
||||
def webdav_url(self) -> str:
|
||||
"""
|
||||
Combined WebDAV URL.
|
||||
"""
|
||||
|
||||
return f"{self.dav_protocol}://" + \
|
||||
f"{self.dav_host}{self.webdav_base_url}"
|
||||
|
||||
|
|
Loading…
Reference in a new issue