Metadata-Version: 2.4
Name: ironfang-render
Version: 3.0.1
Summary: Ironfang Render API client: screenshots, PDFs, QR codes, template images, clips and site previews from a URL or HTML.
Author: Ironfang Ltd
License-Expression: MIT
Project-URL: Homepage, https://ironfang.com/render
Project-URL: Documentation, https://ironfang.com/render/docs
Keywords: screenshot,pdf,qr,html-to-image,ironfang
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# ironfang-render

The official Python client for the [Ironfang Render](https://ironfang.com/render)
API: screenshots, PDFs, QR codes, template images, clips and site previews
from a URL or from HTML you send. No dependencies; Python 3.10 and later.

Formerly `ironfang-renderwolf`; 2.0.0 removed `ironfang_renderwolf` and the old
`Renderwolf` and `RenderwolfError` names. Webhooks are signed with the
`Ironfang-*` headers.

3.0.0 follows Ironfang's move from credits to metered usage on one billing
account: a render reports `meter` and `quantity` instead of `credits`, a job
reports `usage` instead of `credits`, `usage()` returns the month per meter,
request history records `meter` and `quantity`, and billing refusals raise
`IronfangBillingError`. Renders accept `idempotency_key`.

3.0.1 fixes `verify_webhook` for Render: it used the text of the signing secret as
the HMAC key, while a Render secret is the hex of the 32-byte key deliveries
are signed with, so it rejected every genuine delivery. Upgrade if you
verify Render webhooks with it.

```sh
pip install ironfang-render
```

```python
import os
from ironfang_render import IronfangRender

with IronfangRender(api_key=os.environ["IRONFANG_API_KEY"]) as rw:
    shot = rw.screenshot(url="https://example.com", full_page=True)
    shot.save("page.png")
    print(shot.meter, shot.quantity, shot.render_ms, shot.request_id)
```

Every render returns the bytes plus what the API reports about them: content
type, the billing meter it counted against and the quantity it counted there
(zero on a cache hit), render time and the request id to quote to support.
Long outputs stream: `save()` and `iter_bytes()` never hold a whole clip in
memory.

## Change the page before capture

```python
rw.screenshot(
    html="<div class='chat-widget'>Chat</div><details id='details'><summary id='show-more'>Pricing</summary><p>Expanded pricing</p></details>",
    css=".chat-widget { display: none !important; }",
    script="document.querySelector('#show-more').textContent = 'Show pricing';",
    actions=[
        {"type": "click", "selector": "#show-more"},
        {"type": "wait_for_selector", "selector": "#details[open]"},
    ],
).save("expanded.png")
```

The same options work with `rw.pdf()` and URL input. After normal load/waits,
Ironfang Render applies CSS, awaits the script, then executes actions in order
before settling and capture. JavaScript runs in the target page context as an
async function body; use `await` or return a promise for asynchronous work. It
has a five-second limit (or the remaining render timeout, if shorter).

CSS is limited to 64 KiB of UTF-8, script to 16 KiB, and actions to 20. Actions
are `click`, `hover`, `wait_for_selector` (presence), and `delay` with
`duration_ms`; combined delays may total at most 10,000 ms. Selectors are limited
to 1,024 UTF-8 bytes and share the render timeout. Invalid input returns
`bad_request`; script/action execution errors return sanitized `render_failed`
messages. A screenshot cache hit does not re-execute controls; use `no_cache`
for fresh execution. [Full reference](https://ironfang.com/render/docs#page-controls).

## Durable jobs

```python
job = rw.submit_job("pdf", {"html": invoice_html, "paper_format": "a4"}, idempotency_key=f"invoice-{invoice.id}")
done = rw.wait_for_job(job["id"])
if done["status"] == "succeeded":
    rw.job_result(job["id"]).save(f"invoice-{invoice.id}.pdf")
```

The job's `usage` says what it holds: `{"meter", "reserved", "committed"}`.
The most the job can count is reserved when it is accepted, and what it
produced is committed when it succeeds. A job that fails or is cancelled while
queued counts nothing.

## Usage and billing

Ironfang Render has no plans or credits. Usage is billed to your
organisation's Ironfang billing account, shared by every Ironfang product and
managed in Billing in the [Ironfang portal](https://portal.ironfang.com). Each
kind of render counts against a meter, and each meter has a monthly free
allowance; beyond it, usage is pay-as-you-go once paid usage is enabled.

```python
usage = rw.usage()
for m in usage.get("meters", []):
    print(m["id"], m["used"], m["remaining_free"])
```

## Verifying a webhook

Run the check over the raw request body, before parsing it. Pass the signing
secret exactly as it was shown when the destination was created: a Render (or
Finance) secret is 64 hex characters, and the check decodes it to the 32-byte
key; an Ironfang Audit secret (`awsec_...`) is used as it is.

```python
from ironfang_render import verify_webhook

ok = verify_webhook(
    secret=os.environ["RENDER_WEBHOOK_SECRET"],
    timestamp=request.headers["Ironfang-Timestamp"],
    signature=request.headers["Ironfang-Signature"],
    payload=request.get_data(),
)
```

## Errors

Failures raise `IronfangRenderError` with `status`, the API's `code`
(`render_failed`, `rate_limited`, ...), `request_id` and, when the API sent
one, `retry_after` in seconds. Branch on `code` rather than the status: rate
limits and billing refusals can share a 429.

When the billing account refuses a render, nothing is rendered or counted and
the error is an `IronfangBillingError` (a subclass) with `product`, and
`meter` and `reset_at` when the API states them. The codes are
`free_allowance_exhausted`, `account_budget_exhausted`,
`product_budget_exhausted`, `exemption_limit_reached`, `paid_usage_paused`,
`payment_required`, `payment_action_required`, `account_restricted`,
`product_not_eligible`, `account_closed`, `operation_too_large` and
`billing_temporarily_unavailable`. Only the last clears by itself, after
`retry_after`; the rest are resolved in Billing or when the month rolls over.

```python
from ironfang_render import IronfangBillingError

try:
    rw.screenshot(url="https://example.com")
except IronfangBillingError as err:
    print(err.code, err.reset_at)
```

Synchronous renders are never retried by the client: a render that timed out
may already have counted, and retrying it blind counts it twice. Pass
`idempotency_key` to make a render count at most once; the same key again is
refused with `idempotency_key_used`, since a synchronous result is not kept.
