Metadata-Version: 2.5
Name: bytekit-sdk
Version: 0.3.8
Summary: Official Python SDK for the ByteKit API
Project-URL: Homepage, https://bytekit.com
Project-URL: Repository, https://github.com/Hunt-Labs-Inc/ByteKit
Project-URL: Documentation, https://bytekit.com/docs
License-Expression: MIT
License-File: LICENSE
Keywords: api,bytekit,crawler,llm,markdown,scraping,screenshots,sdk,web-scraping
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: attrs>=23.1.0
Requires-Dist: httpx>=0.24.0
Requires-Dist: python-dateutil>=2.8.0
Description-Content-Type: text/markdown

# bytekit-sdk

Official Python SDK for the [ByteKit API](https://bytekit.com).

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

Generated from the OpenAPI spec using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client), filtered to the stable v0.1 operations.

## Installation

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

The installed version is available as `bytekit.__version__`.

## Quick start

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

# base_url defaults to https://api.bytekit.com, so only the token is required.
client = AuthenticatedClient(token="sk_live_your_api_key_here")

result = create_scrape.sync(
    client=client,
    body=ScrapeRequest(
        url="https://example.com",
        # Ask for markdown explicitly. `formats` is a list of enum members, not plain
        # strings. Omit it and the server returns raw HTML instead, leaving
        # `formats.markdown` unset.
        formats=[ScrapeRequestFormatsItem.MARKDOWN],
    ),
)

if isinstance(result, ScrapeSuccessEnvelope):
    print(result.formats.markdown)
```

See [Error handling](#error-handling) for what `result` can be when the request fails.

### Error handling

The SDK has a **two-tier** error contract. Both tiers are safe: an operation never raises
a bare `json.JSONDecodeError` or a bare `KeyError`, even when a server returns an HTML error
page or a payload whose shape has drifted from the spec.

| Situation                                                                                                                                       | What you get                                                                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A status the OpenAPI spec **documents** for that operation (e.g. a `422` or `500` on `create_scrape`), with a JSON body of the documented shape | The typed `Error` model is **returned**, not raised. Check with `isinstance(result, Error)` and read `result.error.code` / `result.error.message`.                                 |
| An **undocumented** status, or a **non-JSON body on a documented status** (HTML error page, load-balancer text, empty body)                     | `errors.UnexpectedStatus` is **raised**, carrying `.status_code` and the raw `.content`. When the body is the documented `Error` envelope, `.code` / `.message` are populated too. |
| A **documented status whose JSON body does not match the documented schema** — a field the server renamed, dropped or retyped                   | `errors.UnexpectedStatus` is **raised**, same shape as the row above. The underlying parse failure (e.g. `KeyError: 'schema_version'`) is attached as `__cause__` for diagnosis.   |
| Any of the above, with `raise_on_unexpected_status=False`                                                                                       | Nothing is raised; the operation returns `None`.                                                                                                                                   |

The table describes the **generated operations** (`create_scrape.sync`, `get_usage.sync`, …),
whose return types are `Optional[...]` accordingly. The hand-written
`AuthenticatedClient.search(...)` convenience method is deliberately outside it: it returns a
non-`Optional` `CreateSearchResponse200` and raises `errors.UnexpectedStatus` on anything else
— including documented error statuses, and including a malformed `200` — **in both raise
modes**. `raise_on_unexpected_status` does not apply to it.

```python
from bytekit import AuthenticatedClient
from bytekit.api.scrape import create_scrape
from bytekit.errors import UnexpectedStatus
from bytekit.models.error import Error
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(token="sk_live_your_api_key_here")

try:
    result = create_scrape.sync(
        client=client,
        body=ScrapeRequest(
            url="https://example.com",
            # Ask for markdown explicitly — omit `formats` and the server returns raw HTML,
            # leaving `formats.markdown` unset.
            formats=[ScrapeRequestFormatsItem.MARKDOWN],
        ),
    )
except UnexpectedStatus as err:
    # Undocumented status, or a non-JSON body on a documented one. Never a JSONDecodeError.
    print(f"request failed with status {err.status_code}: {err.code} {err.message}")
else:
    if isinstance(result, Error):
        # Documented 4xx/5xx with a JSON body: RETURNED as a typed model, not raised.
        print(f"api error {result.error.code}: {result.error.message}")
    elif isinstance(result, ScrapeSuccessEnvelope):
        print(result.formats.markdown)
    else:
        # 202 ScrapeQueuedEnvelope — poll get_scrape with this id until it completes.
        print(f"queued as {result.id}")
