py3.12/refac: partly working

This commit is contained in:
Jörn-Michael Miehe 2023-10-20 10:43:15 +02:00
parent a585d97f9f
commit b8b1c30313
10 changed files with 159 additions and 254 deletions

View file

@ -7,40 +7,34 @@ This file: Sets up logging.
from logging.config import dictConfig from logging.config import dictConfig
from pydantic import BaseModel
from .core.settings import SETTINGS from .core.settings import SETTINGS
# Logging configuration to be set for the server.
# https://stackoverflow.com/a/67937084
class LogConfig(BaseModel): LOG_CONFIG = dict(
""" version=1,
Logging configuration to be set for the server. disable_existing_loggers=False,
https://stackoverflow.com/a/67937084 formatters={
"""
# Logging config
version: int = 1
disable_existing_loggers: bool = False
formatters: dict = {
"default": { "default": {
"()": "uvicorn.logging.DefaultFormatter", "()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s [%(asctime)s] %(name)s: %(message)s", "fmt": "%(levelprefix)s [%(asctime)s] %(name)s: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S", "datefmt": "%Y-%m-%d %H:%M:%S",
}, },
} },
handlers: dict = { handlers={
"default": { "default": {
"formatter": "default", "formatter": "default",
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"stream": "ext://sys.stderr", "stream": "ext://sys.stderr",
}, },
} },
loggers: dict = { loggers={
"ovdashboard_api": { "ovdashboard_api": {
"handlers": ["default"], "handlers": ["default"],
"level": SETTINGS.log_level, "level": SETTINGS.log_level,
}, },
} },
)
dictConfig(LOG_CONFIG)
dictConfig(LogConfig().model_dump())

View file

@ -6,14 +6,19 @@ Main script for `ovdashboard_api` module.
Creates the main `FastAPI` app. Creates the main `FastAPI` app.
""" """
import logging
import time
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from .core.settings import SETTINGS from .core.settings import SETTINGS
from .dav_common import webdav_check from .core.webdav import WebDAV
from .routers import v1_router from .routers import v1_router
_logger = logging.getLogger(__name__)
app = FastAPI( app = FastAPI(
title="OVDashboard API", title="OVDashboard API",
description="This API enables the `OVDashboard` service.", description="This API enables the `OVDashboard` service.",
@ -31,7 +36,11 @@ app = FastAPI(
) )
app.include_router(v1_router) app.include_router(v1_router)
webdav_check()
_logger.info(
"Production mode is %s.",
"enabled" if SETTINGS.production_mode else "disabled",
)
if SETTINGS.production_mode: if SETTINGS.production_mode:
# Mount frontend in production mode # Mount frontend in production mode
@ -44,15 +53,27 @@ if SETTINGS.production_mode:
name="frontend", name="frontend",
) )
for _ in range(SETTINGS.webdav.retries):
if WebDAV._webdav_client.check(""):
break
_logger.warning(
"Waiting for WebDAV connection to %s ...",
repr(SETTINGS.webdav.url),
)
time.sleep(30)
else: else:
assert WebDAV._webdav_client.check("")
# Allow CORS in debug mode # Allow CORS in debug mode
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[
"*",
],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
allow_methods=["*"],
allow_origins=["*"],
expose_headers=["*"], expose_headers=["*"],
) )
_logger.debug("WebDAV connection ok.")

View file

@ -4,13 +4,12 @@ Python representation of the "config.txt" file inside the WebDAV directory.
import tomllib import tomllib
from logging import getLogger from logging import getLogger
from typing import Any, Self from typing import Any
import tomli_w
from pydantic import BaseModel from pydantic import BaseModel
from webdav3.exceptions import RemoteResourceNotFound from webdav3.exceptions import RemoteResourceNotFound
from .caldav import CalDAV # from .caldav import CalDAV
from .settings import SETTINGS from .settings import SETTINGS
from .webdav import WebDAV from .webdav import WebDAV
@ -109,27 +108,27 @@ class Config(BaseModel):
ticker: TickerConfig = TickerConfig() ticker: TickerConfig = TickerConfig()
calendar: CalendarConfig = CalendarConfig() calendar: CalendarConfig = CalendarConfig()
@classmethod
async def get(cls) -> Self:
"""
Load the configuration instance from the server using `TOML`.
"""
try: async def get_config() -> Config:
cfg_str = await WebDAV.read_str(SETTINGS.webdav.config_filename) """
cfg = cls.model_validate(tomllib.loads(cfg_str)) Load the configuration instance from the server using `TOML`.
"""
except RemoteResourceNotFound: try:
_logger.warning( cfg_str = await WebDAV.read_str(SETTINGS.webdav.config_filename)
f"Config file {SETTINGS.webdav.config_filename!r} not found, creating ..." cfg = Config.model_validate(tomllib.loads(cfg_str))
)
cfg = cls() except RemoteResourceNotFound:
cfg.calendar.aggregates["All Events"] = list(await CalDAV.calendars) _logger.warning(
f"Config file {SETTINGS.webdav.config_filename!r} not found, creating ..."
)
await WebDAV.write_str( cfg = Config()
SETTINGS.webdav.config_filename, # cfg.calendar.aggregates["All Events"] = list(await CalDAV.calendars)
tomli_w.dumps(cfg.model_dump()),
)
return cfg # await WebDAV.write_str(
# SETTINGS.webdav.config_filename,
# tomli_w.dumps(cfg.model_dump()),
# )
return cfg

View file

@ -0,0 +1,60 @@
"""
Definition of WebDAV and CalDAV clients.
"""
from logging import getLogger
from os import path
from pathlib import Path
from .. import __file__ as OVD_INIT
from .webdav import WebDAV
_logger = getLogger(__name__)
def webdav_ensure_path(remote_path: str) -> bool:
if WebDAV._webdav_client.check(remote_path):
_logger.debug(
"WebDAV path %s found.",
repr(remote_path),
)
return True
_logger.info(
"WebDAV path %s not found, creating ...",
repr(remote_path),
)
WebDAV._webdav_client.mkdir(remote_path)
return False
def get_skel_path(skel_file: str) -> Path:
skel_path = path.dirname(Path(OVD_INIT).absolute())
return Path(skel_path).joinpath("skel", skel_file)
def webdav_upload_skel(remote_path: str, *skel_files: str) -> None:
for skel_file in skel_files:
_logger.debug(
"Creating WebDAV file %s ...",
repr(skel_file),
)
WebDAV._webdav_client.upload_file(
f"{remote_path}/{skel_file}",
get_skel_path(skel_file),
)
def webdav_ensure_files(remote_path: str, *file_names: str) -> None:
missing_files = (
file_name
for file_name in file_names
if not WebDAV._webdav_client.check(f"{remote_path}/{file_name}")
)
webdav_upload_skel(
remote_path,
*missing_files,
)

View file

@ -1,172 +0,0 @@
"""
Definition of WebDAV and CalDAV clients.
"""
from functools import lru_cache
from logging import getLogger
from os import path
from pathlib import Path
from time import sleep
from typing import Any, Iterator
from asyncify import asyncify
from caldav import DAVClient as CalDAVclient
from caldav import Principal as CalDAVPrincipal
from webdav3.client import Client as WebDAVclient
from webdav3.client import Resource as WebDAVResource
from . import __file__ as OVD_INIT
from .core.settings import SETTINGS
_WEBDAV_CLIENT = WebDAVclient(
{
"webdav_hostname": SETTINGS.webdav.url,
"webdav_login": SETTINGS.webdav.username,
"webdav_password": SETTINGS.webdav.password,
"disable_check": SETTINGS.webdav_disable_check,
}
)
_logger = getLogger(__name__)
def webdav_check() -> None:
"""
Checks if base resources are available.
"""
_logger.info(
"Production mode is %s.",
"enabled" if SETTINGS.production_mode else "disabled",
)
if SETTINGS.production_mode:
for _ in range(SETTINGS.webdav.retries):
if _WEBDAV_CLIENT.check(""):
break
_logger.warning(
"Waiting for WebDAV connection to %s ...",
repr(SETTINGS.webdav.url),
)
sleep(30)
_logger.debug("WebDAV connection ok.")
elif not _WEBDAV_CLIENT.check(""):
_logger.error(
"WebDAV connection to %s FAILED!",
repr(SETTINGS.webdav.url),
)
raise ConnectionError(SETTINGS.webdav.url)
_logger.debug("WebDAV connection ok.")
if not _WEBDAV_CLIENT.check(SETTINGS.webdav_prefix):
_logger.error(
"WebDAV prefix directory %s NOT FOUND, please create it!",
repr(SETTINGS.webdav_prefix),
)
raise FileNotFoundError(SETTINGS.webdav_prefix)
_logger.debug("WebDAV prefix directory found.")
def webdav_ensure_path(remote_path: str) -> bool:
remote_path = f"{SETTINGS.webdav_prefix}/{remote_path}"
if _WEBDAV_CLIENT.check(remote_path):
_logger.debug(
"WebDAV path %s found.",
repr(remote_path),
)
return True
_logger.info(
"WebDAV path %s not found, creating ...",
repr(remote_path),
)
_WEBDAV_CLIENT.mkdir(remote_path)
return False
def get_skel_path(skel_file: str) -> Path:
skel_path = path.dirname(Path(OVD_INIT).absolute())
return Path(skel_path).joinpath("skel", skel_file)
def webdav_upload_skel(remote_path: str, *skel_files: str) -> None:
remote_path = f"{SETTINGS.webdav_prefix}/{remote_path}"
for skel_file in skel_files:
_logger.debug(
"Creating WebDAV file %s ...",
repr(skel_file),
)
_WEBDAV_CLIENT.upload_file(
f"{remote_path}/{skel_file}",
get_skel_path(skel_file),
)
def webdav_ensure_files(remote_path: str, *file_names: str) -> None:
missing_files = (
file_name
for file_name in file_names
if not _WEBDAV_CLIENT.check(
path.join(
SETTINGS.webdav_prefix,
remote_path,
file_name,
)
)
)
webdav_upload_skel(
remote_path,
*missing_files,
)
@lru_cache(maxsize=SETTINGS.cache_size)
def webdav_resource(remote_path: Any) -> WebDAVResource:
"""
Gets a resource using the main WebDAV client.
"""
return _WEBDAV_CLIENT.resource(f"{SETTINGS.webdav_prefix}/{remote_path}")
@asyncify
def webdav_list(remote_path: str) -> list[str]:
"""
Asynchronously lists a WebDAV path using the main WebDAV client.
"""
return _WEBDAV_CLIENT.list(f"{SETTINGS.webdav_prefix}/{remote_path}")
_CALDAV_CLIENT = CalDAVclient(
url=SETTINGS.caldav.url,
username=SETTINGS.caldav.username,
password=SETTINGS.caldav.password,
)
def caldav_principal() -> CalDAVPrincipal:
"""
Gets the `Principal` object of the main CalDAV client.
"""
return _CALDAV_CLIENT.principal()
@asyncify
def caldav_list() -> Iterator[str]:
"""
Asynchronously lists all calendars using the main WebDAV client.
"""
return (str(cal.name) for cal in caldav_principal().calendars())

