Skip to content
32 changes: 27 additions & 5 deletions .github/scripts/check_strict_types.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import atexit

import posthog
from posthog import FeatureFlagEvaluations, FlagValue, Posthog
from posthog.emails import AsyncEmails, Emails, SendEmailResult
from posthog.ai.evaluations import (
AsyncOfflineEvaluations,
BooleanScorerConfig,
Expand All @@ -40,6 +41,18 @@ span.end()

_ = (flag_value, all_flags, enabled, payload, evaluations, active)

emails = Emails("phs_example")
template_send: SendEmailResult = emails.send(to="[email protected]", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", template_data={"name": "Ada"})
rendered_send: SendEmailResult = emails.send(to="[email protected]", template_key="password-reset", subject="Reset", html="<p>Reset</p>", from_="Acme <[email protected]>", reply_to=["[email protected]"])
local_template_send: SendEmailResult = emails.send(to="[email protected]", template_key="welcome", subject="Hi {{ variables.name }}", text="Hi", template_data={"name": "Ada"})
invite_send: SendEmailResult = emails.send(to="[email protected]", template_key="invite", subject="Invite", html="<p>Invite</p>", cc="[email protected]", bcc=["[email protected]"], headers={"X-Entity-Ref-ID": "order-123"}, tags=[{"name": "category", "value": "invite"}], attachments=[{"filename": "invite.ics", "content": b"BEGIN:VCALENDAR", "content_type": "text/calendar"}, {"filename": "logo.png", "content": "iVBORw0KGgo=", "content_id": "logo"}])
_ = (template_send, rendered_send, local_template_send, invite_send)


async def async_email_types() -> None:
sent: SendEmailResult = await AsyncEmails("phs_example").send(to="[email protected]", template_key="welcome", subject="Hi", html="<p>Hi</p>")
_ = sent


async def offline_evaluation_types(
sync_client: OfflineEvaluations, async_client: AsyncOfflineEvaluations
Expand All @@ -64,15 +77,24 @@ async def offline_evaluation_types(
_ = config
PY

cat > "$tmp/invalid_offline_types.py" <<'PY'
cat > "$tmp/invalid_types.py" <<'PY'
# pyright: strict
from posthog.ai.evaluations import BooleanScorerConfig, CategoricalScorerConfig, NumericPassingRule, NumericScorerConfig, OfflineEvaluations
from posthog.emails import AsyncEmails, Emails

client = OfflineEvaluations(project_id=123, secret_key="phx_example")
NumericPassingRule(operator="gt", threshold=0.8) # expected-type-error
NumericScorerConfig(unknown_setting=True) # expected-type-error
CategoricalScorerConfig(selection_mode="single") # expected-type-error
client.scorers.create(name="Bad config", kind="numeric", config=BooleanScorerConfig(true_is_failure=True)) # expected-type-error
Emails("phs_example").send(to="[email protected]", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", template_key="welcome", subject="Hi", html="<p>Hi</p>") # expected-type-error
Emails("phs_example").send(to="[email protected]", template_key="welcome", html="<p>Hi</p>") # expected-type-error
Emails("phs_example").send(to="[email protected]", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", cc="[email protected]") # expected-type-error
Emails("phs_example").send(to="[email protected]", template_key="welcome", subject="Hi", html="<p>Hi</p>", attachments=[{"filename": "a.txt", "content": [104, 105]}]) # expected-type-error


async def invalid_async_email() -> None:
await AsyncEmails("phs_example").send(to="[email protected]", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", subject="Hi") # expected-type-error
PY

"$tmp/.venv/bin/python" - <<'PY' > "$tmp/public_api_access.py"
Expand Down Expand Up @@ -116,20 +138,20 @@ JSON
cd "$tmp"
"$tmp/.venv/bin/python" -m pyright strict_posthog_types.py public_api_access.py

if "$tmp/.venv/bin/python" -m pyright --outputjson invalid_offline_types.py > invalid_types.json; then
echo "Invalid offline evaluation configurations unexpectedly passed type checking."
if "$tmp/.venv/bin/python" -m pyright --outputjson invalid_types.py > invalid_types.json; then
echo "Invalid offline evaluation configurations and email sends unexpectedly passed type checking."
exit 1
fi
"$tmp/.venv/bin/python" - <<'PY'
import json
from pathlib import Path

expected = {
index for index, line in enumerate(Path("invalid_offline_types.py").read_text().splitlines())
index for index, line in enumerate(Path("invalid_types.py").read_text().splitlines())
if "expected-type-error" in line
}
diagnostics = json.loads(Path("invalid_types.json").read_text())["generalDiagnostics"]
actual = {item["range"]["start"]["line"] for item in diagnostics if item["severity"] == "error"}
assert actual == expected, diagnostics
print("Offline evaluation configuration types reject invalid inputs.")
print("Offline evaluation configuration and email send types reject invalid inputs.")
PY
7 changes: 7 additions & 0 deletions .sampo/changesets/emails-code-form.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
pypi/posthog: minor
---

Send transactional email from content in your code with `posthog.emails.send(template_key=..., subject=..., html=...)`. Without `template_data`, the content is sent as is. With `template_data`, the content is a template: PostHog fills in its variables, and the client uploads it by key whenever PostHog doesn't hold its content yet, then sends again with the same idempotency key. A send that mixes `template_id` and `template_key` fails type checks and raises `TypeError`.

A code send also takes `cc`, `bcc`, `headers`, `tags` and `attachments`. An attachment's `content` can be `bytes`, `bytearray` or `memoryview`, which the client encodes as Base64, or a Base64 string.
2 changes: 1 addition & 1 deletion posthog/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def __init__(
@property
def emails(self) -> AsyncEmails:
"""
Send transactional email from PostHog email templates.
Send transactional email from PostHog email templates or from content in your code.

Needs ``secret_key``, a project secret API key with the ``hog_flow:write``
scope, and ``posthog[async]``. Sends go to ``app_host``, or to the PostHog
Expand Down
2 changes: 1 addition & 1 deletion posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,7 @@ def _request_identity_kwargs(self) -> Dict[str, str]:
@property
def emails(self) -> Emails:
"""
Send transactional email from PostHog email templates.
Send transactional email from PostHog email templates or from content in your code.

Needs ``secret_key``, a project secret API key with the ``hog_flow:write``
scope. Sends go to ``app_host``, or to the PostHog Cloud app host of
Expand Down
Loading