feat: seven-send 0.1.0 — Library + CLI für SMS und TTS-Anrufe via seven.io
Wiederverwendbarer Baustein (eigenes Paket), nutzbar in beliebigen Programmen,
Cron-Jobs und Alerts:
- client.py: synchroner SevenClient.send_sms / send_voice (Voice als SSML,
XML-escaped); Result mit ok/code/price/error, best-effort Fehlerbehandlung,
dry_run-Modus
- cli.py: `seven-send sms|voice` mit --dry-run, --json, sauberen Exit-Codes;
API-Key aus --api-key oder $SEVEN_API_KEY
- src-Layout, pyproject (console_script seven-send), MIT-Lizenz
- 12 Tests (Client + CLI), httpx gemockt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:33:12 +02:00
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
import seven_send.cli as cli
|
|
|
|
|
import seven_send.client as cl
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FakeResp:
|
|
|
|
|
status_code = 200
|
|
|
|
|
text = '{"success":"100"}'
|
|
|
|
|
|
|
|
|
|
def json(self):
|
|
|
|
|
return {"success": "100", "messages": [{"success": True}], "total_price": 0.075}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _patch(monkeypatch, cap):
|
|
|
|
|
def fake_post(url, data=None, headers=None, timeout=None):
|
|
|
|
|
cap.append({"url": url, "data": data})
|
|
|
|
|
return FakeResp()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cl.httpx, "post", fake_post)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_sms(monkeypatch, capsys):
|
|
|
|
|
cap = []
|
|
|
|
|
_patch(monkeypatch, cap)
|
|
|
|
|
rc = cli.main(["--api-key", "k", "sms", "--to", "+49", "--text", "Hi"])
|
|
|
|
|
assert rc == 0
|
|
|
|
|
assert cap[0]["url"].endswith("/sms")
|
|
|
|
|
assert "OK" in capsys.readouterr().out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_voice_json(monkeypatch, capsys):
|
|
|
|
|
cap = []
|
|
|
|
|
_patch(monkeypatch, cap)
|
|
|
|
|
rc = cli.main(["--api-key", "k", "--json", "voice", "--to", "+49", "--text", "Hi"])
|
|
|
|
|
assert rc == 0
|
|
|
|
|
data = json.loads(capsys.readouterr().out)
|
|
|
|
|
assert data["ok"] is True and data["channel"] == "voice"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_dry_run(monkeypatch):
|
|
|
|
|
def boom(*a, **k):
|
|
|
|
|
raise AssertionError("kein POST")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cl.httpx, "post", boom)
|
|
|
|
|
assert cli.main(["--dry-run", "sms", "--to", "+49", "--text", "Hi"]) == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_missing_key(monkeypatch, capsys):
|
|
|
|
|
monkeypatch.delenv("SEVEN_API_KEY", raising=False)
|
|
|
|
|
rc = cli.main(["sms", "--to", "+49", "--text", "Hi"])
|
|
|
|
|
assert rc == 2
|
|
|
|
|
assert "Konfig-Fehler" in capsys.readouterr().err
|
2026-06-25 13:00:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_sms_from_flag(monkeypatch):
|
|
|
|
|
monkeypatch.delenv("SEVEN_SMS_FROM", raising=False)
|
|
|
|
|
cap = []
|
|
|
|
|
_patch(monkeypatch, cap)
|
|
|
|
|
cli.main(["--api-key", "k", "sms", "--to", "+49", "--text", "Hi", "--from", "Firma"])
|
|
|
|
|
assert cap[0]["data"]["from"] == "Firma"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_sms_no_from_by_default(monkeypatch):
|
|
|
|
|
monkeypatch.delenv("SEVEN_SMS_FROM", raising=False)
|
|
|
|
|
cap = []
|
|
|
|
|
_patch(monkeypatch, cap)
|
|
|
|
|
cli.main(["--api-key", "k", "sms", "--to", "+49", "--text", "Hi"])
|
|
|
|
|
assert "from" not in cap[0]["data"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_sms_from_env(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("SEVEN_SMS_FROM", "EnvAbs")
|
|
|
|
|
cap = []
|
|
|
|
|
_patch(monkeypatch, cap)
|
|
|
|
|
cli.main(["--api-key", "k", "sms", "--to", "+49", "--text", "Hi"])
|
|
|
|
|
assert cap[0]["data"]["from"] == "EnvAbs"
|