```

### Defaults

The client ships with production-ready defaults so `AuthenticatedClient(token=...)` works out of the box:

| Setting                      | Default                         | Notes                                                                                                                                                           |
| ---------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `base_url`                   | `https://api.bytekit.com`       | Pass `base_url=` to target staging or a proxy.                                                                                                                  |
| `timeout`                    | `120s` (`httpx.Timeout(120.0)`) | Finite by default — requests no longer hang indefinitely. Pass `timeout=` to override.                                                                          |
| `raise_on_unexpected_status` | `True`                          | Undocumented statuses raise `errors.UnexpectedStatus` instead of silently returning `None`. Pass `raise_on_unexpected_status=False` to restore the old opt-out. |

Explicit constructor arguments always win over these defaults (explicit arg > default).

#### Every constructor argument is keyword-only

`AuthenticatedClient` accepts **no positional arguments**. `token`, `base_url`, `prefix`,
`auth_header_name` and the rest are all passed by name:

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

**Migrating from a pre-0.3.0 positional call.** `base_url` used to be the first positional
argument, so `AuthenticatedClient("https://…", "sk_live_…")` was a documented call. Once
`base_url` became a defaulted keyword argument, that same call silently bound the **URL to
`token`** and the **API key to `prefix`** — producing an `Authorization: sk_live_… https://…`
header sent to the _default_ host, i.e. a wrong credential against production with nothing
raised. Since 0.3.5 it raises `TypeError` instead. Add the keywords; nothing else changes.

## 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(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())
```

### Clients and event loops

An `httpx.AsyncClient` — and therefore its connection pool — belongs to the event loop that
created it. The recommended shape is a context manager, which scopes the client to exactly one
loop:

```python
async def main():
    async with AuthenticatedClient(token="sk_live_your_api_key_here") as client:
        ...
```

Two rules cover everything else:

- **A client the SDK builds for you is rebuilt automatically.** If you reuse one client across
  several `asyncio.run(...)` calls, the SDK notices the running loop has changed and transparently
  replaces its internal `AsyncClient`. Your `base_url`, headers, timeout, `httpx_args` and
  authentication are all re-applied, so this is invisible apart from a new connection. Calling
  `get_async_httpx_client()` outside any running loop returns the cached client unchanged.
- **A client you pass to `set_async_httpx_client(...)` is yours.** The SDK never rebuilds or
  closes it, so a client you supply must be created and used on the same loop — that is the one
  case where crossing loops is still your responsibility.

## Available operations

| Module            | Method                                                                                                        | Description                                |
| ----------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `api.scrape`      | `create_scrape`, `get_scrape`                                                                                 | Web content extraction                     |
| `api.screenshots` | `create_screenshot`, `get_screenshot`                                                                         | Page screenshots                           |
| `api.bulk`        | `create_bulk`, `get_bulk`, `delete_bulk`, `list_bulk_screenshots`                                             | Bulk screenshot jobs                       |
| `api.scrape_bulk` | `create_scrape_bulk`, `get_scrape_bulk`                                                                       | Bulk scrape jobs                           |
| `api.fetch`       | `get_fetch`, `post_fetch`                                                                                     | Raw HTTP fetch                             |
| `api.fetch_bulk`  | `create_fetch_bulk`, `get_fetch_bulk`                                                                         | Bulk fetch jobs                            |
| `api.monitors`    | `create_monitor`, `list_monitors`, `get_monitor`, `update_monitor`, `delete_monitor`, `list_monitor_captures` | Page-change monitors (screenshot + scrape) |
| `api.sitemap`     | `create_sitemap`, `get_sitemap`                                                                               | Sitemap crawl                              |
| `api.search`      | `create_search` (or the `AuthenticatedClient.search(...)` convenience method)                                 | Web search                                 |
| `api.usage`       | `get_usage`, `get_usage_daily`, `get_usage_by_endpoint`                                                       | Account usage & billing                    |
| `api.webhooks`    | `list_webhook_deliveries`, `retry_webhook_delivery`                                                           | Webhook delivery log & retry               |
| `api.account`     | `get_account`                                                                                                 | Account details                            |

## License

MIT — see [LICENSE](./LICENSE).
