# omnilogin-python-sdk

Async Python SDK for OmniLogin desktop. Two transports:

- REST over HTTP via `httpx.AsyncClient`: profile/group/proxy CRUD, launching, workflows, AI Apps, IPv6.
- Bridge over WebSocket JSON-RPC via `websockets`: Playwright-style page automation, tabs, cookies, and host services.

All I/O is async. Use `async`/`await` inside an event loop. Requires Python 3.10+.

For exhaustive API docs see `API.md`. For overview examples see `README.md`.

## Install

```bash
pip install omnilogin-python-sdk
```

## AI Assistant Skill Bundle

Before generating OmniLogin automation scripts in a project, install the bundled skill:

```bash
python -m omnilogin init
```

This copies `.claude/skills/omni-sdk/` into the current project. Set `OMNILOGIN_QUIET=1` to silence the runtime hint.

## Compatibility Scope

- Supported public surface: `OmniLogin`, `RestApi`, `Session`, bridge classes, cookie helpers, and exported types/models from `omnilogin`.
- Keep TypeScript-style camelCase method names where they are SDK contract names (`closeAll`, `setTags`, `getByText`, `waitForURL`, etc.).
- Python adds a few Pythonic aliases such as `close_all`, `parse_cookies`, `to_netscape`.
- Do not invent removed pre-publish names such as `OmniLoginClient`, `OmniClient`, `omni.api`, `openAndConnect()`, or `getActivePage()`.

## Quick Start

```python
import asyncio

from omnilogin import OmniLogin


async def main() -> None:
    async with OmniLogin() as omni:
        profile_id = 1
        opened = await omni.open(profile_id)
        session = opened.session

        await session.page.goto("https://example.com")
        await session.page.locator("#email").fill("user@example.com")
        await session.page.locator("#password").fill("secret")
        await session.page.locator('button[type="submit"]').click()

        print(await session.page.title(), opened.result.bridge_url)
        await omni.close(profile_id)


asyncio.run(main())
```

`await omni.open(profileId)` returns `OpenSessionResult` with `.session` and `.result`; it also supports tuple unpacking:

```python
session, result = await omni.open(1)
```

## Top-Level Exports

Primary classes:

- `OmniLogin`
- `RestApi`
- `Session`
- `Page`
- `Locator`
- `Keyboard`
- `Mouse`
- `Tabs`
- `Cookies`
- `PageBrowser`
- `Services`
- `RpcError`
- `OmniLoginError`

Primary types/models:

- REST: `OmniLoginOptions`, `OpenSessionOptions`, `OpenSessionResult`, `RestApiOptions`, `ListProfilesQuery`, `UpdateProfileInput`, `ListGroupsQuery`, `ListProxiesQuery`, `RunAiAppOptions`
- Domain: `OpenResult`, `Profile`, `Account`, `Fingerprints`, `CreateProfileInput`, `CloneProfileInput`, `UpdateFingerprintInput`, `ProxyInput`, `Proxy`, `Group`, `PaginatedResult`, `OpenOptions`
- Bridge/services: `HttpMethod`, `HttpRequestOptions`, `HttpResponse`, `TelegramSendOptions`, `AiOptions`, `AiVisionImage`, `AiImageOptions`, `AiImageResult`, `ImageSearchOptions`, `ImageMatch`, `SheetsGetOptions`, `SheetsUpdateOptions`, `SheetsAppendOptions`, `SheetsCreateResult`, `SheetsAddSheetResult`, `FileExportOptions`, `FileDownloadOptions`, `FileReadLinesOptions`, `FileSaveElementAssetsOptions`, `FileSaveElementAssetsResult`, `EmailReadOptions`, `EmailMessage`, `BridgeProfileInfo`, `InlineProxy`, `ProfileCloneOptions`, `ProfileCloneResult`, `ProfileSwitchProxyOptions`, `SpreadsheetReadOptions`, `SpreadsheetWriteOptions`, `ClipboardReadOptions`, `ClipboardWriteOptions`, `NotificationShowOptions`, `TotpResult`, `KeyboardTypeOptions`, `ClickOptions`, `MouseButton`, `TabInfo`, `NewPageOptions`, `Cookie`, `OriginStorage`, `StorageState`
- Cookie helpers: `parseCookies`, `parse_cookies`, `toBridgeCookies`, `to_bridge_cookies`, `toNetscape`, `to_netscape`, `CookieFormat`

