Skip to content

Commit

Permalink
Add scheduled message functionality (#25)
Browse files Browse the repository at this point in the history
* Add scheduled message functionality

* Bump python-ntfy version to 0.4.0

* Remove test container setup

* Update pyproject.toml with pytest configuration
  • Loading branch information
MatthewCane authored Oct 22, 2024
1 parent 2e34488 commit 9d77b33
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 8 deletions.
5 changes: 0 additions & 5 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Start test containers
uses: hoverkraft-tech/[email protected]
with:
compose-file: tests/assets/test_containers.yml

- name: Run pytest
run: poetry run pytest -v --cov --cov-fail-under=95

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ See the full documentation site at [https://matthewcane.github.io/python-ntfy/](
- Custom servers
- Sending plaintext messages
- Sending Markdown formatted text messages
- Scheduling messages
- Retrieving cached messages
- Scheduled delivery
- Tags
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "python-ntfy"
version = "0.3.8"
version = "0.4.0"
description = "An ntfy library aiming for feature completeness"
authors = ["Matthew Cane <[email protected]>"]
readme = "README.md"
Expand Down Expand Up @@ -74,3 +74,6 @@ ignore = [

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
13 changes: 12 additions & 1 deletion python_ntfy/_send_functions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Optional, Union
Expand Down Expand Up @@ -133,6 +134,7 @@ def send(
priority: MessagePriority = MessagePriority.DEFAULT,
tags: Optional[list] = None,
actions: Optional[list[Union[ViewAction, BroadcastAction, HttpAction]]] = None,
schedule: Optional[datetime] = None,
format_as_markdown: bool = False,
timeout_seconds: int = 5,
) -> dict:
Expand All @@ -147,9 +149,10 @@ def send(
priority: The priority of the message.
tags: A list of tags to attach to the message. Can be an emoji short code.
actions: A list of Actions objects to attach to the message.
schedule: The time to schedule the message to be sent.
format_as_markdown: If true, the message will be formatted as markdown.
additional_topics: A list of additional topics to send the message to.
timeout_seconds: The number of seconds to wait before timing out.
timeout_seconds: The number of seconds to wait before timing out the reqest to the server.
Returns:
dict: The response from the server.
Expand All @@ -176,6 +179,9 @@ def send(
if len(actions) > 0:
headers["Actions"] = " ; ".join([action.to_header() for action in actions])

if schedule:
headers["Delay"] = str(int(schedule.timestamp()))

return json.loads(
requests.post(
url=self.url,
Expand All @@ -194,6 +200,7 @@ def send_file(
priority: MessagePriority = MessagePriority.DEFAULT,
tags: Optional[list] = None,
actions: Optional[list[Union[ViewAction, BroadcastAction, HttpAction]]] = None,
schedule: Optional[datetime] = None,
timeout_seconds: int = 30,
) -> dict:
"""Sends a file to the server.
Expand All @@ -204,6 +211,7 @@ def send_file(
priority: The priority of the message. Optional, defaults to MessagePriority.
tags: A list of tags to attach to the message. Can be an emoji short code.
actions: A list of ActionButton objects to attach to the message.
schedule: The time to schedule the message to be sent. Must be more than 10 seconds away and less than 3 days in the future.
timeout_seconds: The number of seconds to wait before timing out.
Returns:
Expand All @@ -228,6 +236,9 @@ def send_file(
"Actions": " ; ".join([action.to_header() for action in actions]),
}

if schedule:
headers["Delay"] = str(int(schedule.timestamp()))

with Path(file).open("rb") as f:
return json.loads(
requests.post(
Expand Down
25 changes: 24 additions & 1 deletion tests/test_get_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@
async is used here because we have to wait for the messages to be sent
before we can get them, so a sleep is required.
Additional, because we are checking the first message in the list, we
need to make sure that only the message sent for testing is in the topic
so we generate a random topic for each test.
"""

import asyncio
import json
from datetime import datetime

import pytest

from python_ntfy import NtfyClient

from .helpers import random_string, topic
from .helpers import random_string


@pytest.mark.asyncio
async def test_get_topic(localhost_server_no_auth, no_auth) -> None:
message = random_string()
topic = random_string(5)
ntfy = NtfyClient(topic=topic)
ntfy.send(message=message)
await asyncio.sleep(1)
Expand All @@ -30,10 +36,27 @@ async def test_get_topic(localhost_server_no_auth, no_auth) -> None:
@pytest.mark.asyncio
async def test_get_topic_with_limit(localhost_server_no_auth, no_auth) -> None:
message = random_string()
topic = random_string(5)
ntfy = NtfyClient(topic=topic)
ntfy.send(message=message)
await asyncio.sleep(1)
response = ntfy.get_cached_messages(since="1m")
print(json.dumps(response, indent=2))
assert response[0]["topic"] == topic
assert response[0]["message"] == message


@pytest.mark.asyncio
async def test_get_topic_with_scheduled(localhost_server_no_auth, no_auth) -> None:
message = random_string()
topic = random_string(5)
ts = datetime.fromtimestamp(int(datetime.now().timestamp()) + 10)
ntfy = NtfyClient(topic=topic)
response = ntfy.send(message=message, schedule=ts)
await asyncio.sleep(1)

response = ntfy.get_cached_messages(scheduled=True)
print(json.dumps(response, indent=2))
assert response[0]["topic"] == topic
assert response[0]["message"] == message
assert response[0]["time"] == int(ts.timestamp())
11 changes: 11 additions & 0 deletions tests/test_send_file.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

from python_ntfy import NtfyClient

from .helpers import topic
Expand All @@ -15,3 +17,12 @@ def test_send_image_file(localhost_server_no_auth, no_auth) -> None:
response = ntfy.send_file("tests/assets/test_image.png")
assert response["attachment"]["name"] == "test_image.png"
assert response["attachment"]["type"] == "image/png"


def test_send_scheduled_file(localhost_server_no_auth, no_auth) -> None:
ts = datetime.fromtimestamp(int(datetime.now().timestamp()) + 10)
ntfy = NtfyClient(topic=topic)
response = ntfy.send_file("tests/assets/test_text.txt", schedule=ts)
assert response["attachment"]["name"] == "test_text.txt"
assert response["attachment"]["type"] == "text/plain; charset=utf-8"
assert response["time"] == int(ts.timestamp())
11 changes: 11 additions & 0 deletions tests/test_send_message.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
from os import environ
from pathlib import Path

Expand Down Expand Up @@ -127,3 +128,13 @@ def test_send_with_http_action(localhost_server_no_auth, no_auth) -> None:
assert response["event"] == "message"
assert response["topic"] == topic
assert response["actions"] is not None


def test_send_scheduled_message(localhost_server_no_auth, no_auth) -> None:
ntfy = NtfyClient(topic=topic)
ts = datetime.fromtimestamp(int(datetime.now().timestamp()) + 10)
response = ntfy.send(message="test_send_scheduled_message", schedule=ts)
print(ts, response)
assert response["event"] == "message"
assert response["topic"] == topic
assert response["time"] == int(ts.timestamp())

0 comments on commit 9d77b33

Please sign in to comment.