Compare commits

..

No commits in common. "6709284ed9432f89d0a212c6068adb88e90f78c0" and "22846c3d41c55f8ce40e3c5aa75fa189fd80bc2a" have entirely different histories.

12 changed files with 534 additions and 944 deletions

View file

@ -1,19 +1,12 @@
# See here for image contents: https://github.com/devcontainers/images/blob/main/src/python/.devcontainer/Dockerfile # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
# - 3, 3.11, 3.10, 3.9, 3.8, 3.7, 3.6 ARG VARIANT="3.10-bullseye"
# - 3-bullseye, 3.11-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye
# - 3-buster, 3.11-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT="3.11-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
# [Choice] Node.js version: none, lts/*, 18, 16, 14, 12, 10 # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none" ARG NODE_VERSION="none"
RUN set -ex; \ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
\
if [ "${NODE_VERSION}" != "none" ]; then \
su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; \
fi
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/ # COPY requirements.txt /tmp/pip-tmp/
@ -27,7 +20,7 @@ RUN set -ex; \
RUN set -ex; \ RUN set -ex; \
\ \
export DEBIAN_FRONTEND=noninteractive; \ export DEBIAN_FRONTEND=noninteractive; \
apt-get update; apt-get install --yes --no-install-recommends \ apt-get update; apt-get -y install --no-install-recommends \
git-flow \ git-flow \
libmagic1 \ libmagic1 \
; rm -rf /var/lib/apt/lists/*; ; rm -rf /var/lib/apt/lists/*;
@ -35,4 +28,5 @@ RUN set -ex; \
# [Optional] Uncomment this line to install global node packages. # [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1 # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
RUN su vscode -c "curl -sSL https://install.python-poetry.org | python3 -" 2>&1 USER vscode
RUN curl -sSL https://install.python-poetry.org | python3 -

View file

@ -1,51 +1,46 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// README at: https://github.com/devcontainers/templates/tree/main/src/python // https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3
{ {
"name": "OVD API", "name": "Python 3",
"build": { "build": {
"dockerfile": "Dockerfile", "dockerfile": "Dockerfile",
"context": "..", "context": "..",
"args": { "args": {
// Update 'VARIANT' to pick a Python version. // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
// Append -bullseye or -buster to pin to an OS version. // Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon. // Use -bullseye variants on local on arm64/Apple Silicon.
// "VARIANT": "3.11-bullseye", "VARIANT": "3.9",
// Options // Options
"NODE_VERSION": "none" "NODE_VERSION": "none"
} }
}, },
"containerEnv": { // Set *default* container specific settings.json values on container create.
"TZ": "Europe/Berlin" "settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
}, },
// Configure tool-specific properties. // Add the IDs of extensions you want installed when the container is created.
"customizations": { "extensions": [
// Configure properties specific to VS Code. "ms-python.python",
"vscode": { "ms-python.vscode-pylance",
// Set *default* container specific settings.json values on container create. "be5invis.toml"
"settings": { ],
"python.defaultInterpreterPath": "/usr/local/bin/python",
"terminal.integrated.defaultProfile.linux": "zsh"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"be5invis.toml",
"mhutchie.git-graph",
"ms-python.python",
"ms-python.black-formatter",
// "ms-python.flake8",
"ms-python.isort",
"ms-python.vscode-pylance"
]
}
},
// Use 'postStartCommand' to run commands after the container is started.
"postStartCommand": "poetry install"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt", // "postCreateCommand": "pip3 install --user -r requirements.txt",
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. "postStartCommand": "poetry install",
// "remoteUser": "root" // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
} }

View file

@ -8,13 +8,7 @@
"name": "Main Module", "name": "Main Module",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"module": "ovdashboard_api.main", "module": "ovdashboard_api",
"pythonArgs": [
"-Xfrozen_modules=off",
],
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
},
"justMyCode": true "justMyCode": true
} }
] ]

View file

@ -1,22 +1,17 @@
{ {
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.linting.enabled": true, "python.linting.enabled": true,
"python.linting.pylintEnabled": false, "python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true, "python.linting.flake8Enabled": true,
"python.languageServer": "Pylance", "python.languageServer": "Pylance",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": true
}, },
"git.closeDiffOnOperation": true, "git.closeDiffOnOperation": true,
"python.analysis.typeCheckingMode": "basic", "python.analysis.typeCheckingMode": "basic"
"python.analysis.diagnosticMode": "workspace",
"python.testing.pytestArgs": [
"test"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.formatting.provider": "none",
} }

View file

@ -9,8 +9,11 @@ from logging.config import dictConfig
from pydantic import BaseModel from pydantic import BaseModel
from .app import app
from .settings import SETTINGS from .settings import SETTINGS
__all__ = ["app"]
class LogConfig(BaseModel): class LogConfig(BaseModel):
""" """
@ -19,23 +22,23 @@ class LogConfig(BaseModel):
""" """
# Logging config # Logging config
version: int = 1 version = 1
disable_existing_loggers: bool = False disable_existing_loggers = False
formatters: dict = { formatters = {
"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,

View file

@ -9,7 +9,7 @@ def main() -> None:
""" """
uvicorn_run( uvicorn_run(
app="ovdashboard_api.app:app", app="ovdashboard_api:app",
host="0.0.0.0", host="0.0.0.0",
port=8000, port=8000,
reload=not SETTINGS.production_mode, reload=not SETTINGS.production_mode,

View file

@ -30,29 +30,33 @@ app = FastAPI(
redoc_url=SETTINGS.redoc_url, redoc_url=SETTINGS.redoc_url,
) )
app.add_event_handler("startup", webdav_check)
@app.on_event("startup")
async def add_middlewares() -> None:
if SETTINGS.production_mode:
# Mount frontend in production mode
app.mount(
path="/",
app=StaticFiles(
directory=SETTINGS.ui_directory,
html=True,
),
name="frontend",
)
else:
# Allow CORS in debug mode
app.add_middleware(
CORSMiddleware,
allow_origins=[
"*",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)
app.include_router(v1_router) app.include_router(v1_router)
webdav_check()
if SETTINGS.production_mode:
# Mount frontend in production mode
app.mount(
path="/",
app=StaticFiles(
directory=SETTINGS.ui_directory,
html=True,
),
name="frontend",
)
else:
# Allow CORS in debug mode
app.add_middleware(
CORSMiddleware,
allow_origins=[
"*",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)

View file

@ -4,10 +4,10 @@ Python representation of the "config.txt" file inside the WebDAV directory.
from io import BytesIO from io import BytesIO
from logging import getLogger from logging import getLogger
from tomllib import loads as toml_loads
from typing import Any from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
from tomli import loads as toml_loads
from tomli_w import dump as toml_dump from tomli_w import dump as toml_dump
from webdav3.exceptions import RemoteResourceNotFound from webdav3.exceptions import RemoteResourceNotFound
@ -119,7 +119,9 @@ class Config(BaseModel):
dav_file = DavFile(SETTINGS.config_path) dav_file = DavFile(SETTINGS.config_path)
try: try:
cfg = cls.parse_obj(toml_loads(await dav_file.as_string)) cfg = cls.parse_obj(
toml_loads(await dav_file.as_string)
)
except RemoteResourceNotFound: except RemoteResourceNotFound:
_logger.warning( _logger.warning(

View file

@ -8,12 +8,12 @@ from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import total_ordering from functools import total_ordering
from logging import getLogger from logging import getLogger
from typing import Annotated, Iterator from typing import Iterator
from cache import AsyncTTL from cache import AsyncTTL
from caldav import Calendar from caldav import Calendar
from caldav.lib.error import ReportError from caldav.lib.error import ReportError
from pydantic import AfterValidator, BaseModel from pydantic import BaseModel, validator
from vobject.base import Component from vobject.base import Component
from .async_helpers import run_in_executor from .async_helpers import run_in_executor
@ -33,9 +33,6 @@ def _string_strip(in_str: str) -> str:
return in_str.strip() return in_str.strip()
StrippedStr = Annotated[str, AfterValidator(lambda s: s.strip())]
@total_ordering @total_ordering
class CalEvent(BaseModel): class CalEvent(BaseModel):
""" """
@ -47,8 +44,8 @@ class CalEvent(BaseModel):
https://icalendar.org/iCalendar-RFC-5545/3-6-1-event-component.html https://icalendar.org/iCalendar-RFC-5545/3-6-1-event-component.html
""" """
summary: StrippedStr = "" summary: str = ""
description: StrippedStr = "" description: str = ""
dtstart: datetime = datetime.utcnow() dtstart: datetime = datetime.utcnow()
dtend: datetime = datetime.utcnow() dtend: datetime = datetime.utcnow()
@ -69,6 +66,16 @@ class CalEvent(BaseModel):
return self.dict() == other.dict() return self.dict() == other.dict()
_validate_summary = validator(
"summary",
allow_reuse=True,
)(_string_strip)
_validate_description = validator(
"description",
allow_reuse=True,
)(_string_strip)
@classmethod @classmethod
def from_vevent(cls, event: Component) -> "CalEvent": def from_vevent(cls, event: Component) -> "CalEvent":
""" """
@ -171,7 +178,10 @@ async def _get_calendar_events(
vobject: Component = event.vobject_instance # type: ignore vobject: Component = event.vobject_instance # type: ignore
yield from vobject.vevent_list yield from vobject.vevent_list
return sorted([CalEvent.from_vevent(vevent) for vevent in await _inner()]) return sorted([
CalEvent.from_vevent(vevent)
for vevent in await _inner()
])
@dataclass(frozen=True) @dataclass(frozen=True)

View file

@ -9,8 +9,7 @@ Pydantic models might have convenience methods attached.
from typing import Any, Optional from typing import Any, Optional
from pydantic import BaseModel, root_validator from pydantic import BaseModel, BaseSettings, root_validator
from pydantic_settings import BaseSettings
class DavSettings(BaseModel): class DavSettings(BaseModel):

1245
api/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,17 +6,18 @@ name = "ovdashboard-api"
version = "0.1.0" version = "0.1.0"
[tool.poetry.dependencies] [tool.poetry.dependencies]
Markdown = "^3.5" Markdown = "^3.4.1"
Pillow = "^10.1.0" Pillow = "^9.2.0"
async-cache = "^1.1.1" caldav = "^0.9.1"
caldav = "^1.3.6" fastapi = "^0.81.0"
fastapi = "^0.103.2" pydantic = {extras = ["dotenv"], version = "^1.9.2"}
pydantic-settings = "^2.0.3" python = "^3.9"
python = "^3.11"
python-magic = "^0.4.27" python-magic = "^0.4.27"
tomli = "^2.0.1"
tomli-w = "^1.0.0" tomli-w = "^1.0.0"
uvicorn = {extras = ["standard"], version = "^0.23.2"} uvicorn = "^0.18.3"
webdavclient3 = "^3.14.6" webdavclient3 = "3.14.5"
async-cache = "^1.1.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
# pytest = "^5.2" # pytest = "^5.2"