## OmniLogin

```python
OmniLogin(options: OmniLoginOptions | None = None, *, host: str | None = None, timeout: float | None = None)
```

Defaults:

- `host`: `http://localhost:35353`
- `timeout`: `30000` milliseconds; converted internally to seconds for `httpx`

Properties:

- `rest: RestApi`
- `profiles`, `groups`, `proxies`, `workflows`, `aiApps`, `ipv6` shortcuts to `rest.*`

Methods:

- `open(profileId, opts=None, **kwargs)` -> `OpenSessionResult`
- `close(profileId)` -> `None`
- `closeAll()` -> `None`
- `close_all()` -> `None`
- `aclose()` -> `None`

Use `async with OmniLogin() as omni:` to call `aclose()` automatically.

## RestApi

```python
RestApi(options: RestApiOptions | None = None, *, host: str | None = None, timeout: float | None = None)
```

Namespaces:

- `profiles`
- `groups`
- `proxies`
- `workflows`
- `aiApps`
- `browser`
- `ipv6`

### profiles

- `list(query?: { page?, pageSize?, q?, sort?, sortType? })`
- `get(id)`
- `create(data)`
- `clone(id, data=None)`
- `delete(id, deleteData=False)`
- `update(id, patch)`
- `setTags(ids, tags, append=True)`
- `assignProxy(proxyId, profileIds)`
- `setEmbeddedProxy(profileIds, proxy)`

`profiles.update()` routes each field to its dedicated PUT endpoint. Fields are sent sequentially and not atomically.

### groups

- `list(query=None)`
- `get(id)`
- `create(name)`
- `update(id, name)`
- `delete(id)`

### proxies

- `list(query=None)`
- `get(id)`
- `create(data)`
- `update(id, data)`
- `delete(id)`

### workflows

- `start(workflowId, opts=None)`
- `stop(workflowId)`
- `stopAll()`
- `status(taskId)`

### aiApps

- `list()`
- `get(appId)`
- `run(appId, opts=None)`
- `stop(appId)`

### browser

- `open(profileId, opts=None, **kwargs)`
- `stop(profileId)`
- `isActive(profileId)`

`OpenOptions` supports `launchBridge`, `remoteDebugPort`, `headless`, `scale`, `additionArgs`.

### ipv6

- `rotate(proxyId)`

## Session

One live bridge connection to one running browser profile.

```python
Session(bridgeUrl: str)
```

Methods:

- `connect()`
- `disconnect()`

Properties:

- `connected: bool`
- `page: Page`
- `tabs: Tabs`
- `cookies: Cookies`
- `services: Services`
- `rpc: RpcClient`

Use `new Session(bridge_url)` / `Session(bridge_url)` only when the bridge was started outside `OmniLogin.open()`. Otherwise prefer `omni.open(profileId)`.

## Page

Navigation and state:

- `goto(url, options=None, **kwargs)`
- `goBack(options=None, **kwargs)`
- `goForward(options=None, **kwargs)`
- `reload(options=None, **kwargs)`
- `url()`
- `title()`
- `content()`

Wait helpers:

- `waitForLoadState(state=None)`
- `waitForURL(url, options=None, **kwargs)`
- `waitForRequest(url, options=None, **kwargs)`
- `waitForResponse(url, options=None, **kwargs)`
- `waitForFunction(expression, options=None, **kwargs)`
- `waitForTimeout(ms)`

Locators:

