feat: Absender/Titel frei wählbar, kein "Notruf"-Default mehr (0.1.2)

"Notruf" war als Library/CLI-Default falsch — das gehört in die Bridge-Config,
nicht ins Allzweck-Tool. Jetzt:

- kein eingebauter Default-Absender; ohne Angabe nutzt seven.io den
  Kontostandard-Absender (send_sms lässt "from" dann weg)
- CLI --from für SMS (Absender/Titel) und Voice (Caller-ID); zusätzlich
  $SEVEN_SMS_FROM / $SEVEN_VOICE_FROM als persistenter Default
- Tests: Default ohne Absender, per-call-Override, CLI --from + Env (19 gesamt)
- README: Absender/Titel dokumentiert

Die Bridge übergibt sms_from="Notruf" weiterhin explizit -> Voice Assistant
unverändert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-25 13:00:02 +02:00
commit 1344d1f7e9
7 changed files with 64 additions and 10 deletions

View file

@ -19,12 +19,18 @@ pip install -e ".[dev]"
export SEVEN_API_KEY=dein-key export SEVEN_API_KEY=dein-key
seven-send sms --to +4915112345678 --text "Hallo Welt" seven-send sms --to +4915112345678 --text "Hallo Welt"
seven-send sms --to +4915112345678 --text "Hallo Welt" --from "MeineFirma" # Absender/Titel
seven-send voice --to +4915112345678 --text "Dies wird vorgelesen." --voice de-de-female seven-send voice --to +4915112345678 --text "Dies wird vorgelesen." --voice de-de-female
seven-send --dry-run sms --to +49… --text "nur testen" # nichts senden seven-send --dry-run sms --to +49… --text "nur testen" # nichts senden
seven-send --json sms --to +49… --text "..." # maschinenlesbar seven-send --json sms --to +49… --text "..." # maschinenlesbar
``` ```
**Absender/Titel:** Per `--from` setzbar (SMS: alphanumerischer Titel oder Nummer;
Voice: verifizierte Caller-ID). Ohne `--from` greift `$SEVEN_SMS_FROM` /
`$SEVEN_VOICE_FROM`, sonst der **Kontostandard-Absender** von seven.io — es gibt
**keinen** eingebauten Default-Absender.
Exit-Codes: `0` Erfolg · `1` Sendefehler · `2` Konfig-/Aufruf-Fehler. Exit-Codes: `0` Erfolg · `1` Sendefehler · `2` Konfig-/Aufruf-Fehler.
## Library ## Library
@ -32,7 +38,7 @@ Exit-Codes: `0` Erfolg · `1` Sendefehler · `2` Konfig-/Aufruf-Fehler.
```python ```python
from seven_send import SevenClient from seven_send import SevenClient
c = SevenClient(api_key, sms_from="Notruf", voice_name="de-de-female") c = SevenClient(api_key, sms_from="MeineFirma", voice_name="de-de-female")
res = c.send_sms("+4915112345678", "Hallo") res = c.send_sms("+4915112345678", "Hallo")
if not res: # bool(res) == res.ok if not res: # bool(res) == res.ok
@ -52,7 +58,7 @@ res = await asyncio.to_thread(c.send_sms, "+49…", "Hallo")
| Parameter | Default | Bedeutung | | Parameter | Default | Bedeutung |
|-----------|---------|-----------| |-----------|---------|-----------|
| `api_key` | | seven.io API-Key (oder `$SEVEN_API_KEY` für die CLI) | | `api_key` | | seven.io API-Key (oder `$SEVEN_API_KEY` für die CLI) |
| `sms_from` | `Notruf` | Absender-ID der SMS (alphanumerisch, max. 11 Zeichen) | | `sms_from` | `None` | Absender-ID/Titel der SMS (alphanumerisch max. 11 Zeichen oder Nummer); leer = Kontostandard |
| `voice_from` | `None` | Caller-ID für Anrufe (verifizierte Nummer) | | `voice_from` | `None` | Caller-ID für Anrufe (verifizierte Nummer) |
| `voice_name` | `de-de-female` | SSML-Stimme für die Anruf-Ansage | | `voice_name` | `de-de-female` | SSML-Stimme für die Anruf-Ansage |
| `dry_run` | `False` | nichts senden, nur protokollieren | | `dry_run` | `False` | nichts senden, nur protokollieren |

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "seven-send" name = "seven-send"
version = "0.1.1" version = "0.1.2"
description = "Kleine Library + CLI zum Verschicken von SMS und TTS-Sprachanrufen über seven.io" description = "Kleine Library + CLI zum Verschicken von SMS und TTS-Sprachanrufen über seven.io"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View file

@ -3,4 +3,4 @@
from seven_send.client import Result, SevenClient, SevenError from seven_send.client import Result, SevenClient, SevenError
__all__ = ["SevenClient", "Result", "SevenError"] __all__ = ["SevenClient", "Result", "SevenError"]
__version__ = "0.1.1" __version__ = "0.1.2"

View file