View file

@ -6,16 +6,19 @@ This file: Main API router definition.
from fastapi import APIRouter from fastapi import APIRouter
from . import aggregate, calendar, file, image, misc, text, ticker # from . import aggregate, calendar, file, image, misc, text, ticker
from . import file, misc
router = APIRouter(prefix="/api/v1") router = APIRouter(prefix="/api/v1")
router.include_router(misc.router) router.include_router(misc.router)
router.include_router(text.router) # router.include_router(text.router)
router.include_router(ticker.router) # router.include_router(ticker.router)
router.include_router(image.router) # router.include_router(image.router)
router.include_router(file.router) router.include_router(file.router)
router.include_router(calendar.router) # router.include_router(calendar.router)
router.include_router(aggregate.router) # router.include_router(aggregate.router)
__all__ = ["router"]

View file

@ -10,8 +10,9 @@ from typing import Iterator, Protocol
from fastapi import HTTPException, status from fastapi import HTTPException, status
from webdav3.exceptions import RemoteResourceNotFound from webdav3.exceptions import RemoteResourceNotFound
from ...config import Config from ...core.caldav import CalDAV
from ...dav_common import caldav_list, webdav_list from ...core.config import get_config
from ...core.webdav import WebDAV
_logger = getLogger(__name__) _logger = getLogger(__name__)
@ -32,7 +33,7 @@ _RESPONSE_OK = {
} }
@dataclass(frozen=True) @dataclass(frozen=True, slots=True)
class FileNameLister: class FileNameLister:
""" """
Can be called to create an iterator containing file names. Can be called to create an iterator containing file names.
@ -55,13 +56,13 @@ class FileNameLister:
@property @property
async def remote_path(self) -> str: async def remote_path(self) -> str:
cfg = await Config.get() cfg = await get_config()
return str(cfg.model_dump()[self.path_name]) return getattr(cfg, self.path_name)
async def __call__(self) -> Iterator[str]: async def __call__(self) -> Iterator[str]:
try: try:
file_names = await webdav_list(await self.remote_path) file_names = await WebDAV.list_files(await self.remote_path)
return (name for name in file_names if self.re.search(name)) return (name for name in file_names if self.re.search(name))
@ -73,29 +74,29 @@ class FileNameLister:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@dataclass(frozen=True) @dataclass(frozen=True, slots=True)
class CalendarNameLister: class CalendarNameLister:
""" """
Can be called to create an iterator containing calendar names. Can be called to create an iterator containing calendar names.
""" """
async def __call__(self) -> Iterator[str]: async def __call__(self) -> Iterator[str]:
return await caldav_list() return iter(await CalDAV.calendars)
@dataclass(frozen=True) @dataclass(frozen=True, slots=True)
class AggregateNameLister: class AggregateNameLister:
""" """
Can be called to create an iterator containing aggregate calendar names. Can be called to create an iterator containing aggregate calendar names.
""" """
async def __call__(self) -> Iterator[str]: async def __call__(self) -> Iterator[str]:
cfg = await Config.get() cfg = await get_config()
return iter(cfg.calendar.aggregates.keys()) return iter(cfg.calendar.aggregates.keys())
@dataclass(frozen=True) @dataclass(frozen=True, slots=True)
class PrefixFinder: class PrefixFinder:
""" """
Can be called to create an iterator containing some names, all starting Can be called to create an iterator containing some names, all starting
@ -124,7 +125,7 @@ class PrefixFinder:
) )
@dataclass(frozen=True) @dataclass(frozen=True, slots=True)
class PrefixUnique: class PrefixUnique:
""" """
Can be called to determine if a given prefix is unique in the list Can be called to determine if a given prefix is unique in the list

