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
|
import logging.config
|
||||||
|
|
||||||
from .config import LogConfig
|
from .config import LogConfig
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue