Metadata-Version: 2.4
Name: truncus
Version: 0.1.0
Summary: Official Python SDK for Truncus Email
Author-email: Truncus <support@truncus.co>
License: MIT
Project-URL: Homepage, https://truncus.co
Project-URL: Documentation, https://truncus.co/manual
Project-URL: Repository, https://github.com/vanmoose/truncus-python
Project-URL: Bug Tracker, https://github.com/vanmoose/truncus-python/issues
Keywords: email,transactional,api,truncus,smtp,ses
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28.0

# truncus

Python SDK for [Truncus](https://truncus.co) — transactional email infrastructure built on AWS SES with deliverability built-in.

## Installation

```bash
pip install truncus
```

## Quick Start

```python
from truncus import Truncus

client = Truncus(api_key="tr_live_...")

result = client.send(
    to="user@example.com",
    from_="hello@mail.yourdomain.com",
    subject="Welcome!",
    html="<h1>Hello World</h1>",
)

print(result.id)     # Email ID
print(result.status) # 'sent'
```

## Configuration

```python
client = Truncus(
    api_key="tr_live_...",          # Required
    base_url="https://truncus.co",  # Optional, defaults to production
    timeout=30,                     # Optional, request timeout in seconds (default 30)
    sandbox=False,                  # Optional, enable sandbox mode globally (default False)
)
```

## Sandbox mode

Sandbox mode validates payloads, checks domain ownership, suppression lists, and rate limits — but never delivers the email.

```python
# Enable globally for all calls
client = Truncus(api_key="tr_live_...", sandbox=True)
result = client.send(to=..., from_=..., subject=..., html="...")
# result.status == "sandbox"

# Or use validate() for a one-off dry-run (same params as send())
result = client.validate(
    to="user@example.com",
    from_="hello@mail.yourdomain.com",
    subject="Test",
    html="<p>Hi</p>",
)
# result.status == "sandbox"
```

## API Reference

### `send()`

Send a transactional email.

```python
result = client.send(
    # Required
    to="user@example.com",
    from_="hello@mail.yourdomain.com",  # Must match a verified domain
    subject="Welcome!",
    html="<h1>Hello World</h1>",

    # Optional
    text="Hello World",               # Plain-text fallback
    cc=["cc@example.com"],
    bcc=["bcc@example.com"],
    template_id="tmpl_...",           # Use a saved template instead of html
    variables={"name": "John"},       # Template variables ({{name}})
    metadata={"user_id": "123"},      # Custom metadata (stored, not sent)
    idempotency_key="order-123",      # Prevent duplicate sends
    tenant_id="tenant_abc",           # Multi-tenant suppression isolation
    send_at="2026-03-10T09:00:00Z",  # Schedule for future delivery (ISO 8601)
    track_opens=True,                 # 1×1 tracking pixel (default: True)
    track_clicks=True,                # Rewrite links through click proxy (default: True)
    attachments=[
        {
            "filename": "invoice.pdf",
            "content": "<base64-encoded-content>",
            "content_type": "application/pdf",
        }
    ],
)

# result.status == 'sent'       — sent immediately
# result.status == 'scheduled'  — queued for send_at time
# result.send_at                — ISO 8601 delivery time (scheduled emails)
```

### `get_email(email_id)`

Retrieve details for a specific email, including open and click tracking stats.

```python
email = client.get_email("email_id")

print(email.status)      # 'delivered'
print(email.open_count)  # 3 (total opens, including repeats)
print(email.opened_at)   # '2026-03-04T10:23:00Z' (first open)
print(email.click_count) # 1
```

### `batch(emails)`

Send up to 100 emails in a single request.

```python
result = client.batch([
    {
        "to": "user1@example.com",
        "from_": "hello@mail.yourdomain.com",
        "subject": "Hi",
        "html": "<p>Hi User 1</p>",
    },
    {
        "to": "user2@example.com",
        "from_": "hello@mail.yourdomain.com",
        "subject": "Hi",
        "html": "<p>Hi User 2</p>",
    },
])

print(result.sent)   # 2
print(result.failed) # 0
```

## Open & Click Tracking

Tracking is enabled by default. Truncus injects a 1×1 pixel for opens and rewrites links for click tracking. Unsubscribe links are never rewritten.

```python
# Default — both opens and clicks tracked
result = client.send(to=..., from_=..., subject=..., html="...")

# Disable tracking
result = client.send(
    to=..., from_=..., subject=..., html="...",
    track_opens=False,
    track_clicks=False,
)

# Check engagement after sending
email = client.get_email(result.id)
print(email.opened_at)   # First open timestamp (None if not opened)
print(email.open_count)  # Total opens
print(email.click_count) # Total link clicks
```

## Scheduled Sending

```python
result = client.send(
    to="user@example.com",
    from_="hello@mail.yourdomain.com",
    subject="Your weekly digest",
    html="...",
    send_at="2026-03-10T09:00:00Z",  # Must be a future datetime
)

print(result.status)  # 'scheduled'
print(result.send_at) # '2026-03-10T09:00:00Z'
```

## Idempotency

Prevent duplicate sends with a stable idempotency key. Retrying with the same key returns the original response without sending again:

```python
result = client.send(
    to="user@example.com",
    from_="hello@mail.yourdomain.com",
    subject="Order Confirmation",
    html="...",
    idempotency_key=f"order-{order_id}-confirmation",
)
```

A UUID v4 is auto-generated when not provided.

## Multi-tenant Apps

Isolate suppression lists per tenant:

```python
result = client.send(
    to="user@example.com",
    from_="hello@mail.yourdomain.com",
    subject="Welcome!",
    html="...",
    tenant_id="tenant_123",
)
```

## Error Handling

```python
from truncus import TruncusError

try:
    client.send(...)
except TruncusError as e:
    print(e.code)    # 'DOMAIN_NOT_VERIFIED'
    print(str(e))    # 'Domain is not verified'
    print(e.status)  # 400
```

### Error Codes

| Code | Status | Description |
|------|--------|-------------|
| `MISSING_API_KEY` | 401 | Authorization header missing |
| `INVALID_API_KEY` | 401 | API key is invalid or revoked |
| `SCOPE_REQUIRED` | 403 | API key missing required scope |
| `INVALID_REQUEST` | 400 | Request body validation failed |
| `DOMAIN_NOT_FOUND` | 404 | Domain doesn't exist |
| `DOMAIN_NOT_VERIFIED` | 400 | Domain pending DNS verification |
| `DOMAIN_PAUSED` | 400 | Domain paused due to high bounce/complaint rate |
| `WARMUP_CAP_EXCEEDED` | 429 | Daily sending limit reached |
| `ALL_RECIPIENTS_SUPPRESSED` | 422 | All recipients are on suppression list |
| `PROVIDER_ERROR` | 502 | AWS SES returned an error |
| `TIMEOUT` | 408 | Request timed out |
| `NETWORK_ERROR` | 0 | Network connectivity issue |

## License

MIT