@ -8,7 +8,7 @@ import logging
import os import os
import sys import sys
from seven_send.client import DEFAULT_SMS_FROM, DEFAULT_VOICE_NAME, Result, SevenClient, SevenError from seven_send.client import DEFAULT_VOICE_NAME, Result, SevenClient, SevenError
def _build_parser() -> argparse.ArgumentParser: def _build_parser() -> argparse.ArgumentParser:
@ -28,12 +28,18 @@ def _build_parser() -> argparse.ArgumentParser:
sms = sub.add_parser("sms", help="SMS senden") sms = sub.add_parser("sms", help="SMS senden")
sms.add_argument("--to", required=True, help="Empfänger (E.164, z. B. +4915112345678)") sms.add_argument("--to", required=True, help="Empfänger (E.164, z. B. +4915112345678)")
sms.add_argument("--text", required=True) sms.add_argument("--text", required=True)
sms.add_argument("--from", dest="sender", default=DEFAULT_SMS_FROM, help="Absender-ID") sms.add_argument(
"--from", dest="sender", default=os.getenv("SEVEN_SMS_FROM") or None,
help="Absender-ID/Titel (Default: $SEVEN_SMS_FROM, sonst seven.io-Kontostandard)",
)
voice = sub.add_parser("voice", help="TTS-Anruf auslösen") voice = sub.add_parser("voice", help="TTS-Anruf auslösen")
voice.add_argument("--to", required=True, help="Empfänger (E.164)") voice.add_argument("--to", required=True, help="Empfänger (E.164)")
voice.add_argument("--text", required=True, help="vorzulesender Text") voice.add_argument("--text", required=True, help="vorzulesender Text")
voice.add_argument("--from", dest="sender", default=None, help="Caller-ID (verifizierte Nummer)") voice.add_argument(
"--from", dest="sender", default=os.getenv("SEVEN_VOICE_FROM") or None,
help="Caller-ID (verifizierte Nummer; Default: $SEVEN_VOICE_FROM)",
)
voice.add_argument("--voice", dest="voice_name", default=DEFAULT_VOICE_NAME, help="SSML-Stimme") voice.add_argument("--voice", dest="voice_name", default=DEFAULT_VOICE_NAME, help="SSML-Stimme")
voice.add_argument("--no-ssml", dest="ssml", action="store_false", help="Text roh ohne SSML senden") voice.add_argument("--no-ssml", dest="ssml", action="store_false", help="Text roh ohne SSML senden")
return p return p

View file

@ -17,8 +17,8 @@ import httpx
logger = logging.getLogger("seven_send") logger = logging.getLogger("seven_send")
BASE_URL = "https://gateway.seven.io/api" BASE_URL = "https://gateway.seven.io/api"
DEFAULT_SMS_FROM = "Notruf"
DEFAULT_VOICE_NAME = "de-de-female" DEFAULT_VOICE_NAME = "de-de-female"
# Kein Default-Absender: ohne Angabe nutzt seven.io den Kontostandard-Absender.
class SevenError(Exception): class SevenError(Exception):
@ -51,7 +51,7 @@ class SevenClient:
self, self,
api_key: str, api_key: str,
*, *,
sms_from: str = DEFAULT_SMS_FROM, sms_from: str | None = None,
voice_from: str | None = None, voice_from: str | None = None,
voice_name: str = DEFAULT_VOICE_NAME, voice_name: str = DEFAULT_VOICE_NAME,
timeout: float = 15.0, timeout: float = 15.0,
@ -70,7 +70,10 @@ class SevenClient:
# -- öffentlich --------------------------------------------------------- # -- öffentlich ---------------------------------------------------------
def send_sms(self, to: str, text: str, *, sender: str | None = None) -> Result: def send_sms(self, to: str, text: str, *, sender: str | None = None) -> Result:
data = {"to": to, "text": text, "from": sender or self.sms_from} data = {"to": to, "text": text}
frm = sender or self.sms_from
if frm: # leer/None -> seven.io-Kontostandard-Absender
data["from"] = frm
return self._post("sms", to, data) return self._post("sms", to, data)
def send_voice( def send_voice(

View file

@ -51,3 +51,27 @@ def test_cli_missing_key(monkeypatch, capsys):
rc = cli.main(["sms", "--to", "+49", "--text", "Hi"]) rc = cli.main(["sms", "--to", "+49", "--text", "Hi"])
assert rc == 2 assert rc == 2
assert "Konfig-Fehler" in capsys.readouterr().err assert "Konfig-Fehler" in capsys.readouterr().err
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"

View file

@ -54,6 +54,21 @@ def test_voice_no_ssml(monkeypatch):
assert cap[0]["data"]["text"] == "roh" assert cap[0]["data"]["text"] == "roh"
def test_send_sms_default_omits_sender(monkeypatch):
# Ohne sms_from kein "from" -> seven.io nutzt den Kontostandard-Absender
cap = []
_patch_post(monkeypatch, cap, FakeResp(payload={"success": "100"}))
SevenClient("key").send_sms("+49", "Hi")
assert "from" not in cap[0]["data"]
def test_send_sms_sender_override(monkeypatch):
cap = []
_patch_post(monkeypatch, cap, FakeResp(payload={"success": "100"}))
SevenClient("key", sms_from="Firma").send_sms("+49", "Hi", sender="Aktion")
assert cap[0]["data"]["from"] == "Aktion" # per-call schlägt Instanz-Default
def test_failure_code(monkeypatch): def test_failure_code(monkeypatch):
cap = [] cap = []
_patch_post(monkeypatch, cap, FakeResp(payload={"success": "401"})) _patch_post(monkeypatch, cap, FakeResp(payload={"success": "401"}))