Metadata-Version: 2.4
Name: wellmarked
Version: 1.0.0
Summary: Official Python SDK for the WellMarked API — convert any URL to clean Markdown.
Project-URL: Homepage, https://wellmarked.io
Project-URL: Documentation, https://wellmarked.io/docs
Author-email: WellMarked <support@wellmarked.io>
License: MIT
License-File: LICENSE
Keywords: extraction,html-to-markdown,llm,markdown,pipeline,rag,scraping,wellmarked
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
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.9
Requires-Dist: httpx<1.0,>=0.25
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# wellmarked

[![PyPI](https://img.shields.io/pypi/v/wellmarked.svg)](https://pypi.org/project/wellmarked/)
[![Python](https://img.shields.io/pypi/pyversions/wellmarked.svg)](https://pypi.org/project/wellmarked/)

Official Python SDK for the **[WellMarked](https://wellmarked.io)** API — convert any URL to clean Markdown.

```bash
pip install wellmarked
```

## Quick start

```python
from wellmarked import WellMarked

with WellMarked(api_key="wm_...") as wm:
    result = wm.extract("https://example.com/article")
    print(result.markdown)
    print(result.metadata.title, "by", result.metadata.author)
```

The API key can also be picked up from the `WELLMARKED_API_KEY` environment variable, in which case `WellMarked()` is enough.

Get a key at [wellmarked.io](https://wellmarked.io).

## Pricing

|                       | Free      | Pro                  | Enterprise    |
|-----------------------|-----------|----------------------|---------------|
| **Price**             | $0        | $19/mo               | $99/mo        |
| **Included Requests** | 500/mo    | 5,000/mo             | 30,000/mo     |
| **Bulk Requests**     | ❌         | ✅ (up to 50/request) | ✅ (Unlimited) |
| **Overage Rate**      | —         | $0.004/req           | $0.002/req    |
| **JS Rendering**      | ❌         | ✅                    | ✅             |
| **Priority Queue**    | Standard  | High                 | Highest       |

See additional pricing information at [WellMarked](http://wellmarked.io/#pricing)

## Async

`AsyncWellMarked` is a drop-in async equivalent — every endpoint method is a coroutine.

```python
import asyncio
from wellmarked import AsyncWellMarked

async def main():
    async with AsyncWellMarked() as wm:
        result = await wm.extract("https://example.com/article")
        print(result.markdown)

asyncio.run(main())
```

## Bulk extraction

Submit many URLs at once (Pro: up to 50; Enterprise: unlimited). The call returns immediately with a `job_id`. Poll with `get_job` or block until done with `wait_for_job`.

```python
job = wm.bulk([
    "https://example.com/article-1",
    "https://example.com/article-2",
])
job = wm.wait_for_job(job.job_id)         # blocks until status == "done"

for item in job.results:
    if item.ok:
        print(item.metadata.title)
    else:
        print(f"{item.url} failed: {item.error}")
```

## Usage & rate limits

`get_usage()` is the source of truth for your current-period quota. The quota state belongs on the account, so call `get_usage()` when you want it:

```python
usage = wm.get_usage()
print(f"{usage.used} / {usage.limit} used this period ({usage.plan}) — {usage.remaining} left")
```

`GET /usage` itself does not count toward your quota.

## Key rotation

```python
rotated = wm.rotate_key()
print("New key:", rotated.api_key)  # shown once — store it before the program exits
```

After `rotate_key()` the client automatically switches to the new key for subsequent calls; you still need to persist `rotated.api_key` somewhere durable, because the previous key stops working immediately and there is no recovery flow.

## Errors

Every non-2xx response is translated into a typed exception. Catch the base class to handle anything, or the specific subclass to handle one failure mode:

```python
from wellmarked import (
    WellMarked,
    AuthenticationError,
    PermissionDeniedError,
    NotFoundError,
    UnprocessableEntityError,
    RateLimitError,
    APIConnectionError,
)

with WellMarked() as wm:
    try:
        result = wm.extract("https://example.com/paywalled")
    except RateLimitError as e:
        print(f"Quota hit. Resets in {e.retry_after}s.")
    except UnprocessableEntityError as e:
        # e.code is one of: no_content, target_timeout, js_rendering_disabled, ...
        print(f"Extraction failed ({e.code}): {e.message}")
```

| Exception                  | HTTP | Typical `code` values                                                        |
|----------------------------|------|------------------------------------------------------------------------------|
| `AuthenticationError`      | 401  | `missing_api_key`, `invalid_api_key`                                         |
| `PermissionDeniedError`    | 403  | `account_inactive`, `plan_not_supported`, `forbidden`                        |
| `NotFoundError`            | 404  | `job_not_found`                                                              |
| `UnprocessableEntityError` | 422  | `no_content`, `target_timeout`, `js_rendering_disabled`, `bulk_cap_exceeded` |
| `RateLimitError`           | 429  | `rate_limit_exceeded`  *(carries `retry_after` in seconds)*                  |
| `InternalServerError`      | 5xx  | —                                                                            |
| `APIConnectionError`       | —    | DNS / TCP / TLS / timeout failures, raised before any HTTP round-trip        |

All inherit from `WellMarkedError`.

## Configuration

```python
WellMarked(
    api_key="wm_...",                   # or set WELLMARKED_API_KEY
    base_url="https://api.wellmarked.io",
    timeout=30.0,                       # seconds, per request
    http_client=my_httpx_client,        # optional: bring your own httpx.Client
)
```

Passing your own `httpx.Client`/`httpx.AsyncClient` is useful for custom transports, proxies, or shared connection pools. When you do, the SDK won't close it on `__exit__` — you remain responsible for its lifecycle.

## For Agents

If you are an agent, feel free to find additional context [here](https://wellmarked.io/llms.txt)!

## License

MIT
