# HAX Python SDK

Python client for the HAX (Human Approval eXchange) API. Enables agents and automated systems to programmatically collect human input.

## Installation

The Python SDK is not yet published to PyPI. Install from source:

```bash
pip install -e sdks/python  # from the repo root
# or
pip install -e .  # from sdks/python/
```

## Quick Start

```python
from hax import HaxClient

client = HaxClient(
    api_key="hax_live_...",
    base_url="http://localhost:3000/api/v1",
)

request = client.create_request(
    type="text-approval-v1",
    payload={"text": "Deploy main to prod?", "approveLabel": "Ship it", "denyLabel": "Hold"},
    webhook_url="https://myapp.com/webhook",
)

print("Share with approver:", request.url)

# Poll until completed/expired/cancelled
request = client.wait_for_response(request.id, timeout=300)
if request.is_completed:
    print("Decision:", request.response.get("decision"))
```

## Features

- **Pydantic models**: Typed request/response models with validation
- **FormBuilder**: Fluent API for building typed forms with runtime type inference
- **E2E encryption**: RSA-OAEP + AES-GCM hybrid encryption for sensitive responses
- **Webhook verification**: HMAC-SHA256 signature verification
- **Delivery**: Send requests via email or SMS
- **Polling**: Built-in `wait_for_response` with configurable timeout
- **Error handling**: Typed exception hierarchy

## Request Methods

```python
# Create a request
request = client.create_request(
    type="text-approval-v1",
    payload={"text": "Approve this action?"},
    title="Optional title",
    description="Optional description",
    webhook_url="https://myapp.com/webhook",
    expires_in_seconds=3600,
    metadata={"pr_number": 123},
)

# Send via email
request = client.request_via_email(
    type="confirm-action-v1",
    payload={"title": "Approve?", "confirmPhrase": "YES"},
    to_email="approver@example.com",
    subject="Approval Required",
)

# Send via SMS
request = client.request_via_sms(
    type="text-approval-v1",
    payload={"text": "Approve?"},
    to_phone="+15551234567",
)

# Get a request by ID
request = client.get_request("req_123")

# List recent requests
requests = client.list_requests()

# Cancel a pending request
cancelled = client.cancel_request("req_123")

# Submit a response (for testing)
completed = client.submit_response("req_123", {"decision": "approve"})

# Wait for completion with timeout
result = client.wait_for_response("req_123", poll_interval=2.0, timeout=60)

# List available template types
types = client.list_types()
```

### Status Helpers

```python
if request.is_pending:
    print("Waiting for response...")
if request.is_completed:
    print("Response:", request.response)
if request.is_expired:
    print("Request expired")
if request.is_cancelled:
    print("Request was cancelled")
```

## FormBuilder

Build typed forms with a fluent API:

```python
from hax import HaxClient, FormBuilder

client = HaxClient(api_key="hax_live_...")

form = (FormBuilder()
    .title("Event Registration")
    .input("name", label="Full Name", required=True)
    .input("email", label="Email", variant="email", required=True)
    .number("age", label="Age", min=0, max=120)
    .checkbox("newsletter", checkbox_label="Subscribe to newsletter"))

handle = client.create_form_request(form,
    webhook_url="https://myapp.com/webhook")

print(f"Form URL: {handle.url}")

# Wait for typed response
response = handle.wait_for_response(timeout=300)
print(response.values.name)       # str
print(response.values.email)      # str
print(response.values.age)        # float
print(response.values.newsletter) # bool
```

### Available Field Types

| Method | Output Type | Description |
|--------|------------|-------------|
| `.input(id)` | `str` | Text input (variants: text, email, url, tel) |
| `.textarea(id)` | `str` | Multi-line text input |
| `.select(id, options=...)` | `str` | Dropdown select |
| `.radio_group(id, options=...)` | `str` | Radio button group |
| `.date(id)` | `str` | Date picker (ISO format) |
| `.number(id)` | `float` | Numeric input |
| `.slider(id, min=, max=)` | `float` | Slider control |
| `.checkbox(id)` | `bool` | Single checkbox |
| `.switch(id)` | `bool` | Toggle switch |
| `.checkbox_group(id, options=...)` | `list[str]` | Multi-select checkboxes |
| `.hidden(id, value)` | `type(value)` | Hidden field |