- `locator(selector)`
- `getByText(text)`
- `getByRole(role)`
- `getByLabel(text)`
- `getByPlaceholder(text)`
- `getByTestId(testId)`
- `getByAltText(text)`
- `getByTitle(text)`

Input and snapshots:

- `keyboard`
- `mouse`
- `screenshot(options=None, **kwargs)`
- `accessibilitySnapshot(options=None, **kwargs)`
- `markdownSnapshot(options=None, **kwargs)`

Network, frames, dialogs:

- `setExtraHTTPHeaders(headers)`
- `route(url, {"action": "abort" | "continue" | "fulfill", ...})`
- `unroute(url=None)`, `unrouteAll()`
- `frame(nameOrUrl)`, `frames()`
- `acceptDialog(promptText=None)`, `dismissDialog()`

Evaluate escape hatches:

- `evaluate(expression, *args)`
- `evaluateHandle(expression)`

Python cannot serialize Python callables to JavaScript. Pass JavaScript source strings.

Events:

- `on(event, handler)`

Raw protocol access:

- `page.rpc.call(method, params=None, timeout=None)`
- `page.rpc.notify(method, params=None)`
- `page.rpc.on(event, handler)`, `page.rpc.off(event, handler)`

Compatibility shim:

- `page.browser` is a `PageBrowser` facade kept for AI App script compatibility.
- New SDK code should prefer `session.tabs` and `session.cookies` directly.

## Locator

Actions:

- `click`, `dblclick`, `hover`, `fill`, `clear`, `check`, `uncheck`, `press`, `pressSequentially`, `focus`, `blur`, `selectOption`, `setInputFiles`, `dragTo`, `scrollIntoViewIfNeeded`, `dispatchEvent`

Reads:

- `textContent`, `innerText`, `innerHTML`, `getAttribute`, `inputValue`, `boundingBox`, `count`, `allInnerTexts`, `allTextContents`, `screenshot`

State:

- `isVisible`, `isEnabled`, `isChecked`, `isEditable`

Wait/narrow/evaluate:

- `waitFor`
- `locator`, `first`, `last`, `nth`, `filter`, `all`
- `evaluate`, `evaluateAll`

Humanization is enabled by default for `click`, `fill`, `hover`, `dblclick`, `check`, `uncheck`, and `pressSequentially`. Pass `{"humanize": False}` to disable it.

`locator.setInputFiles(files)` accepts absolute local paths, `http(s)://...`, `data:<mime>;base64,...`, and `<filename>|<url-or-data>`. Pass `[]` to clear.

## Tabs

- `open(url=None, options=None)`
- `close(targetId=None)`
- `list()` -> `{ "pages": list[TabInfo] }`
- `activate(targetId)`
- `next()`
- `previous()`

## Cookies

- `list(urls=None)`
- `add(cookies)`
- `clear()`
- `getStorageState()`
- `setStorageState(state=None, *, cookies=None, origins=None)`
- `exportNetscape(urls=None)` -> Netscape cookies.txt string

Cookie helper functions:

- `parseCookies(input, opts=None, *, format=None, defaults=None)`
- `parse_cookies`
- `toBridgeCookies`
- `to_bridge_cookies`
- `toNetscape`
- `to_netscape`
- `CookieFormat`

`parseCookies()` accepts bridge `Cookie[]`, Playwright `{ cookies: [...] }`, single cookie objects, JSON strings, Netscape `cookies.txt`, and HTTP `Cookie:` header strings. It returns `[]` on unparseable input.

## PageBrowser Compatibility Shim

Available via `page.browser`:

- `newPage(url=None, options=None)`
- `closePage(targetId=None)`
- `pages()`
- `bringToFront(targetId)`
- `nextPage()`
- `previousPage()`
- `context()`

`page.browser.context()` exposes:

- `cookies(urls=None)`
- `addCookies(cookies)`
- `clearCookies()`
- `storageState()`
- `setStorageState(state)`

## Services

Host-side helpers exposed as `session.services`.