View file

@ -11,9 +11,9 @@ from typing import Iterator
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ovdashboard_api.config import Config from ...core.caldav import CalDAV
from ...core.calevent import CalEvent
from ...dav_calendar import CalEvent, DavCalendar from ...core.config import Config
from ._common import AggregateNameLister, PrefixFinder, PrefixUnique from ._common import AggregateNameLister, PrefixFinder, PrefixUnique
from .calendar import calendar_unique from .calendar import calendar_unique

View file

@ -15,8 +15,8 @@ from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from magic import Magic from magic import Magic
from ...dav_common import webdav_ensure_files, webdav_ensure_path # from ...core.dav_common import webdav_ensure_files, webdav_ensure_path
from ...dav_file import DavFile from ...core.webdav import WebDAV
from ._common import FileNameLister, PrefixFinder, PrefixUnique from ._common import FileNameLister, PrefixFinder, PrefixUnique
_logger = getLogger(__name__) _logger = getLogger(__name__)
@ -40,12 +40,12 @@ file_unique = PrefixUnique(file_finder)
async def start_router() -> None: async def start_router() -> None:
_logger.debug(f"{router.prefix} router starting.") _logger.debug(f"{router.prefix} router starting.")
if not webdav_ensure_path(await file_lister.remote_path): # if not webdav_ensure_path(await file_lister.remote_path):
webdav_ensure_files( # webdav_ensure_files(
await file_lister.remote_path, # await file_lister.remote_path,
"logo.svg", # "logo.svg",
"thw.svg", # "thw.svg",
) # )
@router.get( @router.get(
@ -79,8 +79,7 @@ async def get_file(
prefix: str, prefix: str,
name: str = Depends(file_unique), name: str = Depends(file_unique),
) -> StreamingResponse: ) -> StreamingResponse:
dav_file = DavFile(f"{await file_lister.remote_path}/{name}") buffer = BytesIO(await WebDAV.read_bytes(f"{await file_lister.remote_path}/{name}"))
buffer = BytesIO(await dav_file.as_bytes)
mime = _magic.from_buffer(buffer.read(2048)) mime = _magic.from_buffer(buffer.read(2048))
buffer.seek(0) buffer.seek(0)

View file

@ -11,8 +11,8 @@ from socket import AF_INET, SOCK_DGRAM, socket
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ...config import Config, LogoUIConfig, ServerUIConfig from ...core.config import Config, LogoUIConfig, ServerUIConfig, get_config
from ...settings import SETTINGS from ...core.settings import SETTINGS
_logger = getLogger(__name__) _logger = getLogger(__name__)
@ -51,7 +51,7 @@ async def get_version() -> str:
response_model=ServerUIConfig, response_model=ServerUIConfig,
) )
async def get_server_ui_config( async def get_server_ui_config(
cfg: Config = Depends(Config.get), cfg: Config = Depends(get_config),
) -> ServerUIConfig: ) -> ServerUIConfig:
return cfg.server return cfg.server
@ -61,6 +61,6 @@ async def get_server_ui_config(
response_model=LogoUIConfig, response_model=LogoUIConfig,
) )
async def get_logo_ui_config( async def get_logo_ui_config(
cfg: Config = Depends(Config.get), cfg: Config = Depends(get_config),
) -> LogoUIConfig: ) -> LogoUIConfig:
return cfg.logo return cfg.logo