Metadata-Version: 2.4
Name: brawsr
Version: 0.1.1
Summary: Python client for brawsr sessions, checkpoints, rewind, fork, and CDP handoff.
Project-URL: Homepage, https://brawsr.io
Project-URL: Documentation, https://docs.brawsr.io
Project-URL: Repository, https://github.com/brawsr/sdk-python
Project-URL: Issues, https://github.com/brawsr/sdk-python/issues
Author: brawsr
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: baas,brawsr,browser,cdp,playwright
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: build==1.5.0; extra == 'dev'
Requires-Dist: hatchling==1.31.0; extra == 'dev'
Requires-Dist: mypy>=1.14; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-requests>=2.32; extra == 'dev'
Provides-Extra: examples
Requires-Dist: playwright==1.61.0; extra == 'examples'
Description-Content-Type: text/markdown

# brawsr Python SDK

The official synchronous Python client for brawsr sessions, checkpoints,
rewind, fork, and CDP handoff. It turns asynchronous API operations into one bounded
method call; it does not bundle or proxy a browser library.

Published on PyPI as `brawsr`. Requires Python 3.11+.

## Install

```bash
pip install brawsr
```

Install your browser client separately, for example:

```bash
pip install playwright
```

## Session and browser connection

```python
from brawsr import BrawsrClient
from playwright.sync_api import sync_playwright

with BrawsrClient() as brawsr:  # reads BRAWSR_API_KEY
    session = brawsr.create_session(ttl_seconds=300, display_label="checkout retry")
    try:
        with sync_playwright() as playwright:
            connection = brawsr.connect_cdp(session)
            browser = playwright.chromium.connect_over_cdp(
                connection.endpoint_url,
                headers=connection.headers,
            )
            page = browser.contexts[0].pages[0]
            page.goto("https://example.com")
            print(page.title())
    finally:
        brawsr.close_session(session.id)
```

The production API endpoint is built in as `https://api.brawsr.io`. Pass
`base_url="http://localhost:8080"` explicitly when testing against another
deployment. The client can also receive `api_key` explicitly instead of reading
`BRAWSR_API_KEY`.

`connect_cdp` returns immutable connection data. The SDK does not retain the
API key in a browser object or speak CDP itself.

`get_session(id)` returns `cdp_url` only while the session is attachable, so a
restarted process can fetch an active session and pass it directly to
`connect_cdp`. Calling `connect_cdp` with a closed or expired `Session` raises a
clear local error.

## Session workspace

```python
# Pages use opaque cursors; iterators fetch each page lazily.
for session in brawsr.iterate_sessions(status="active"):
    print(session.id, session.display_label, session.attachable)

brawsr.update_session(session.id, display_label="")  # clear the label

# Capture history is an audit view and can include deleted checkpoints.
captures = brawsr.list_checkpoints(session.id)

# Ancestry contains the current rewind candidates. The server revalidates the
# selected capture when rewind is requested.
capture = next(brawsr.iterate_ancestry(session.id))
restored = brawsr.rewind(session.id, capture)

activity = brawsr.list_activity(session.id)
lineage = brawsr.get_lineage(session.id)
for child in brawsr.iterate_lineage_children(session.id):
    print(child.ordinal, child.session.id)
```

Session lists are newest-first. They support exact `status`, `display_label`,
and `session_id` filters, case-insensitive label `search`, and inclusive
`created_from` / exclusive `created_before` creation-time bounds. `get_session`
and `update_session` return `SessionDetail`, including collection counts,
an optional active lifecycle operation, and the canonical collection paths.
`get_lineage` returns one hop: the selected session, its optional parent edge,
and a bounded page of direct children. Fetch a child session's lineage to
expand a nested fork tree.

## Checkpoint and rewind

```python
checkpoint = brawsr.create_checkpoint(
    session.id,
    label="before-submit",
    timeout=30.0,
)

# Continue browser work. This state is intentionally newer than the checkpoint.
page.fill("textarea", "draft that should be discarded")

# A label, checkpoint ID, Capture, Checkpoint, or CheckpointResult is accepted.
restored = brawsr.rewind(session.id, checkpoint)

# Rewind replaces the browser process. The old Browser/Page handles are stale.
# Connect again and rediscover the default context and its pages.
connection = brawsr.connect_cdp(restored)
browser = playwright.chromium.connect_over_cdp(
    connection.endpoint_url,
    headers=connection.headers,
)
restored_context = browser.contexts[0]
restored_pages = restored_context.pages

# The early rewind result is already CDP-usable. Wait only before another
# lifecycle mutation on this same session (for example checkpoint or close).
brawsr.wait_rewind(restored.operation_id)
```

`create_checkpoint`, `rewind`, `fork`, and `delete_checkpoint` each send one mutation.
When the API returns `202 Accepted`, the SDK polls only the operation resource
and returns the final result. Callers do not need to implement polling.

