Metadata-Version: 2.4
Name: primitif
Version: 0.0.9
Summary: Primitif Python SDK — Mail, Approval, and Audit APIs for AI agents
Project-URL: Homepage, https://primitif.ai
Project-URL: Repository, https://github.com/PrimitifAI/primitif-python
Project-URL: Documentation, https://primitif.ai/docs
Author-email: Primitif <hello@primitif.ai>
License-Expression: MIT
License-File: LICENSE
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.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: respx>=0.22.0; extra == 'dev'
Description-Content-Type: text/markdown

# primitif

Python SDK for [Primitif](https://primitif.ai) — give your AI agents email and human approval.

```
pip install primitif
```

## Setup

Set your API key as an env var and you're good to go:

```bash
export PRIMITIF_API_KEY="pk_live_..."
```

## Mail

Create a temporary mailbox, send and receive emails, then throw it away.

```python
from primitif import Primitif

with Primitif() as p:
    mb = p.mail.create_mailbox(name="support-agent", ttl=3600)
    print(mb.address)  # support-agent-x7k2@mail.primitif.ai

    # send an email
    mb.send(to="user@example.com", subject="Hey", body_text="Following up on your ticket.")

    # check inbox
    for msg in mb.inbox():
        email = mb.read(msg.id)
        print(email.from_address, email.subject, email.body_text)
        # reply to it
        mb.reply(msg.id, body_text="Thanks, we're on it.")

    # done — delete the mailbox
    mb.delete()
```

If you already have a mailbox token, use it directly:

```python
from primitif import Mailbox

with Mailbox(token="mb_tok_...") as mb:
    mb.send(to="someone@example.com", subject="Hi", body_text="Hello")
```

Tokens can be persisted for crash recovery:

```python
mb = p.mail.create_mailbox(name="agent")
save_to_db(mb.token)  # persist the token

# later, reconnect:
mb = Mailbox(token=load_from_db())
```

### Send with human approval

Emails can be held for human approval before sending:

```python
result = mb.send(
    to="ceo@bigclient.com",
    subject="Contract proposal",
    body_text="Please find attached...",
    require_approval=True,
)
print(result.status)        # "held"
print(result.approval_url)  # link for a human to approve/reject
```

### Attachments

```python
for att in mb.list_attachments(message_id):
    print(att.filename, att.content_type, att.size_bytes)

data = mb.download_attachment(message_id, att.id)
with open(att.filename, "wb") as f:
    f.write(data)
```

### Sender allowlist

New mailboxes start with the org owner allowlisted. Recipients are auto-added when the agent sends. To open a mailbox to all senders, add the wildcard `*`:

```python
# open inbox — receive from anyone
mb.add_allowlist("*")

# or restrict to specific senders
mb.add_allowlist("trusted@example.com")

# list allowed senders
for entry in mb.get_allowlist():
    print(entry.sender_address)

# remove a sender (removing "*" switches back to allowlist-only)
mb.remove_allowlist(entry.id)
```

### Custom domains

Use your own domain instead of `@mail.primitif.ai`:

```python
# 1. register the domain
domain = p.mail.add_custom_domain("yourcompany.com")
# → returns DNS records to add at your provider

# 2. add DNS records, then verify
verified = p.mail.verify_custom_domain(domain.id)

# 3. create mailboxes on the domain
mb = p.mail.create_mailbox(name="agent", domain_id=domain.id)
# → agent@yourcompany.com

# 4. optionally, set as default — all new mailboxes use this domain automatically
p.mail.set_default_domain(domain.id)
mb = p.mail.create_mailbox(name="agent")  # agent@yourcompany.com, no domain_id needed

# clear the default (new mailboxes go back to @mail.primitif.ai)
p.mail.clear_default_domain()

# list / delete
for d in p.mail.list_custom_domains():
    print(d.domain, d.status)
p.mail.delete_custom_domain(domain.id)
```

### Email authentication policy

Control how inbound emails are verified (SPF/DKIM/DMARC):

```python
mb.set_auth_policy("reject")   # reject emails that fail auth (default)
mb.set_auth_policy("warn")     # accept but flag failures
mb.set_auth_policy("none")     # accept everything
```

### Pagination

All list methods return a `PaginatedList` that you can iterate directly,
or use `auto_paging_iter()` to fetch all pages automatically:

```python
for msg in mb.inbox().auto_paging_iter():
    print(msg.subject)
```

## Approval

Gate your agent's tools behind human approval.

```python
from primitif import Primitif

p = Primitif()

# Protect sensitive tools with a decorator
@p.approval.require_approval()
def send_invoice(client, amount):
    billing.send(client, amount)

@p.approval.require_approval()
def delete_account(user_id):
    accounts.delete(user_id)

# Agent calls the tool — gets a pending approval, carries on
result = send_invoice("Acme", 45000)
print(result.status)        # "pending"
print(result.approval_url)  # human reviews here
```

Without the decorator:

```python
request = p.approval.create_request(
    action="Delete user account #1234",
    context={"user_id": 1234, "reason": "requested by user"},
    callback_url="https://myapp.com/webhooks/approval",
)
```

## Webhooks

When the human approves, `dispatch()` automatically executes the right tool
with the original arguments:

```python
from flask import Flask, request
from primitif import Primitif, verify_webhook, InvalidSignature

app = Flask(__name__)
p = Primitif()

WEBHOOK_SECRET = "whsec_..."

@p.approval.require_approval()
def send_invoice(client, amount):
    billing.send(client, amount)

@app.post("/webhooks/approval")
def handle_approval():
    try:
        event = verify_webhook(
            secret=WEBHOOK_SECRET,
            signature=request.headers["X-Webhook-Signature"],
            timestamp=request.headers["X-Webhook-Timestamp"],
            body=request.data,
        )
    except InvalidSignature:
        return "", 401

    p.approval.dispatch(event)
    return "", 200
```

> Set your webhook URL in the [Console](https://dash.primitif.ai) under Settings → Webhooks.

## Audit

Log everything your agent does — Primitif products log automatically,
and you can add your own events for custom tools.

```python
# Log a custom event
p.audit.log("tool.called", metadata={"tool": "search", "query": "revenue Q4"})

# Decorator — automatically logs function calls with duration and status
@p.audit
def search_database(query):
    return db.search(query)

# Decorator with options
@p.audit(capture_result=False)
def fetch_secret(key):
    return vault.get(key)

# List events (unified timeline across all products)
events = p.audit.list(limit=20)
for event in events:
    print(event.action, event.product, event.status, event.duration_ms)

# Filter by product
mail_events = p.audit.list(product="mail")
approval_events = p.audit.list(product="approval")
custom_events = p.audit.list(source="custom")

# Get a single event
event = p.audit.get("evt_abc123")
```

## Error handling

All errors inherit from `PrimitifError`, so you can catch broadly or narrowly:

```python
from primitif import PrimitifError, AuthError, NetworkError
from primitif.mail import MailRateLimitError

try:
    mb.send(to="user@test.com", subject="Hi", body_text="Hello")
except MailRateLimitError as e:
    print(f"Slow down — retry after {e.retry_after}s")
except NetworkError:
    print("Network issue — timeout or DNS failure")
except AuthError:
    print("Bad credentials")
except PrimitifError:
    print("Something went wrong")
```

## License

MIT
