diff --git a/.gitignore b/.gitignore index 9284eed..e1cbde1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ site/ .ruff_cache/ .pytest_cache/ htmlcov/ +TASKS.md \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index 57930d4..43d24a6 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -56,12 +56,12 @@ message = """# My Message client.send(message, format_as_markdown=True) ``` -## Send a message with an attachment +## Send a file as an attachment ```python client = NtfyClient(topic="Your topic") -client.send("Your message here", attachment="/path/to/your/file.txt") +client.send_file("/path/to/your/file.txt") ``` ## Send a message with priority diff --git a/docs/reference.md b/docs/reference.md index c03d5ae..78ad63f 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1,19 +1,19 @@ # API Reference -::: python_ntfy._ntfy.NtfyClient +::: python_ntfy.client.NtfyClient options: show_root_heading: true show_root_toc_entry: true show_bases: false -::: python_ntfy._send_functions +::: python_ntfy options: - show_root_heading: false - show_root_toc_entry: false - heading_level: 3 - -::: python_ntfy._get_functions - options: - show_root_heading: false - show_root_toc_entry: false - heading_level: 3 + members: + - MessagePriority + - ViewAction + - BroadcastAction + - HttpAction + - MessageSendError + show_root_heading: true + show_root_toc_entry: true + heading_level: 2 diff --git a/pyproject.toml b/pyproject.toml index 38d8980..43356be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,9 @@ dev = [ [tool.ruff] target-version = "py312" +[tool.ruff.lint.isort] +combine-as-imports = true + [tool.ruff.lint] select = [ "E", # PycodeStyle errors diff --git a/python_ntfy/__init__.py b/python_ntfy/__init__.py index 0261772..88c4fda 100644 --- a/python_ntfy/__init__.py +++ b/python_ntfy/__init__.py @@ -1,3 +1,17 @@ -from ._ntfy import NtfyClient as NtfyClient +from ._exceptions import MessageSendError as MessageSendError +from ._send_functions import ( + BroadcastAction as BroadcastAction, + HttpAction as HttpAction, + MessagePriority as MessagePriority, + ViewAction as ViewAction, +) +from .client import NtfyClient as NtfyClient -__all__ = ["NtfyClient"] +__all__ = [ + "BroadcastAction", + "HttpAction", + "MessagePriority", + "MessageSendError", + "NtfyClient", + "ViewAction", +] diff --git a/python_ntfy/_ntfy.py b/python_ntfy/_ntfy.py index 7bb4a20..4e3b533 100644 --- a/python_ntfy/_ntfy.py +++ b/python_ntfy/_ntfy.py @@ -1,134 +1,19 @@ -"""This module provides the NtfyClient class for interacting with the ntfy notification service. +"""Compatibility shim for historical import path. -The NtfyClient class allows users to send notifications, files, and perform various actions -through the ntfy.sh service. It also supports retrieving cached messages. +Prefer importing from `python_ntfy` package root: -Typical usage example: + from python_ntfy import NtfyClient - client = NtfyClient(topic="my_topic") - client.send("Hello, World!") +This module re-exports `NtfyClient` to maintain backward compatibility with +older references like `python_ntfy._ntfy.NtfyClient`. """ -import os +from warnings import warn -from ._get_functions import get_cached_messages -from ._send_functions import ( - BroadcastAction, - HttpAction, - MessagePriority, - ViewAction, - send, - send_file, -) - - -class GetFunctionsMixin: - """Mixin for getting messages.""" - - get_cached_messages = get_cached_messages - - -class SendFunctionsMixin: - """Mixin for sending messages.""" - - send = send - send_file = send_file - BroadcastAction = BroadcastAction - HttpAction = HttpAction - MessagePriority = MessagePriority - ViewAction = ViewAction - - -class NtfyClient(GetFunctionsMixin, SendFunctionsMixin): - """A class for interacting with the ntfy notification service.""" - - def __init__( - self, - topic: str, - server: str = "https://ntfy.sh", - auth: tuple[str, str] | str | None = None, - ) -> None: - """Itinialize the NtfyClient. - - Args: - topic: The topic to use for this client - server: The server to connect to. Must include the protocol (http/https) - auth: The authentication credentials to use for this client. Takes precedence over environment variables. Can be a tuple of (user, password) or a token. - - Returns: - None - - Exceptions: - None - - Examples: - client = NtfyClient(topic="my_topic") - """ - self._server = os.environ.get("NTFY_SERVER") or server - self._topic = topic - self.__set_url(self._server, topic) - self._auth: tuple[str, str] | None = self._resolve_auth(auth) +from .client import NtfyClient as NtfyClient - def _resolve_auth( - self, auth: tuple[str, str] | str | None - ) -> tuple[str, str] | None: - """Resolve the authentication credentials. - - Args: - auth: The authentication credentials to use for this client. Takes precedence over environment variables. Can be a tuple of (user, password) or a token string. - - Returns: - tuple[str, str] | None: The authentication credentials. - """ - # If the user has supplied credentials, use them (including empty string) - if auth is not None: - if isinstance(auth, tuple): - return auth - if isinstance(auth, str): - return ("", auth) - - # Otherwise, check environment variables - user = os.environ.get("NTFY_USER") - password = os.environ.get("NTFY_PASSWORD") - token = os.environ.get("NTFY_TOKEN") - - if user and password: - return (user, password) - - if token: - return ("", token) - - # If no credentials are found, return None - return None - - def __set_url( - self, - server, - topic, - ) -> None: - self.url = server.strip("/") + "/" + topic - - def set_topic( - self, - topic: str, - ) -> None: - """Set a new topic for the client. - - Args: - topic: The topic to set for this client. - - Returns: - None - """ - self._topic = topic - self.__set_url(self._server, self._topic) - - def get_topic( - self, - ) -> str: - """Get the current topic. - - Returns: - str: The current topic. - """ - return self._topic +warn( + "`python_ntfy._ntfy` is deprecated and will be removed in a future version. Please import from `python_ntfy` instead.", + DeprecationWarning, + stacklevel=2, +) diff --git a/python_ntfy/client.py b/python_ntfy/client.py new file mode 100644 index 0000000..73a84c6 --- /dev/null +++ b/python_ntfy/client.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import os +from datetime import datetime +from pathlib import Path + +from ._get_functions import get_cached_messages as _get_cached_messages +from ._send_functions import ( + BroadcastAction as _BroadcastAction, + HttpAction as _HttpAction, + MessagePriority as _MessagePriority, + ViewAction as _ViewAction, + send as _send_message, + send_file as _send_file, +) + + +class NtfyClient: + """A client for interacting with the ntfy notification service.""" + + # Backwards-compatible attribute exposure (discouraged for new code): + BroadcastAction = _BroadcastAction + HttpAction = _HttpAction + MessagePriority = _MessagePriority + ViewAction = _ViewAction + + def __init__( + self, + topic: str, + server: str = "https://ntfy.sh", + auth: tuple[str, str] | str | None = None, + ) -> None: + """Initialize the client. + + Args: + topic: The topic to use for this client. + server: The server base URL (must include protocol, e.g., https://). + auth: Credentials for this client. Takes precedence over environment + variables. May be a tuple ``(user, password)`` for Basic auth + or a token string for Bearer auth. + """ + self._server = os.environ.get("NTFY_SERVER") or server + self._topic = topic + self.__set_url(self._server, topic) + self._auth: tuple[str, str] | None = self._resolve_auth(auth) + + def _resolve_auth( + self, auth: tuple[str, str] | str | None + ) -> tuple[str, str] | None: + """Resolve authentication credentials using args or environment variables.""" + # Explicitly provided credentials take precedence (including empty string) + if auth is not None: + if isinstance(auth, tuple): + return auth + if isinstance(auth, str): + return ("", auth) + + # Fallback to environment variables + user = os.environ.get("NTFY_USER") + password = os.environ.get("NTFY_PASSWORD") + token = os.environ.get("NTFY_TOKEN") + + if user and password: + return (user, password) + if token: + return ("", token) + + return None + + def __set_url(self, server: str, topic: str) -> None: + self.url = server.strip("/") + "/" + topic + + def set_topic(self, topic: str) -> None: + """Set a new topic for this client.""" + self._topic = topic + self.__set_url(self._server, self._topic) + + def get_topic(self) -> str: + """Get the current topic.""" + return self._topic + + # Public API methods delegate to internal helpers for implementation + + def send( + self, + message: str, + title: str | None = None, + priority: _MessagePriority = _MessagePriority.DEFAULT, + tags: list[str] | None = None, + actions: list[_ViewAction | _BroadcastAction | _HttpAction] | None = None, + schedule: datetime | None = None, + format_as_markdown: bool = False, + timeout_seconds: int = 5, + email: str | None = None, + ) -> dict: + """Send a text-based message to the server.""" + return _send_message( + self, + message=message, + title=title, + priority=priority, + tags=tags, + actions=actions, + schedule=schedule, + format_as_markdown=format_as_markdown, + timeout_seconds=timeout_seconds, + email=email, + ) + + def send_file( + self, + file: str | Path, + title: str | None = None, + priority: _MessagePriority = _MessagePriority.DEFAULT, + tags: list[str] | None = None, + actions: list[_ViewAction | _BroadcastAction | _HttpAction] | None = None, + schedule: datetime | None = None, + timeout_seconds: int = 30, + email: str | None = None, + ) -> dict: + """Send a file to the server.""" + return _send_file( + self, + file=file, + title=title, + priority=priority, + tags=tags, + actions=actions, + schedule=schedule, + timeout_seconds=timeout_seconds, + email=email, + ) + + def get_cached_messages( + self, + since: str = "all", + scheduled: bool = False, + timeout_seconds: int = 10, + ) -> list[dict]: + """Get cached messages from the server.""" + return _get_cached_messages( + self, + since=since, + scheduled=scheduled, + timeout_seconds=timeout_seconds, + ) diff --git a/tests/conftest.py b/tests/conftest.py index d54bbf0..cb3a220 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,7 @@ from time import sleep from pytest import fixture, mark -from requests import ConnectionError as RequestsConnectionError -from requests import get +from requests import ConnectionError as RequestsConnectionError, get @fixture diff --git a/tests/test_send_file.py b/tests/test_send_file.py index 51cae2c..bc44ef9 100644 --- a/tests/test_send_file.py +++ b/tests/test_send_file.py @@ -2,8 +2,9 @@ from time import sleep import requests +from pytest import raises -from python_ntfy import NtfyClient +from python_ntfy import MessageSendError, NtfyClient from .helpers import topic @@ -45,3 +46,9 @@ def test_send_file_with_email(localhost_server_no_auth, no_auth) -> None: ).content.decode() print(res) assert "test_text.txt" in res + + +def test_send_file_not_found() -> None: + ntfy = NtfyClient(topic=topic) + with raises(MessageSendError): + ntfy.send_file(file="not_found.txt") diff --git a/tests/test_send_message.py b/tests/test_send_message.py index 1709053..2577184 100644 --- a/tests/test_send_message.py +++ b/tests/test_send_message.py @@ -6,8 +6,7 @@ import requests from pytest import raises -from python_ntfy import NtfyClient -from python_ntfy._exceptions import MessageSendError +from python_ntfy import MessageSendError, NtfyClient from .helpers import topic @@ -188,9 +187,3 @@ def test_send_message_with_email(localhost_server_no_auth, no_auth) -> None: ).content.decode() print(res) assert res.index(message) - - -def test_send_file_not_found() -> None: - ntfy = NtfyClient(topic=topic) - with raises(MessageSendError): - ntfy.send_file(file="not_found.txt")