Metadata-Version: 2.4
Name: ironfang-render
Version: 3.1.0
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.

3.1.0 adds capture bundles (`bundle()`), stored document templates, and each
render's `diagnostics`. Additions only; see the [changelog](CHANGELOG.md).

```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).

## Lazy content, success rules, language and clock

Every request field is a keyword argument, so everything the API accepts works
as written in the [reference](https://ironfang.com/render/docs):

```python
shot = rw.screenshot(
    url="https://example.com/pricing",
    full_page=True,
    full_page_readiness="scroll",            # load lazy content first
    success_rules={"fail_on_status": ["4xx", "5xx"], "forbid_text": ["Access denied"]},
    locale="de-DE",                          # Intl, navigator.language, Accept-Language
    timezone="Europe/Berlin",                # the page's clock
)
print(shot.diagnostics)  # {'target_status': 200, 'locale': 'de-DE', 'timezone': 'Europe/Berlin', ...}
```

A failed success rule raises `IronfangRenderError` with code
`success_rule_failed` and is not charged. PDFs take `paper_width` and
`paper_height` (`"210mm"`), `page_ranges` (`"1-3, 5"`), `prefer_css_page_size`
and `metadata`.

## Capture bundles

One page load, the outputs you choose:

```python
b = rw.bundle(url="https://example.com/blog/post", outputs=["markdown", "metadata"])
print(b.metadata["title"], b.markdown)
for o in b.manifest["outputs"]:
    if o["status"] != "included":
        print(o["name"], o["status"], o.get("reason"))
```

Counts one `render.screenshot`. Captured HTML and text are the page's own
content: treat them as untrusted.

## Document templates

```python
tpl = rw.create_document_template(name="invoice", source="<h1>Invoice {{.number}}</h1>",
                                  schema={"type": "object", "required": ["number"],
                                          "properties": {"number": {"type": "string"}}})
rw.publish_document_template(tpl["id"])
rw.render_document(tpl["id"], {"number": "INV-0042"}).save("invoice.pdf")
```

`validate_document_template` checks data without printing;
`publish_document_revision` rolls back to an earlier revision.

## 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.
