Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ site/
.ruff_cache/
.pytest_cache/
htmlcov/
TASKS.md
4 changes: 2 additions & 2 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions docs/reference.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions python_ntfy/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
139 changes: 12 additions & 127 deletions python_ntfy/_ntfy.py
Original file line number Diff line number Diff line change
@@ -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,
)
146 changes: 146 additions & 0 deletions python_ntfy/client.py
Original file line number Diff line number Diff line change
@@ -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,
)
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading