314 lines
8.0 KiB
Python
314 lines
8.0 KiB
Python
"""
|
|
This module defines base classes for desktop notifications.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import logging
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
from pathlib import Path
|
|
from typing import Any, Callable, ContextManager
|
|
from urllib.parse import unquote, urlparse
|
|
|
|
__all__ = [
|
|
"Capability",
|
|
"FileResource",
|
|
"Resource",
|
|
"Icon",
|
|
"Sound",
|
|
"Attachment",
|
|
"Button",
|
|
"ReplyField",
|
|
"Urgency",
|
|
"AuthorisationError",
|
|
"Notification",
|
|
"DEFAULT_ICON",
|
|
"DEFAULT_SOUND",
|
|
]
|
|
|
|
try:
|
|
from importlib.resources import as_file, files
|
|
|
|
def resource_path(package: str, resource: str) -> ContextManager[Path]:
|
|
return as_file(files(package) / resource)
|
|
|
|
except ImportError:
|
|
from importlib.resources import path as resource_path
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
python_icon_path = resource_path(
|
|
package="desktop_notifier.resources", resource="python.png"
|
|
).__enter__()
|
|
|
|
|
|
def uuid_str() -> str:
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FileResource:
|
|
"""
|
|
A file resource represented by a URI or path
|
|
|
|
Only one of :attr:`path` or :attr:`uri` can be set.
|
|
"""
|
|
|
|
path: Path | None = None
|
|
"""Path to a local file"""
|
|
|
|
uri: str | None = None
|
|
"""URI reference to a file"""
|
|
|
|
def __post_init__(self) -> None:
|
|
fields = dataclasses.fields(self)
|
|
set_fields = [f for f in fields if getattr(self, f.name) != f.default]
|
|
if len(set_fields) > 1:
|
|
raise RuntimeError("Only a single field can be set")
|
|
if len(set_fields) == 0:
|
|
field_names = [f.name for f in fields]
|
|
raise RuntimeError(f"Either of {field_names} must be set")
|
|
|
|
def as_uri(self) -> str:
|
|
"""
|
|
Returns the represented resource as a URI string
|
|
"""
|
|
if self.uri is not None:
|
|
return self.uri
|
|
if self.path is not None:
|
|
return self.path.as_uri()
|
|
raise AttributeError("No path or URI provided")
|
|
|
|
def as_path(self) -> Path:
|
|
"""
|
|
Returns the represented resource as a Path
|
|
|
|
Note that any information about the URI scheme is lost on conversion.
|
|
"""
|
|
if self.path is not None:
|
|
return self.path
|
|
if self.uri is not None:
|
|
parsed_uri = urlparse(self.uri)
|
|
return Path(unquote(parsed_uri.path))
|
|
|
|
raise AttributeError("No path or URI provided")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Resource(FileResource):
|
|
"""
|
|
A resource represented by a resource name, URI or path
|
|
|
|
Only one of :attr:`path`, :attr:`uri` or :attr:`name` can be set.
|
|
"""
|
|
|
|
name: str | None = None
|
|
"""Name of the system resource"""
|
|
|
|
def is_named(self) -> bool:
|
|
"""Returns whether the instance was initialized with ``name``"""
|
|
return self.name is not None
|
|
|
|
def is_file(self) -> bool:
|
|
"""Returns whether the instance was initialized with ``path`` or ``uri``"""
|
|
return self.path is not None or self.uri is not None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Icon(Resource):
|
|
"""An icon represented by an icon name, URI or path"""
|
|
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Attachment(FileResource):
|
|
"""An attachment represented by a URI or path"""
|
|
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Sound(Resource):
|
|
"""A sound represented by a sound name, URI or path"""
|
|
|
|
pass
|
|
|
|
|
|
DEFAULT_ICON: Icon = Icon(path=python_icon_path)
|
|
"""Python icon"""
|
|
|
|
DEFAULT_SOUND: Sound = Sound(name="default")
|
|
"""Default system notification sound"""
|
|
|
|
|
|
class AuthorisationError(Exception):
|
|
"""Raised when we are not authorised to send notifications"""
|
|
|
|
|
|
class Urgency(Enum):
|
|
"""Enumeration of notification levels
|
|
|
|
The interpretation and visuals depend on the platform.
|
|
"""
|
|
|
|
Critical = "critical"
|
|
"""For critical errors."""
|
|
|
|
Normal = "normal"
|
|
"""Default platform notification level."""
|
|
|
|
Low = "low"
|
|
"""Low priority notification."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Button:
|
|
"""A button for interactive notifications"""
|
|
|
|
title: str
|
|
"""The localized button title"""
|
|
|
|
on_pressed: Callable[[], Any] | None = None
|
|
"""Method to call when the button is pressed"""
|
|
|
|
identifier: str = dataclasses.field(default_factory=uuid_str)
|
|
"""A unique identifier to use in callbacks to specify with button was clicked"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReplyField:
|
|
"""A text field for interactive notifications"""
|
|
|
|
title: str = "Reply"
|
|
"""A title for the field itself. On macOS, this will be the title of a button to
|
|
show the field."""
|
|
|
|
button_title: str = "Send"
|
|
"""The title of the button to send the reply"""
|
|
|
|
on_replied: Callable[[str], Any] | None = None
|
|
"""Method to call when the 'reply' button is pressed"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Notification:
|
|
"""A desktop notification
|
|
|
|
Some properties of a notification may be ignored or interpreted differently
|
|
depending on the platform.
|
|
"""
|
|
|
|
title: str
|
|
"""Notification title"""
|
|
|
|
message: str
|
|
"""Notification message"""
|
|
|
|
urgency: Urgency = Urgency.Normal
|
|
"""Notification urgency. Can determine stickiness, notification appearance and
|
|
break through silencing."""
|
|
|
|
icon: Icon | None = None
|
|
"""Icon to use for the notification"""
|
|
|
|
buttons: tuple[Button, ...] = field(default_factory=tuple)
|
|
"""Buttons shown on an interactive notification"""
|
|
|
|
reply_field: ReplyField | None = None
|
|
"""Text field shown on an interactive notification. This can be used for example
|
|
for messaging apps to reply directly from the notification."""
|
|
|
|
on_clicked: Callable[[], Any] | None = None
|
|
"""Method to call when the notification is clicked"""
|
|
|
|
on_dismissed: Callable[[], Any] | None = None
|
|
"""Method to call when the notification is dismissed"""
|
|
|
|
attachment: Attachment | None = None
|
|
"""A file attached to the notification which may be displayed as a preview"""
|
|
|
|
sound: Sound | None = None
|
|
"""A sound to play on notification"""
|
|
|
|
thread: str | None = None
|
|
"""An identifier to group related notifications together, e.g., from a chat space"""
|
|
|
|
timeout: int = -1
|
|
"""Duration in seconds for which the notification is shown"""
|
|
|
|
identifier: str = field(default_factory=uuid_str)
|
|
"""A unique identifier for this notification. Generated automatically if not
|
|
passed by the client."""
|
|
|
|
_buttons_dict: dict[str, Button] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
for button in self.buttons:
|
|
self._buttons_dict[button.identifier] = button
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"<{self.__class__.__name__}(identifier='{self.identifier}', "
|
|
f"title='{self.title}', message='{self.message}')>"
|
|
)
|
|
|
|
|
|
class Capability(Enum):
|
|
"""Notification capabilities that can be supported by a platform"""
|
|
|
|
APP_NAME = auto()
|
|
"""Supports setting a custom app name"""
|
|
|
|
TITLE = auto()
|
|
"""Supports setting a notification title"""
|
|
|
|
MESSAGE = auto()
|
|
"""Supports setting a notification message"""
|
|
|
|
URGENCY = auto()
|
|
"""Supports different urgency levels"""
|
|
|
|
ICON = auto()
|
|
"""Supports custom notification icons"""
|
|
|
|
ICON_FILE = auto()
|
|
"""Supports setting a custom icon from a user-provided file"""
|
|
|
|
ICON_NAME = auto()
|
|
"""Supports setting a named system icon as notification icon"""
|
|
|
|
BUTTONS = auto()
|
|
"""Supports at least two notification buttons"""
|
|
|
|
REPLY_FIELD = auto()
|
|
"""Supports reply fields"""
|
|
|
|
ATTACHMENT = auto()
|
|
"""Supports notification attachments. Allowed file types vary by platform."""
|
|
|
|
ON_CLICKED = auto()
|
|
"""Supports on-clicked callbacks"""
|
|
|
|
ON_DISMISSED = auto()
|
|
"""Supports on-dismissed callbacks"""
|
|
|
|
SOUND = auto()
|
|
"""Supports custom notification sounds"""
|
|
|
|
SOUND_FILE = auto()
|
|
"""Supports setting a custom sound from a user-provided file"""
|
|
|
|
SOUND_NAME = auto()
|
|
"""Supports setting a named system sound as notification sound"""
|
|
|
|
THREAD = auto()
|
|
"""Supports grouping notifications by topic thread"""
|
|
|
|
TIMEOUT = auto()
|
|
"""Supports notification timeouts"""
|