# Python SDK

Official Python client for ByteKit, with sync and async support.

The official Python SDK is generated from the ByteKit OpenAPI spec, so its models and
methods always match the API. `ByteKit` is a thin client on top of it for everyday use; the
generated operations underneath ship in both synchronous and asynchronous form.

## Install

```bash
pip install bytekit-sdk
```

The PyPI distribution is `bytekit-sdk`; the import name is `bytekit`.

## Your first scrape

```python
from bytekit import ByteKit

client = ByteKit(api_key="sk_live_your_api_key_here")

result = client.scrape.create(url="https://example.com", formats=["markdown"])
print(result.formats.markdown)
```

`result` is the parsed `ScrapeSuccessEnvelope`. Request fields are keyword arguments under the
API's own field names, with plain JSON values, so `formats=["markdown"]` needs no enum import.
Only the fields you pass are sent.

## Handle errors

Any non-2xx response raises `ByteKitError`, as does a `scrape.create` call that comes back as
HTTP 200 with a `failed` envelope. So does a response the generated operation cannot turn into a
documented model: an undocumented status (even a 2xx), a body that is not JSON, or a body that
does not match the documented shape. It carries the HTTP `status_code` and the API's `code` and
`message` (both `None` when the body is not an error envelope):

```python
import httpx
from bytekit import ByteKit, ByteKitError

client = ByteKit(api_key="sk_live_your_api_key_here")

try:
    client.scrape.create(url="https://example.com")
except ByteKitError as err:
    print(err.status_code, err.code, err.message)
except httpx.HTTPError:
    print("Could not reach the API")  # connection refused, DNS failure, timeout, ...
```

`ByteKitError` subclasses `bytekit.errors.UnexpectedStatus`, so code that already catches that
keeps working. A transport failure is not wrapped: it propagates as the `httpx` exception.

## Async scrapes

Pass `async_=True` to get a queued receipt back and poll it by ID. A `202` is returned, not
raised:

```python
queued = client.scrape.create(url="https://example.com", async_=True)
job = client.scrape.get(queued.id)
```

`scrape.get` returns the job's envelope as it stands, so a job that ended in failure comes back
as a `ScrapeErrorEnvelope` rather than raising.

## Other resources

Every API operation is a method on the client, grouped by resource. Path IDs are the first
positional argument, query parameters are keywords (`limit=5`, `status="completed"`), and every
other keyword is a request-body field. Pass body fields as keywords, not as a `body=` argument
(that raises `TypeError`). A string for an enum-valued query parameter is converted, and one that
is not a valid value raises `ValueError`.

| Resource      | Methods                                                      |
| ------------- | ------------------------------------------------------------ |
| `account`     | `get`                                                        |
| `bulk`        | `create`, `get`, `delete`, `list_screenshots`                |
| `fetch`       | `create`, `get`                                              |
| `fetch_bulk`  | `create`, `get`                                              |
| `monitors`    | `create`, `list`, `get`, `update`, `delete`, `list_captures` |
| `scrape`      | `create`, `get`                                              |
| `scrape_bulk` | `create`, `get`                                              |
| `screenshots` | `create`, `get`                                              |
| `search`      | `create`                                                     |
| `sitemap`     | `create`, `get`                                              |
| `usage`       | `get`, `get_daily`, `get_by_endpoint`                        |
| `webhooks`    | `list_deliveries`, `retry_delivery`                          |

Parameters keep the generated spelling: `from_` on `usage.get_daily` and
`usage.get_by_endpoint`, `async_` on `scrape.create` and `screenshots.create`, and `url_query`
(the `url` query parameter) on `fetch.get`. Date parameters take objects, not strings:
`datetime.date` for `from_` and `to` on the `usage` methods, `datetime.datetime` for `since` on
`webhooks.list_deliveries`.

## The generated client

`ByteKit` calls the generated operations in `bytekit.api.*`, which you can use directly when you
want the typed request models and the full response:

```python
from bytekit import AuthenticatedClient
from bytekit.api.scrape import create_scrape
from bytekit.models.scrape_request import ScrapeRequest
from bytekit.models.scrape_request_formats_item import ScrapeRequestFormatsItem
from bytekit.models.scrape_success_envelope import ScrapeSuccessEnvelope

client = AuthenticatedClient(
    base_url="https://api.bytekit.com",
    token="sk_live_your_api_key_here",
)

body = ScrapeRequest(
    url="https://example.com",
    formats=[ScrapeRequestFormatsItem.MARKDOWN],
)
response = create_scrape.sync(client=client, body=body)

# A documented error status (400, 429, 500, ...) comes back as an `Error` model, and a queued
# receipt as a ScrapeQueuedEnvelope. Only a ScrapeSuccessEnvelope carries populated formats.
if isinstance(response, ScrapeSuccessEnvelope) and isinstance(response.formats.markdown, str):
    print(response.formats.markdown)
else:
    print(f"Scrape not complete: {response}")
```

Each operation exposes four call styles:

- `create_scrape.sync(...)` — returns the parsed model.
- `create_scrape.sync_detailed(...)` — returns the full `Response` (status code, headers, body).
- `create_scrape.asyncio(...)` — awaitable form of `sync`.
- `create_scrape.asyncio_detailed(...)` — awaitable form of `sync_detailed`.

What a call does with a response it cannot turn into a documented model depends on the client's
`raise_on_unexpected_status`, which defaults to `True`:

- An undocumented status, a body that is not JSON or does not match the documented shape, or a
  `200` scrape carrying the `failed` envelope raises `bytekit.errors.UnexpectedStatus` (with `status_code`, `code` and `message`).
- `sync` and `asyncio` return `None` for those only when you construct the client with
  `raise_on_unexpected_status=False`.
- A transport failure never returns `None`: it raises the `httpx` exception.

### Async usage

```python
import asyncio
from bytekit import AuthenticatedClient
from bytekit.api.screenshots import create_screenshot
from bytekit.models.screenshot_request import ScreenshotRequest

async def main():
    client = AuthenticatedClient(
        base_url="https://api.bytekit.com",
        token="sk_live_your_api_key_here",
    )
    body = ScreenshotRequest(url="https://example.com")
    response = await create_screenshot.asyncio(client=client, body=body)
    print(response)

asyncio.run(main())
```

## Reference

The [Python SDK Reference](https://bytekit.com/docs/sdk/python/client) covers the generated client classes and operations
by resource. `ByteKit` and `ByteKitError` are documented on this page.

## Next steps

- [TypeScript SDK](https://bytekit.com/docs/libraries/typescript) — the same API surface in TypeScript.
- [API Reference](https://bytekit.com/docs/api) — the underlying REST endpoints.
- [Errors](https://bytekit.com/docs/guides/errors) — error codes and retry guidance.