## Webhooks

Verify and parse webhook events:

```python
from hax import verify_signature, parse_event

# In your webhook handler
def handle_webhook(request):
    # Verify signature
    is_valid = verify_signature(
        payload=request.body,
        signature=request.headers["X-Hax-Signature"],
        secret="whsec_...",
    )
    if not is_valid:
        return 400, "Invalid signature"

    # Parse the event
    event = parse_event(request.body)

    if event.event_type == "completed":
        print(f"Request {event.request_id} completed!")
        print(f"Response: {event.response}")
    elif event.event_type == "expired":
        print(f"Request {event.request_id} expired")

    return 200, "OK"
```

### Event Types

- `request.sent` — Notification was delivered (email/SMS)
- `request.opened` — Human opened the request link
- `request.completed` — Human submitted a response
- `request.expired` — Request expired without action

## Encryption

For sensitive response data, use end-to-end encryption:

```python
from hax import HaxClient

# Passphrase-based (automatic encrypt/decrypt)
client = HaxClient(
    api_key="hax_live_...",
    encryption_key="my-secret-passphrase",
)

# Public key is automatically sent with requests
request = client.create_request(
    type="text-approval-v1",
    payload={"text": "Approve this sensitive action?"},
)

# Response is automatically decrypted when retrieved
completed = client.get_request(request.id)
print(completed.response)  # Decrypted plaintext
```

### Manual Decryption

```python
from hax import generate_key_pair, decrypt_response, is_encrypted_response

public_key, private_key = generate_key_pair("my-secret")

# Use public_key when creating the client
client = HaxClient(api_key="...", public_key=public_key)

# Later, manually decrypt
request = client.get_request("req_123")
if is_encrypted_response(request.response):
    decrypted = decrypt_response(request.response["_encrypted"], private_key)
```

## Error Handling

```python
from hax import (
    HaxError,             # Base error
    AuthenticationError,  # Invalid API key (401)
    ValidationError,      # Invalid request data (400/422)
    NotFoundError,        # Resource not found (404)
    RateLimitError,       # Too many requests (429)
    ServerError,          # Server error (500+)
    DecryptionError,      # Decryption failure
)

try:
    request = client.create_request(...)
except AuthenticationError:
    print("Check your API key")
except ValidationError as e:
    print(f"Invalid request: {e}")
except RateLimitError:
    print("Rate limited, try again later")
except HaxError as e:
    print(f"API error: {e}")
```

## Template Types

| Template | Description |
|----------|-------------|
| `text-approval-v1` | Show text and collect an approve/deny decision |
| `confirm-action-v1` | Require typing a specific phrase to confirm a destructive action |
| `collect-email-v1` | Prompt the user for an email address |
| `form-builder` | Advanced forms with field types, layouts, validation, and conditional logic |
| `multi-choice-selection-v1` | Single or multiple selection from customizable option cards |
| `code-changes-v1` | GitHub-style diff view with inline line comments |
| `rich-text-editor-v1` | Markdown-formatted text editing for documents and reports |
| `file-upload-v1` | Collect files (documents, images, CSVs) from users |
| `signature-capture-v1` | Capture e-signatures with optional signer name and legal text |
| `data-table-review-v1` | Review, select, or edit tabular data |
| `scheduling-picker-v1` | Date/time slot selection with optional recurring schedules |
| `multi-step-wizard-v1` | Sequential steps with navigation and progress indicator |
| `side-by-side-comparison-v1` | Compare two versions with diff highlighting |
| `terminal-output-v1` | Display command output/logs with approve-to-continue |

## Notes

- Auth is **API key only**. Provide the key via `HaxClient(api_key=...)`; Clerk/session auth is not required for API access.
- API responses wrap resources (e.g., `{"request": {...}}`); the SDK unwraps this automatically.
- Template payloads and responses are flexible; consult the template configs for the fields each template expects/returns.
- The client supports context manager usage: `with HaxClient(...) as client:`