### sheets

- `get(spreadsheetId, range, opts=None)`
- `update(spreadsheetId, range, values, opts=None)`
- `append(spreadsheetId, range, values, opts=None)`
- `clear(spreadsheetId, range)`
- `create(title)`
- `addSheet(spreadsheetId, title)`

### file

- `read(path)`
- `readLines(path, opts=None)`
- `write(path, content)`
- `export(data, opts)`
- `download(url, opts=None)`
- `saveElementAssets(selectorChain, folder, opts=None)`

`file.download()` returns the saved path when `opts["path"]` is provided, otherwise the response body as base64.

### email

- `read({ host, port, user, password?, accessToken?, clientId?, refreshToken?, tls?, tlsInsecure?, folder?, since?, from_?, to?, subject?, body?, unreadOnly?, markAsRead?, bodyRegex?, bodyRegexFlags?, limit? })`

Use `from_` in Python input where TypeScript uses `from`; the SDK maps it to `"from"` before calling the bridge.

Auth modes:

- basic: `password`
- generic XOAUTH2: `accessToken`
- Outlook OAuth: `clientId` + `refreshToken`

### profile

- `info()`
- `setTags(tags)`
- `clone(opts=None)`
- `switchProxy(opts)`

### extension

- `trigger(extensionId)`

### spreadsheet

- `read(path, opts=None)`
- `write(path, values, opts=None)`
- `clear(path, range)`

### clipboard

- `read(opts=None)`
- `write(text, opts=None)`

### notification

- `show(title, opts=None)`

### totp

- `generate(secret)`
- `code(secret)`

### direct callables

- `http(method, url, opts=None, **kwargs)`
- `telegram(message, opts=None, **kwargs)`
- `ai(prompt, opts=None, **kwargs)`
- `aiImage(prompt, opts=None, **kwargs)`
- `imageSearch(template, opts=None, **kwargs)`

## Selector Syntax

Supported patterns:

- CSS: `#id`, `.class`, `div > span`
- `text=`
- `label=`
- `placeholder=`
- `role=`
- `testid=`
- `xpath=`
- `css=`
- chained locators via `locator(...).locator(...)`

Locator factories on `page`: `locator()`, `getByText()`, `getByLabel()`, `getByPlaceholder()`, `getByRole()`, `getByTestId()`, `getByAltText()`, `getByTitle()`.

## Method Priority

When generating or reviewing automation code, prefer this order:

1. First-class SDK methods: `page.locator()`, `page.getBy*()`, `page.goto()`, `page.waitFor*()`, `accessibilitySnapshot()`, `markdownSnapshot()`.
2. Raw CDP via `page.rpc.call()` / `page.rpc.on()` if the SDK does not expose the capability.
3. Runtime JS strings such as `page.evaluate()`, `locator.evaluate()`, `locator.evaluateAll()`, `page.waitForFunction()` only as the last resort.

Prefer dedicated methods over runtime JS:

- `page.url()` over `page.evaluate("() => location.href")`
- `page.title()` over `page.evaluate("() => document.title")`
- `locator.textContent()` over `locator.evaluate("(el) => el.textContent")`
- `locator.count()` over `page.evaluate("() => document.querySelectorAll(...).length")`

## Defaults

- `click`, `fill`, `hover`, `dblclick`, `check`, `uncheck`, `pressSequentially` are humanized by default.
- Every I/O method is async.
- Selector chaining narrows scope.

## Errors

```python
from omnilogin import OmniLoginError, RpcError

try:
    await session.page.locator("#missing").click()
except RpcError as exc:
    print(exc.code, exc.message, exc.data)
except OmniLoginError as exc:
    print(str(exc))
```

`RpcError` exposes `code`, `message`, and `data`.

## Source Layout

Package source lives in `src/omnilogin/`.

Public imports are from package root:

```python
from omnilogin import OmniLogin, Session, RpcError, OmniLoginError
```

Do not import from `src.omnilogin`.
