Metadata-Version: 2.4
Name: stackshift
Version: 1.0.1
Summary: Official Python SDK for StackShift.
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31

# StackShift Python SDK

Official Python SDK for StackShift.

## Install

```bash
pip install stackshift
```

## Send email

```python
from stackshift import StackShift

stackshift = StackShift()

message = stackshift.mail.send(
    from_="StackShift <noreply@mail.stackshift.cloud>",
    to="ada@example.com",
    subject="Welcome",
    text="Welcome to StackShift.",
    idempotency_key="welcome:user_123",
)

print(message["id"], message["status"], message["idempotencyStatus"])
```

Inspect message status:

```python
messages = stackshift.mail.messages.list(status="mta_accepted", limit=20)
detail = stackshift.mail.messages.get(messages["data"][0]["id"])
attempts = stackshift.mail.messages.attempts(detail["id"])
logs = stackshift.mail.messages.logs(detail["id"])
```

`mta_accepted` means the message was accepted by StackShift's outbound MTA. It does not mean recipient-MX acceptance, inbox placement, opens, clicks, or spam placement. Recipient records later transition independently to `delayed`, `delivered`, `bounced`, `failed`, `suppressed`, or `complained`.

## Events and webhooks

```python
events = stackshift.mail.events.list(
    type="mail.message.bounced",
    message_id="msg_123",
    limit=20,
)

event = stackshift.mail.events.get("evt_123")
timeline = stackshift.mail.messages.timeline("msg_123")

webhook = stackshift.mail.webhooks.create(
    url="https://example.com/stackshift-mail",
    event_types=["mail.message.bounced", "mail.otp.verified"],
)
print(webhook["id"], webhook["secret"])  # Secret is only returned on create/rotate.

deliveries = stackshift.mail.webhooks.deliveries(webhook["id"], status="failed")
retried = stackshift.mail.webhooks.retry_delivery(deliveries["data"][0]["id"])
```

Verify a webhook signature before processing the payload:

```python
valid = stackshift.mail.webhooks.verify_signature(
    raw_body=request.get_data(),
    signature_header=request.headers.get("StackShift-Signature"),
    timestamp_header=request.headers.get("StackShift-Timestamp"),
    secret=os.environ["STACKSHIFT_WEBHOOK_SECRET"],
)
```

Webhook handlers should be idempotent. StackShift retries non-2xx responses. Delivery events do not include full email bodies or OTP codes by default.

## Send a template

Templates are rendered by StackShift servers. The Python SDK only calls the REST API.

```python
stackshift.mail.templates.create(
    name="Welcome Email",
    slug="welcome-email",
    subject="Welcome, {{name}}",
    text="Welcome, {{name}}",
)

preview = stackshift.mail.templates.preview(
    "welcome-email",
    data={"name": "Ada"},
)

message = stackshift.mail.send_template(
    template="welcome-email",
    to="ada@example.com",
    from_="Acme <noreply@acme.com>",
    data={"name": "Ada"},
    idempotency_key="welcome:user_123",
)
```

Missing variables fail before a message is queued. Template sends use the same sender-domain, suppression, idempotency, Durable Jobs, and Postfix handoff pipeline as `mail.send`.

## Bounces and suppressions

```python
suppressions = stackshift.mail.suppressions.list()

manual = stackshift.mail.suppressions.create(
    email="bad@example.com",
    reason="manual",
)

stackshift.mail.suppressions.delete(manual["id"])

bounces = stackshift.mail.bounces.list(type="hard")
message_bounces = stackshift.mail.messages.bounces("msg_123")
```

Hard bounces are automatically suppressed by the backend. Suppressions are workspace scoped and checked before a message is queued.

## Verify a sending domain

```python
domain = stackshift.mail.domains.create("acme.com")
print(domain["records"])

stackshift.mail.domains.verify(domain["id"])

message = stackshift.mail.send(
    from_="Acme <noreply@acme.com>",
    to="user@example.com",
    subject="Welcome",
    html="<h1>Welcome</h1>",
    text="Welcome",
)
```

Add the returned SPF, DKIM, and return-path DNS records before verification. DMARC is recommended unless your environment sets `MAIL_DMARC_REQUIRED=true`. DNS propagation can take time.

## Upload an asset

```python
from stackshift import StackShift

stackshift = StackShift()

asset = stackshift.assets.upload(
    "avatar.png",
    folder="avatars",
    visibility="public",
    metadata={"user_id": "user_123"},
)

print(asset["url"])
```

You do not pass a deployed StackShift project ID. The API key identifies the StackShift account, and StackShift resolves the default asset space internally.

## Private asset URL

```python
signed = stackshift.assets.signed_url(
    asset["id"],
    expires_in="10m",
    max_downloads=1,
)

print(signed["url"])
```

## Image transformations

Use built-in presets or create your own named transformations.

```python
from stackshift import StackShift, asset_transform_options, get_asset_transform_preset

stackshift = StackShift()
hero = get_asset_transform_preset("hero")
hero_options = asset_transform_options(hero)

stackshift.assets.create_transformation(hero["name"], **hero_options)

named = stackshift.assets.named_url("asset_123", hero["name"])
signed = stackshift.assets.signed_transform_url("asset_123", **hero_options, expiresIn="10m")

stackshift.assets.delete_transformation("old-preset")
print(named, signed["url"])
```

## Direct browser uploads

Create the upload session on your Python backend:

```python
upload = stackshift.assets.signed_upload_url(
    bucket="avatars",
    key="users/user_123.png",
    visibility="public",
    expiresIn="10m",
    maxBytes=5_000_000,
)
```

Then upload the browser `File` to `upload["url"]` from your frontend. Do not expose your StackShift API key to browser code.

The SDK only talks to the StackShift REST API. Storage placement, replication, disks, and repair are StackShift internals.
