1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
"""Helper methods for the library."""
from __future__ import annotations
import base64
from dataclasses import dataclass
from typing import Any
import httpx
from .const import (
DEFAULT_BKGCOLOR,
DEFAULT_DURATION,
DEFAULT_FONTSIZE,
DEFAULT_ICON,
DEFAULT_POSITION,
DEFAULT_TRANSPARENCY,
BkgColor,
FontSize,
Position,
Transparency,
)
from .exceptions import ConnectError, InvalidImage, InvalidImageData
@dataclass
class ImageSource:
"""Image source from url or local path."""
path: str | None = None
url: str | None = None
auth: httpx.Auth | None = None
@classmethod
def from_path(cls, path: str) -> ImageSource:
"""Initiate image source class."""
return cls(path=path)
@classmethod
def from_url(
cls,
url: str,
username: str | None = None,
password: str | None = None,
auth: str | None = None,
) -> ImageSource:
"""Initiate image source class."""
_cls = cls(url=url)
if auth:
if auth not in ["basic", "digest"]:
raise ValueError(f"Invalid auth '{auth}', must be 'basic' or 'digest'")
if username is None or password is None:
raise ValueError("username and password must be specified")
if auth == "basic":
_cls.auth = httpx.BasicAuth(username, password)
else:
_cls.auth = httpx.DigestAuth(username, password)
return _cls
async def async_get_image(self) -> bytes:
"""Load file from path or url."""
if self.path is not None:
try:
with open(self.path, "rb") as file:
return file.read()
except FileNotFoundError as err:
raise InvalidImage(err) from err
if self.url is not None:
try:
async with httpx.AsyncClient(verify=False) as client:
response = await client.get(self.url, auth=self.auth, timeout=30)
except (httpx.ConnectError, httpx.TimeoutException) as err:
raise ConnectError(
f"Error fetching image from {self.url}: {err}"
) from err
if response.status_code != httpx.codes.OK:
raise InvalidImage(f"Error fetching image from {self.url}: {response}")
if "image" not in response.headers["content-type"]:
raise InvalidImage(
f"Response content type is not an image: {response.headers['content-type']}"
)
return response.content
raise ValueError("Either path or url must be specified")
@dataclass
class NotificationParams:
"""Notification parameters.
:param duration: (Optional) Display the notification for the specified period.
Default duration is 5 seconds.
:param position: (Optional) Specify notification position from class Position.
Default is `Positions.BOTTOM_RIGHT`.
:param fontsize: (Optional) Specify text font size from class FontSize.
Default is `FontSizes.MEDIUM`.
:param color: (Optional) Specify background color from class BkgColor.
Default is `BkgColors.GREY`.
:param transparency: (Optional) Specify the background transparency of the notification
from class `Transparency`. Default is 0%.
:param interrupt: (Optional) Setting it to true makes the notification interactive
and can be dismissed or selected to display more details. Default is False
:param icon: (Optional) Attach icon to notification. Construct using ImageSource.
:param image_file: (Optional) Attach image to notification. Construct using ImageSource.
"""
duration: int = DEFAULT_DURATION
position: int = DEFAULT_POSITION
fontsize: FontSize = DEFAULT_FONTSIZE
transparency: int = DEFAULT_TRANSPARENCY
color: BkgColor = DEFAULT_BKGCOLOR
interrupt: bool = False
icon: ImageSource | None = None
image: ImageSource | None = None
@property
def data_params(self) -> dict[str, Any]:
"""Return notification parameters as a dict."""
return {
"duration": self.duration,
"position": self.position,
"fontsize": self.fontsize.value,
"transparency": self.transparency,
"color": self.color.value,
"interrupt": self.interrupt,
}
async def get_images(self) -> dict[str, Any]:
"""Return notification icon and image."""
icon_bytes = base64.b64decode(DEFAULT_ICON)
if self.icon is not None:
icon_bytes = await self.icon.async_get_image()
files = {
"filename": (
"image",
icon_bytes,
"application/octet-stream",
{"Expires": "0"},
)
}
if self.image is not None:
image_bytes = await self.image.async_get_image()
files["filename2"] = (
"image",
image_bytes,
"application/octet-stream",
{"Expires": "0"},
)
return files
@classmethod
def from_dict(cls, kwargs: dict[str, Any]) -> NotificationParams:
"""Initiate notification parameters class."""
_params: dict[str, Any] = {}
if duration := kwargs.get("duration"):
try:
_params["duration"] = int(duration)
except ValueError:
_params["duration"] = DEFAULT_DURATION
if position := kwargs.get("position"):
_params["position"] = Position.from_string(position)
if (font_size := kwargs.get("fontsize")) and hasattr(
FontSize, font_size.upper()
):
_params["fontsize"] = getattr(FontSize, font_size.upper())
if transparency := kwargs.get("transparency"):
_params["transparency"] = Transparency.from_percentage(transparency)
if (color := kwargs.get("color")) and hasattr(BkgColor, color.upper()):
_params["color"] = getattr(BkgColor, color.upper())
if interrupt := kwargs.get("interrupt"):
_params["interrupt"] = bool(interrupt)
if icon := kwargs.get("icon"):
_params["icon"] = create_image_source("icon", icon)
if image := kwargs.get("image"):
_params["image"] = create_image_source("image", image)
return NotificationParams(**_params)
def create_image_source(key: str, data: dict[str, Any]) -> ImageSource:
"""create image source class."""
if isinstance(data, str):
return (
ImageSource.from_url(data)
if data.startswith("http")
else ImageSource.from_path(data)
)
elif isinstance(data, dict) and "path" in data:
return ImageSource.from_path(data["path"])
elif (
isinstance(data, dict) and (url := data.get("url")) and (url.startswith("http"))
):
try:
return ImageSource.from_url(**data)
except ValueError as err:
raise InvalidImageData(f"Invalid '{key}' data: {str(err)}") from err
else:
raise InvalidImageData(f"Invalid '{key}' data")
|