Metadata-Version: 2.5
Name: pagermail
Version: 0.2.0
Summary: First-party Python SDK for the PagerMail API
Project-URL: Homepage, https://github.com/pagerai/pagermail#readme
Project-URL: Repository, https://github.com/pagerai/pagermail
Project-URL: Issues, https://github.com/pagerai/pagermail/issues
License-Expression: MIT
License-File: LICENSE
Keywords: agents,email,pagermail,python,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.7
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == 'dev'
Requires-Dist: langchain<2,>=1; extra == 'dev'
Requires-Dist: mypy<2,>=1.15; extra == 'dev'
Requires-Dist: pytest<10,>=8.3; extra == 'dev'
Requires-Dist: ruff<1,>=0.11; extra == 'dev'
Provides-Extra: langchain
Requires-Dist: langchain<2,>=1; extra == 'langchain'
Description-Content-Type: text/markdown

# `pagermail`

The first-party synchronous Python client for PagerMail. Version `0.2.0` combines typed Pydantic
v2 models derived from the repository OpenAPI contract with hand-written resource namespaces and
conservative mail-sending safety semantics.

Python 3.10 or newer is required.

## Installation

```bash
python -m pip install pagermail
```

## Quickstart

```python
import os

from pagermail import CreateAgentInput, CreateInboxInput, PagerMail, SendMessageInput

recipient = "customer@example.com"
confirm_send = True  # Set only after explicitly confirming this recipient for this call.
if confirm_send is not True:
    raise RuntimeError("Recipient confirmation is required")

with PagerMail(
    api_key=os.environ["PAGERMAIL_API_KEY"],
    base_url=os.getenv("PAGERMAIL_BASE_URL", "https://pagermail.ai/api"),
) as client:
    agent = client.agents.create(CreateAgentInput(name="Support triage"))
    inbox = client.inboxes.create(CreateInboxInput(agent_id=agent.id))

    queued = client.messages.send(
        SendMessageInput(
            from_inbox_id=inbox.id,
            to=recipient,
            subject="We received your request",
            body_text="A support agent will follow up shortly.",
        )
    )

    # HTTP 202 / status=queued is acceptance for routing, never delivery proof.
    if not queued.deliveries:
        raise RuntimeError("Queued response omitted its visible delivery reference")

    thread = client.threads.get(queued.message.thread_id)
    dossier = client.deliveries.get_dossier(queued.deliveries[0].id)
    print(thread.messages)
    print(dossier.delivery.delivery_state)
    print(dossier.delivery.accepted_at, dossier.delivery.delivered_at)
```

The client exposes `agents`, `inboxes`, `messages`, `threads`, `deliveries`, `drafts`, `search`,
`webhooks`, `metrics`, `capabilities`, and `trust` namespaces.

## Contract fidelity

The SDK deliberately works with the current OpenAPI document without changing it:

- Core responses and pagination cursor fields follow the spec's required and nullable declarations.
- Delivery timeline/explanation objects, search results, capability signals, and trust quotas have
  dedicated Pydantic models.
- Only the five send-family operations document keyed replay, which continues to define the retry
  boundary.
- The idempotency `409` split prefers the machine-readable `code`; message matching remains only as
  compatibility for older payloads that omit it.
- `SendResponse.status` remains `Literal["queued"]`; neither models nor examples infer delivery from
  a queued response.

## Idempotency and retries

Every mutation receives one automatically generated `Idempotency-Key`. PagerMail currently
documents keyed replay semantics only for send, reply, reply-all, forward, and draft-send, so only
those five mutation families can retry. Other mutations carry a key but are not retried. GETs may
also retry.

The key and compact JSON bytes are computed once and reused unchanged on every internal retry.
Retries are bounded, use exponential backoff, honor a bounded `Retry-After`, and apply only to
network failures, `408`, `429`, and `5xx` responses.

An intentionally unkeyed send is never retried:

```python
from pagermail import RequestOptions

client.messages.send(payload, options=RequestOptions(idempotency_key=False))
```

## Pagination

List calls return a typed page. Iterator methods follow opaque tokens and stop if the server repeats
a non-advancing token:

```python
for inbox in client.inboxes.iterate(page_size=50):
    print(inbox.email)

for delivery in client.deliveries.iterate(
    workspace_id=workspace_id,
    inbox_id=inbox_id,
    state="accepted",
):
    print(delivery.id, delivery.delivery_state)
```

Capability and live tier-limit reads are also typed:

```python
capabilities = client.capabilities.get()
trust = client.trust.get(agent_id)
print(capabilities.delivery_confirmation.enabled)
print(trust.quota.daily_limit, trust.quota.remaining)
```

## Errors, timeouts, and cancellation

API failures derive from `PagerMailError`, with dedicated validation, authentication, quota,
suppression, idempotency-in-progress, and idempotency-conflict subclasses. The two `409` subclasses
prefer the response's machine-readable `code`; defensive message matching remains as a fallback for
older servers that omit it.

The default timeout is 30 seconds and can be overridden globally or with `RequestOptions`. Sync
cancellation is cooperative: it interrupts backoff immediately and is checked before and after each
bounded HTTP request; the request timeout bounds active blocking I/O.

```python
from pagermail import CancellationToken, RequestOptions

cancel = CancellationToken()
cancel.cancel()
client.messages.get(message_id, options=RequestOptions(cancellation_token=cancel))
```

## LangChain adapter

LangChain is an optional dependency:

```bash
python -m pip install 'pagermail[langchain]'
```

```python
from pagermail.adapters.langchain import create_pagermail_tools

tools = create_pagermail_tools(client)
```

The send tool requires `confirm_send=True` on every invocation and otherwise refuses before calling
PagerMail. Tool output explicitly reports queued acceptance as not delivered.

The framework-neutral equivalent is in `examples/generic_workflow.py`.

## Development

```bash
git clone https://github.com/pagerai/pagermail.git
cd pagermail/sdk/python
python -m pip install -e '.[dev]'
ruff check .
ruff format --check .
mypy
pytest
python -m build
```

## License

MIT