Checkpoint labels are case-sensitive and unique among live checkpoints in a
session. A label that has the checkpoint-ID shape is rejected; use the returned
ID when an immutable reference is required.

## Resources and operations

- `create_session`, `list_sessions`, `iterate_sessions`, `get_session`,
  `update_session`, `close_session`
- `create_checkpoint`, `get_checkpoint`, `list_checkpoints`,
  `iterate_checkpoints`, `delete_checkpoint`
- `list_ancestry`, `iterate_ancestry`
- `list_activity`, `iterate_activity`
- `get_lineage`, `iterate_lineage_children`
- `rewind`
- `fork`, `close_sessions`
- `get_operation`, `wait_operation`, `wait_checkpoint`, `wait_rewind`,
  `wait_fork`
- `connect_cdp`

Collection iterators are lazy and fetch one bounded page at a time. Cursors are
opaque and must be passed back unchanged.

Every waiter accepts a timeout and optional `threading.Event`. Stopping a
waiter does not cancel work already admitted by the server. The
operation-specific waiters return the same typed result as the corresponding
mutation, including the restored CDP URL after rewind; callers never need to
decode raw operation results. `get_operation` and `wait_operation` remain
available for generic observability.

## Fork and explicit cleanup

```python
forked = brawsr.fork(
    session.id,
    checkpoint,
    n=3,
    ttl_seconds=300,
    timeout=30.0,
)

for child in forked.children:
    connection = brawsr.connect_cdp(child)
    # Each child is an independent session. Attach with your browser library.
    print(child.branch_index, child.session_id, connection.endpoint_url)

outcomes = brawsr.close_sessions(forked, concurrency=3)
for outcome in outcomes:
    if not outcome.ok:
        print("close failed", outcome.session_id, outcome.error)
```

The returned children are immutable and ordered by `branch_index`. The source
session remains open. Children may checkpoint, rewind, or fork again; there is
no browser-state merge. `close_sessions` never selects a winner, closes the
source, or hides partial failures. It accepts a fork result, child objects, or
session IDs and preserves input order in its outcome tuple.

By default, cleanup requests use isolated HTTP sessions so concurrency does
not share mutable `requests.Session` state. A caller that injects a custom
`session` may also inject a `session_factory`; without one, cleanup safely
serializes while retaining identical ordered outcomes.

## Errors and recovery

- `BrawsrError`: common base for API, transport, operation, response, timeout,
  cancellation, and closed-client failures.
- `BrawsrApiError`: safe API envelope with `status_code`, stable `code`,
  `request_id`, optional `operation_id`, `retryable`, and `retry_after_ms`.
- `BrawsrResponseError`: the API returned JSON that does not match the public
  response contract.
- `BrawsrOperationError`: the server operation reached `failed`.
- `BrawsrWaitTimeoutError` / `BrawsrWaitCancelledError`: local waiting stopped;
  server work may still complete. For an admitted mutation, `operation_id` and
  `idempotency_key` remain available programmatically for recovery.
- `BrawsrTransportError`: the transport outcome is ambiguous. Its
  `idempotency_key` is available programmatically for recovery but is omitted
  from the message.

The client does not retry an ambiguous mutation by default. Advanced callers
may provide `is_pre_response_connection_error`; only a definitely pre-response
failure is retried, at most once, with the same idempotency key.

Resume a timed-out rewind without resending it:

```python
try:
    restored = brawsr.rewind(session.id, checkpoint, timeout=1.0)
except BrawsrWaitTimeoutError as error:
    if error.operation_id is None:
        raise
    restored = brawsr.wait_rewind(error.operation_id, timeout=30.0)

connection = brawsr.connect_cdp(restored)
browser = playwright.chromium.connect_over_cdp(
    connection.endpoint_url,
    headers=connection.headers,
)
```

Rewind restores browser/client state, not side effects already committed by a
remote website. Long-lived WSS, SSE, or WebRTC connections may need
application-level reconnection after restore.

See `examples/rewind_playwright.py` for an executable reconnect story that
rejects a stale page handle and rediscovers the restored pages. A classic
Selenium WebDriver session cannot be rebound to the replacement browser in
v0.1; a later WebDriver/BiDi adapter owns that contract.

## Development

```bash
python -m pip install uv==0.10.9
uv sync --locked --all-extras
uv run ruff format --check src tests scripts examples
uv run ruff check src tests scripts examples
uv run mypy src
uv run python scripts/verify_contract.py
uv run pytest -q
uv run python scripts/verify_package.py
```

The package check uses the release-pinned Python 3.11 + Hatchling 1.31.0
toolchain, builds each archive twice with a fixed source epoch, compares
contents and SHA-256, and imports the wheel from a clean temporary install. Use
Python 3.11 for this release-only gate; the installed SDK remains supported on
newer Python versions.
