krz/daily-poem
Poetry emailed, at your will.
clone: git clone https://gitbay.org/krz/daily-poem.git
756d010b0945ac8cd7b4559ac39d9d4d9c7e9395
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-23T02:18:37Z
.github/workflows/test.yml | 38 ++++++++++++++++++++++ .gitignore | 5 +++ main.py | 80 +++++++++++++++++++++++++--------------------- requirements-dev.in | 2 ++ requirements-dev.txt | 22 +++++++++++++ tests/test_main.py | 47 +++++++++++++++++++++++++++ 6 files changed, 158 insertions(+), 36 deletions(-) new file mode 100644 @@ -0,0 +1,38 @@ +name: Test + +# main.py had never run: it referenced recipient_email where the variable was +# recipient_emails, so it raised NameError before sending anything. Nothing +# would have caught that — ruff's F821 does, in a second. +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: '3.11' + + - name: Install + run: pip install -r requirements-dev.txt + + - uses: astral-sh/ruff-action@v3.2.2 + with: + version: "0.16.4" + + - name: Lint + run: ruff check --no-fix + + - name: Format + run: ruff format --check + + - name: Tests + run: python -m pytest -q new file mode 100644 @@ -0,0 +1,5 @@ + +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ @@ -1,39 +1,47 @@ -# Import Python packages -from email.mime.text import MIMEText +# Fetch a random poem from PoetryDB and mail it. import smtplib -import json +from email.mime.text import MIMEText + import requests -# Send API request for a random poem -json_data = requests.get('https://poetrydb.org/random').json() - -# Extract the poem details from the JSON response -title = json_data[0]['title'] -author = json_data[0]['author'] -line_count = json_data[0]['linecount'] -lines = '' -for line in json_data[0]['lines']: - lines = lines + line + "\n" - -# A test print() statement to ensure the request and parsing processed the data -# correctly -# print(title, "\n", author, "\n\n", lines) - -msg_body = title + "\n" + author + "\n\n" + lines - -# Create plaintext message container -msg = MIMEText(msg_body) - -# Prepare the metadata of the message -sender_email = '' -recipient_emails = '' -msg['Subject'] = 'Your Daily Poem (' + line_count + ' lines)' -msg['From'] = sender_email -msg['To'] = recipient_email - -# Send the message via our own SMTP server, but don't include the -# envelope header. -smtp_server = 'localhost' -s = smtplib.SMTP(smtp_server) -s.sendmail(sender_email, [recipient_emails], msg.as_string()) -s.quit() +POETRY_URL = "https://poetrydb.org/random" +SMTP_SERVER = "localhost" + +# Fill these in before running. +SENDER_EMAIL = "" +RECIPIENT_EMAILS = "" + + +def fetch_poem(url=POETRY_URL): + """Return (title, author, line_count, body) for a random poem.""" + json_data = requests.get(url, timeout=30).json() + poem = json_data[0] + lines = "" + for line in poem["lines"]: + lines = lines + line + "\n" + return poem["title"], poem["author"], poem["linecount"], lines + + +def build_message(title, author, line_count, lines, sender, recipient): + """Return a plaintext MIMEText message for one poem.""" + msg = MIMEText(title + "\n" + author + "\n\n" + lines) + msg["Subject"] = "Your Daily Poem (" + line_count + " lines)" + msg["From"] = sender + msg["To"] = recipient + return msg + + +def main(): + title, author, line_count, lines = fetch_poem() + msg = build_message( + title, author, line_count, lines, SENDER_EMAIL, RECIPIENT_EMAILS + ) + + # Send via the local SMTP server, without the envelope header. + s = smtplib.SMTP(SMTP_SERVER) + s.sendmail(SENDER_EMAIL, [RECIPIENT_EMAILS], msg.as_string()) + s.quit() + + +if __name__ == "__main__": + main() new file mode 100644 @@ -0,0 +1,2 @@ +requests +pytest new file mode 100644 @@ -0,0 +1,22 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-dev.in -o requirements-dev.txt --python-version 3.11 +certifi==2026.7.22 + # via requests +charset-normalizer==3.5.1 + # via requests +idna==3.19 + # via requests +iniconfig==2.3.0 + # via pytest +packaging==26.3 + # via pytest +pluggy==1.6.0 + # via pytest +pygments==2.21.0 + # via pytest +pytest==9.1.1 + # via -r requirements-dev.in +requests==2.34.2 + # via -r requirements-dev.in +urllib3==2.7.0 + # via requests new file mode 100644 @@ -0,0 +1,47 @@ +import unittest +from unittest.mock import patch + +from main import build_message, fetch_poem + + +class FakeResponse: + def __init__(self, payload): + self._payload = payload + + def json(self): + return self._payload + + +class TestBuildMessage(unittest.TestCase): + def test_headers_and_body(self): + msg = build_message("A Title", "An Author", "4", "one\ntwo\n", "from@x", "to@y") + + self.assertEqual(msg["Subject"], "Your Daily Poem (4 lines)") + self.assertEqual(msg["From"], "from@x") + self.assertEqual(msg["To"], "to@y") + self.assertIn("A Title", msg.get_payload()) + self.assertIn("An Author", msg.get_payload()) + self.assertIn("one\ntwo", msg.get_payload()) + + +class TestFetchPoem(unittest.TestCase): + """PoetryDB is stubbed: the test covers the shape this code expects from it, + not the service being up.""" + + PAYLOAD = [ + { + "title": "Sonnet", + "author": "Anon", + "linecount": "2", + "lines": ["first line", "second line"], + } + ] + + def test_fields_are_extracted_and_lines_joined(self): + with patch("main.requests.get", return_value=FakeResponse(self.PAYLOAD)): + title, author, line_count, lines = fetch_poem() + + self.assertEqual(title, "Sonnet") + self.assertEqual(author, "Anon") + self.assertEqual(line_count, "2") + self.assertEqual(lines, "first line\nsecond line\n")